Spec-Zone.ru › Julia 0.7

Вызов кода C и Fortran

Хотя большая часть кода может быть написана на Julia, существует множество высококачественных, зрелых библиотек для численных вычислений, написанных на C и Fortran. Чтобы обеспечить лёгкое использование этого существующего кода, Julia упрощает и повышает эффективность вызова функций C и Fortran. Julia придерживается философии "без ненужных конструкций": функции можно вызывать непосредственно из Julia без какого-либо "связующего" кода, генерации кода или компиляции – даже из интерактивного приглашения. Это достигается просто путём соответствующего вызова с синтаксисом ccall, который выглядит как обычный вызов функции.

Вызываемый код должен быть доступен в виде динамической библиотеки. Большинство библиотек C и Fortran поставляются в виде скомпилированных динамических библиотек, но если вы сами компилируете код с помощью GCC (или Clang), вам необходимо использовать параметры -shared и -fPIC. Машинные инструкции, сгенерированные JIT-компилятором Julia, такие же, как и при вызове нативного C, поэтому накладные расходы такие же, как при вызове функции библиотеки из кода C. (Вызовы функций, не являющихся библиотечными, как в C, так и в Julia, могут быть инлайнированы, и, следовательно, могут иметь ещё меньшие накладные расходы, чем вызовы функций динамической библиотеки. Когда и библиотеки, и исполняемые файлы генерируются LLVM, можно выполнять оптимизации всего приложения, которые могут оптимизировать даже через эту границу, но Julia пока не поддерживает это. Однако в будущем это может быть реализовано, что приведёт к ещё большему увеличению производительности.)

Динамические библиотеки и функции ссылаются на кортеж вида (:function, "library") или ("function", "library"), где function — имя экспортированной функции C. library относится к имени динамической библиотеки: доступные в (зависимости от платформы) пути загрузки динамические библиотеки будут разрешаться по имени, и при необходимости может быть указан прямой путь.

Имя функции может использоваться самостоятельно вместо кортежа (просто :function или "function"). В этом случае имя разрешается внутри текущего процесса. Этот формат можно использовать для вызова функций библиотеки C, функций среды выполнения Julia или функций приложения, связанного с Julia.

По умолчанию компиляторы Fortran генерируют замаскированные имена (например, преобразуют имена функций в нижний или верхний регистр, часто добавляя подчёркивание), и поэтому для вызова функции Fortran через ccall необходимо передать замаскированный идентификатор, соответствующий правилу, используемому компилятором Fortran. Кроме того, при вызове функции Fortran все входные данные должны передаваться как указатели на выделенные значения в куче или стеке. Это относится не только к массивам и другим изменяемым объектам, которые обычно выделяются в куче, но и к скалярным значениям, таким как целые числа и числа с плавающей запятой, которые обычно выделяются в стеке и обычно передаются в регистрах при использовании соглашений о вызовах C или Julia.

Наконец, можно использовать ccall для фактического генерации вызова функции библиотеки. Аргументы для ccall следующие:

  1. Пара (:function, "library") , которая должна быть записана как буквальное константа,

    ИЛИ

    указатель на функцию (например, из dlsym).

  2. Тип возвращаемого значения (см. ниже для сопоставления объявленного типа C с типом Julia)

    • Этот аргумент будет оценён во время компиляции, когда будет определён содержащий метод.
  3. Кортеж типов входных данных. Типы входных данных должны быть записаны как буквальный кортеж, а не как кортеж-переменная или выражение.

    • Этот аргумент будет оценён во время компиляции, когда будет определён содержащий метод.
  4. Следующие аргументы, если они есть, представляют фактические значения аргументов, передаваемые функции.

В качестве примера полностью, но простого, следующий вызов функции clock из стандартной библиотеки C:

julia> t = ccall((:clock, "libc"), Int32, ())
2292761

julia> t
2292761

julia> typeof(ans)
Int32

clock не принимает аргументов и возвращает Int32. Одна распространённая проблема — то, что 1-кортеж должен быть написан с заключительной запятой. Например, для вызова функции getenv для получения указателя на значение переменной среды, выполняется вызов такого вида:

julia> path = ccall((:getenv, "libc"), Cstring, (Cstring,), "SHELL")
Cstring(@0x00007fff5fbffc45)

julia> unsafe_string(path)
"/bin/bash"

Обратите внимание, что кортеж типов аргументов должен быть записан как (Cstring,), а не (Cstring). Это связано с тем, что (Cstring) — это просто выражение Cstring в скобках, а не 1-кортеж, содержащий Cstring:

julia> (Cstring)
Cstring

julia> (Cstring,)
(Cstring,)

На практике, особенно при предоставлении многократно используемой функциональности, обычно оборачивают ccall в функциях Julia, которые настраивают аргументы и затем проверяют ошибки в том виде, как их указывает функция C или Fortran, передавая их вызывающей функции Julia в виде исключений. Это особенно важно, так как API C и Fortran известны своей несовместимостью в отношении способов указания условий ошибок. Например, функция C getenv обернута в следующую функцию Julia, которая является упрощённой версией фактического определения из env.jl:

function getenv(var::AbstractString)
    val = ccall((:getenv, "libc"),
                Cstring, (Cstring,), var)
    if val == C_NULL
        error("getenv: undefined variable: ", var)
    end
    unsafe_string(val)
end

Функция C getenv указывает на ошибку, возвращая NULL, но другие стандартные функции C указывают на ошибки различными способами, включая возврат -1, 0, 1 и других специальных значений. Этот обертка генерирует исключение, чётко указывающее на проблему, если вызывающая функция пытается получить несуществующую переменную среды:

julia> getenv("SHELL")
"/bin/bash"

julia> getenv("FOOBAR")
getenv: undefined variable: FOOBAR

Вот немного более сложный пример, который определяет имя хоста локальной машины:

function gethostname()
    hostname = Vector{UInt8}(128)
    ccall((:gethostname, "libc"), Int32,
          (Ptr{UInt8}, Csize_t),
          hostname, sizeof(hostname))
    hostname[end] = 0; # ensure null-termination
    return unsafe_string(pointer(hostname))
end

В этом примере сначала выделяется массив байтов, затем вызывается функция библиотеки C gethostname для заполнения массива именем хоста, берётся указатель на буфер имени хоста и преобразуется указатель в строку Julia, предполагая, что это строка C с завершением нулём. Для библиотек C часто используется такая схема, когда вызывающая сторона должна выделить память, которую затем передаётся вызываемой стороне и заполняется ею. Выделение памяти из Julia таким образом обычно выполняется путём создания неинициализированного массива и передачи указателя на данные в функцию C. Вот почему мы не используем тип Cstring здесь: так как массив неинициализирован, он может содержать байты с нулём. Преобразование в Cstring как часть ccall проверяет наличие байтов с нулём и, следовательно, может генерировать ошибку преобразования.

Создание совместимых с C указателей на функции Julia

Можно передавать функции Julia в нативные функции C, которые принимают аргументы в виде указателей на функции. Например, чтобы соответствовать прототипам C вида:

typedef returntype (*functiontype)(argumenttype, ...)

Макрос @cfunction генерирует совместимый с C указатель на функцию для вызова функции Julia. Аргументы для @cfunction следующие:

  1. Функция Julia
  2. Тип возвращаемого значения
  3. Буквальный кортеж типов входных данных

Как и в случае с ccall, все эти аргументы будут оценены во время компиляции, когда будет определён содержащий метод.

В настоящее время поддерживается только платформа-стандартное соглашение о вызовах C. Это означает, что указатели, сгенерированные @cfunction, не могут использоваться в вызовах, где WINAPI ожидает stdcall-функцию в 32-битной Windows, но могут использоваться в WIN64 (где stdcall унифицировано со соглашением о вызовах C).

Классический пример — функция стандартной библиотеки C qsort, объявленная как:

void qsort(void *base, size_t nmemb, size_t size,
           int (*compare)(const void*, const void*));

Аргумент base — указатель на массив длиной nmemb, с элементами по size байт каждый. compare — функция обратного вызова, которая принимает указатели на два элемента a и b и возвращает целое число меньше/больше нуля, если a должен появиться раньше/позже b (или ноль, если какой-либо порядок разрешён). Теперь предположим, что у нас есть одномерный массив A значений в Julia, который мы хотим отсортировать, используя функцию qsort (а не встроенную функцию Julia sort). Прежде чем беспокоиться о вызове qsort и передаче аргументов, нам необходимо написать функцию сравнения, которая работает для произвольных объектов (которые определяют <):

julia> function mycompare(a, b)::Cint
           return (a < b) ? -1 : ((a > b) ? +1 : 0)
       end
mycompare (generic function with 1 method)

Обратите внимание, что мы должны быть осторожны с типом возвращаемого значения: qsort ожидает функцию, возвращающую C int, поэтому мы аннотируем тип возвращаемого значения функции, чтобы убедиться, что она возвращает Cint.

Для передачи этой функции в C мы получаем её адрес, используя макрос @cfunction:

julia> mycompare_c = @cfunction(mycompare, Cint, (Ref{Cdouble}, Ref{Cdouble}));

@cfunction требует трёх аргументов: функцию Julia (mycompare), тип возвращаемого значения (Cint) и буквальный кортеж типов аргументов входных данных, в данном случае для сортировки массива Cdouble (Float64) элементов.

Окончательный вызов qsort выглядит так:

julia> A = [1.3, -2.7, 4.4, 3.1]
4-element Array{Float64,1}:
  1.3
 -2.7
  4.4
  3.1

julia> ccall(:qsort, Cvoid, (Ptr{Cdouble}, Csize_t, Csize_t, Ptr{Cvoid}),
             A, length(A), sizeof(eltype(A)), mycompare_c)

julia> A
4-element Array{Float64,1}:
 -2.7
  1.3
  3.1
  4.4

Как видно, A изменяется на отсортированный массив [-2.7, 1.3, 3.1, 4.4]. Обратите внимание, что Julia знает, как преобразовать массив в Ptr{Cdouble}, как вычислить размер типа в байтах (идентично оператору sizeof в C) и так далее. Для интереса, попробуйте вставить строку println("mycompare($a, $b)") в mycompare, что позволит вам увидеть сравнения, которые выполняет qsort (и проверить, что она действительно вызывает функцию Julia, которую вы ей передали).

Сопоставление типов C с типами Julia

Крайне важно точно сопоставлять объявленный тип C с его объявлением в Julia. Несоответствия могут привести к тому, что код, корректно работающий на одной системе, даст сбои или произведёт неопределённые результаты на другой системе.

Обратите внимание, что никакие заголовочные файлы C не используются нигде в процессе вызова функций C: вы несёте ответственность за обеспечение того, чтобы ваши типы Julia и сигнатуры вызовов точно отражали типы и сигнатуры в заголовочном файле C. (Пакет Clang можно использовать для автоматической генерации кода Julia из заголовочного файла C.)

Автопреобразование:

Julia автоматически вставляет вызовы функции Base.cconvert для преобразования каждого аргумента в указанный тип. Например, следующий вызов:

ccall((:foo, "libfoo"), Cvoid, (Int32, Float64), x, y)

будет вести себя так, как будто было написано следующее:

ccall((:foo, "libfoo"), Cvoid, (Int32, Float64),
      Base.unsafe_convert(Int32, Base.cconvert(Int32, x)),
      Base.unsafe_convert(Float64, Base.cconvert(Float64, y)))

Base.cconvert обычно просто вызывает convert, но может быть определён для возврата произвольного нового объекта, более подходящего для передачи в C. Это следует использовать для выполнения всех выделений памяти, к которым будет обращаться код C. Например, это используется для преобразования массива объектов (например, строк) в массив указателей.

Base.unsafe_convert обрабатывает преобразование в типы Ptr. Он считается небезопасным, потому что преобразование объекта в родной указатель может скрыть объект от сборщика мусора, что приведёт к его преждевременному освобождению.

Соответствия типов:

Сначала рассмотрим некоторые релевантные термины Julia для типов:

Синтаксис/Ключевое слово Пример Описание
mutable struct String «Листовой тип» :: Группа связанных данных, которая включает тег типа, управляется сборщиком мусора Julia и определяется тождеством объекта. Параметры типа листового типа должны быть полностью определены (не допускаются TypeVars) для того, чтобы экземпляр был построен.
abstract type Any, AbstractArray{T, N}, Complex{T} «Родительский тип» :: Родительский тип (не листовой тип), который нельзя экземплировать, но который можно использовать для описания группы типов.
T{A} Vector{Int} «Параметр типа» :: Специализация типа (обычно используется для диспетчеризации или оптимизации хранения).
«TypeVar» :: T в объявлении параметра типа называется TypeVar (сокращение от type variable).
primitive type Int, Float64 «Примитивный тип» :: Тип без полей, но с размером. Он хранится и определяется по значению.
struct Pair{Int, Int} «Структура» :: Тип со всеми полями, определёнными как константы. Он определяется по значению и может храниться с тегом типа.
ComplexF64 (isbits) «Is-bits» :: primitive type, или тип struct, где все поля являются другими типами isbits. Определяется по значению и хранится без тега типа.
struct ...; end nothing «Синглтон» :: Листовой тип или структура без полей.
(...) или tuple(...) (1, 2, 3) «Кортеж» :: Неизменяемая структура данных, похожая на анонимный тип структуры или на постоянный массив. Представлена либо массивом, либо структурой.

Типы битов

Следует учитывать несколько специальных типов, так как никакой другой тип не может быть определён для работы таким же образом:

  • Float32

    Точно соответствует типу float в C (или REAL*4 в Fortran).

  • Float64

    Точно соответствует типу double в C (или REAL*8 в Fortran).

  • ComplexF32

    Точно соответствует типу complex float в C (или COMPLEX*8 в Fortran).

  • ComplexF64

    Точно соответствует типу complex double в C (или COMPLEX*16 в Fortran).

  • Signed

    Точно соответствует аннотации типа signed в C (или любому типу INTEGER в Fortran). Любой тип Julia, который не является подтипом Signed, предполагается беззнаковым.

  • Ref{T}

    Ведёт себя как Ptr{T} , который может управлять своей памятью через сборщик мусора Julia.

  • Array{T,N}

    Когда массив передаётся в C в качестве аргумента Ptr{T}, он не переинтерпретируется: Julia требует, чтобы тип элементов массива соответствовал T, и передаётся адрес первого элемента.

    Поэтому, если массив Array содержит данные в неправильном формате, необходимо явно преобразовать его с помощью вызова, такого как trunc(Int32, a).

    Чтобы передать массив A в качестве указателя другого типа без предварительного преобразования данных (например, чтобы передать массив Float64 в функцию, которая работает с неинтерпретированными байтами), вы можете объявить аргумент как Ptr{Cvoid}.

    Если массив с типом элементов Ptr{T} передаётся как аргумент Ptr{Ptr{T}}, Base.cconvert попытается сначала создать нуль-терминированную копию массива, где каждый элемент будет заменён его версией Base.cconvert. Это позволяет, например, передать массив указателей argv типа Vector{String} в аргумент типа Ptr{Ptr{Cchar}}.

На всех системах, которые мы в настоящее время поддерживаем, базовые типы значений C/C++ могут быть преобразованы в типы Julia следующим образом. Каждый тип C также имеет соответствующий тип Julia с тем же именем, но с префиксом C. Это может помочь при написании переносимого кода (и запоминании, что int в C не то же самое, что Int в Julia).

Независимо от системы:

Имя C Имя Fortran Стандартное псевдоним Julia Тип Julia Base
unsigned char CHARACTER Cuchar UInt8
bool (только в C++) Cuchar UInt8
short INTEGER*2, LOGICAL*2 Cshort Int16
unsigned short Cushort UInt16
int, BOOL (C, типичный) INTEGER*4, LOGICAL*4 Cint Int32
unsigned int Cuint UInt32
long long INTEGER*8, LOGICAL*8 Clonglong Int64
unsigned long long Culonglong UInt64
intmax_t Cintmax_t Int64
uintmax_t Cuintmax_t UInt64
float REAL*4i Cfloat Float32
double REAL*8 Cdouble Float64
complex float COMPLEX*8 ComplexF32 Complex{Float32}
complex double COMPLEX*16 ComplexF64 Complex{Float64}
ptrdiff_t Cptrdiff_t Int
ssize_t Cssize_t Int
size_t Csize_t UInt
void Cvoid
void и [[noreturn]] или _Noreturn Union{}
void* Ptr{Cvoid}
T* (где T представляет собой соответствующим образом определённый тип) Ref{T}
char* (или char[], например, строка) CHARACTER*N Cstring если завершается нулём, или Ptr{UInt8} если нет
char** (или *char[]) Ptr{Ptr{UInt8}}
jl_value_t* (любой тип Julia) Any
jl_value_t** (ссылка на тип Julia) Ref{Any}
va_arg Не поддерживается
... (спецификация функции с переменным числом аргументов) T... (где T — один из перечисленных выше типов; функции с переменным числом аргументов разных типов не поддерживаются)

Тип Cstring по существу является синонимом для Ptr{UInt8}, за исключением того, что преобразование в Cstring вызовет ошибку, если строка Julia содержит какие-либо вложенные символы NULL (что приведёт к неявной обрезке строки, если процедура C интерпретирует NULL как терминатор). Если вы передаёте char* в процедуру C, которая не предполагает нуль-терминацию (например, потому что вы передаёте явную длину строки), или если вы уверены, что ваша строка Julia не содержит NULL и хотите пропустить проверку, вы можете использовать Ptr{UInt8} как тип аргумента. Cstring также может использоваться в качестве типа возвращаемого значения ccall, но в этом случае он, очевидно, не добавляет дополнительных проверок и предназначен только для повышения удобочитаемости вызова.

Зависимые от системы:

C name Standard Julia Alias Julia Base Type
char Cchar Int8 (x86, x86_64), UInt8 (powerpc, arm)
long Clong Int (UNIX), Int32 (Windows)
unsigned long Culong UInt (UNIX), UInt32 (Windows)
wchar_t Cwchar_t Int32 (UNIX), UInt16 (Windows)
Примечание

При вызове Fortran все входные данные должны передаваться по указателям на значения, выделенные в куче или на стеке, поэтому все соответствия типов выше должны содержать дополнительную обёртку Ptr{..} или Ref{..} вокруг спецификации типа.

Предупреждение

Для строковых аргументов (char*) тип Julia должен быть Cstring (если ожидаются данные, завершённые символом NUL) или Ptr{Cchar} или Ptr{UInt8} в противном случае (эти два типа указателей имеют одинаковый эффект), как описано выше, а не String. Аналогично, для аргументов массива (T[] или T*) тип Julia должен быть снова Ptr{T}, а не Vector{T}.

Предупреждение

Тип Char в Julia составляет 32 бита, что не соответствует типу широких символов (wchar_t или wint_t ) на всех платформах.

Предупреждение

Возвращаемый тип Union{} означает, что функция не вернёт значение, т.е. C++11 [[noreturn]] или C11 _Noreturn (например, jl_throw или longjmp). Не используйте это для функций, которые ничего не возвращают (void), но возвращают значение; используйте Cvoid вместо этого.

Примечание

Для аргументов wchar_t*, тип Julia должен быть Cwstring (если C-функция ожидает строку, завершенную нулём) или Ptr{Cwchar_t} в противном случае. Обратите также внимание, что данные строк UTF-8 в Julia внутренне завершаются нулём, поэтому их можно передавать C-функциям, ожидающим нуль-завершённые данные, без копирования (но использование типа Cwstring приведёт к ошибке, если сама строка содержит нули).

Примечание

C-функции, принимающие аргумент типа char**, могут вызываться с использованием типа Ptr{Ptr{UInt8}} в Julia. Например, C-функции вида:

int main(int argc, char **argv);

могут вызываться с помощью следующего кода Julia:

argv = [ "a.out", "arg1", "arg2" ]
ccall(:main, Int32, (Int32, Ptr{Ptr{UInt8}}), length(argv), argv)
Примечание

Для Fortran-функций, принимающих строки переменной длины типа character(len=*), длины строк предоставляются как скрытые аргументы. Тип и позиция этих аргументов в списке зависят от компилятора, где поставщики компиляторов обычно по умолчанию используют Csize_t в качестве типа и добавляют скрытые аргументы в конец списка аргументов. Хотя это поведение фиксировано для некоторых компиляторов (GNU), другие по желанию позволяют размещать скрытые аргументы непосредственно после символьного аргумента (Intel, PGI). Например, Fortran подпрограммы вида

subroutine test(str1, str2)
character(len=*) :: str1,str2

могут вызываться с помощью следующего кода Julia, где длины добавляются

str1 = "foo"
str2 = "bar"
ccall(:test, Void, (Ptr{UInt8}, Ptr{UInt8}, Csize_t, Csize_t),
                    str1, str2, sizeof(str1), sizeof(str2))
Предупреждение

Компиляторы Fortran могут также добавлять другие скрытые аргументы для указателей, массивов с предположением размера (: ) и массивов с предположением размера (*). Такое поведение можно избежать, используя ISO_C_BINDING и включая bind(c) в определении подпрограммы, что настоятельно рекомендуется для совместимого кода. В этом случае скрытых аргументов не будет, ценой некоторых возможностей языка (например, только character(len=1) будет разрешено передавать строки).

Примечание

C-функция, объявленная как возвращающая Cvoid , вернёт значение nothing в Julia.

Соответствия типов структур

Составные типы, известные как struct в C или TYPE в Fortran90 (или STRUCTURE / RECORD в некоторых вариантах F77), могут быть отражены в Julia путём создания определения struct с той же компоновкой полей.

При рекурсивном использовании типы isbits хранятся встроены. Все остальные типы хранятся как указатель на данные. При отражении структуры, используемой по значению внутри другой структуры в C, крайне важно не пытаться вручную копировать поля, так как это не сохранит правильную выравнивание полей. Вместо этого объявите тип структуры isbits и используйте его вместо этого. Безымянные структуры невозможны при переводе в Julia.

Упакованные структуры и объявления союзов не поддерживаются Julia.

Можно получить приблизительное представление структуры union, если заранее известно поле с наибольшим размером (возможно, включая заполнение). При переводе полей в Julia объявите поле Julia только этого типа.

Массивы параметров могут быть выражены с помощью NTuple:

in C:
struct B {
    int A[3];
};
b_a_2 = B.A[2];

in Julia:
struct B
    A::NTuple{3, CInt}
end
b_a_2 = B.A[3]  # note the difference in indexing (1-based in Julia, 0-based in C)

Массивы неизвестного размера (структуры переменной длины, соответствующие стандарту C99, определяемые [] или [0] ), не поддерживаются напрямую. Часто лучший способ работы с ними - работа с байтовыми смещениями напрямую. Например, если C-библиотека объявила правильный тип строки и вернула указатель на него:

struct String {
    int strlen;
    char data[];
};

В Julia мы можем получить доступ к частям независимо, чтобы сделать копию этой строки:

str = from_c::Ptr{Cvoid}
len = unsafe_load(Ptr{Cint}(str))
unsafe_string(str + Core.sizeof(Cint), len)

Параметры типа

Аргументы типа для ccall и @cfunction вычисляются статически, когда определяется метод, содержащий использование. Поэтому они должны иметь вид литеральной кортежа, а не переменной, и не могут ссылаться на локальные переменные.

Это может показаться странным ограничением, но помните, что поскольку C не является динамическим языком, таким как Julia, его функции могут принимать только типы аргументов со статически известной, фиксированной сигнатурой.

Однако, хотя структура типа должна быть статически известна для вычисления предназначенного C ABI, статические параметры функции рассматриваются как часть этой статической среды. Статические параметры функции могут использоваться в качестве параметров типа в сигнатуре вызова, при условии, что они не влияют на структуру типа. Например, f(x::T) where {T} = ccall(:valid, Ptr{T}, (Ptr{T},), x) допустимо, так как Ptr всегда является примитивным типом размера слова. Но g(x::T) where {T} = ccall(:notvalid, T, (T,), x) недействительно, так как структура типа T не известна статически.

SIMD-значения

Примечание: Эта функция реализована только на 64-битных платформах x86 и AArch64.

Если у C/C++ процедуры есть аргумент или возвращаемое значение, являющееся собственным типом SIMD, соответствующим типом Julia является гомогенная кортежа из VecElement, которая естественным образом отображается на тип SIMD. Конкретно:

  • Кортеж должен иметь такой же размер, как и тип SIMD. Например, кортеж, представляющий __m128 на x86, должен иметь размер 16 байтов.
  • Тип элемента кортежа должен быть экземпляром VecElement{T}, где T является примитивным типом, который составляет 1, 2, 4 или 8 байтов.

Например, рассмотрим эту C-процедуру, использующую инструкции AVX:

#include <immintrin.h>

__m256 dist( __m256 a, __m256 b ) {
    return _mm256_sqrt_ps(_mm256_add_ps(_mm256_mul_ps(a, a),
                                        _mm256_mul_ps(b, b)));
}

Следующий код Julia вызывает dist с помощью ccall:

const m256 = NTuple{8, VecElement{Float32}}

a = m256(ntuple(i -> VecElement(sin(Float32(i))), 8))
b = m256(ntuple(i -> VecElement(cos(Float32(i))), 8))

function call_dist(a::m256, b::m256)
    ccall((:dist, "libdist"), m256, (m256, m256), a, b)
end

println(call_dist(a,b))

Компьютер-хост должен иметь необходимые SIMD-регистры. Например, код выше не будет работать на компьютерах-хостах без поддержки AVX.

Владение памятью

malloc/free

Выделение и освобождение памяти таких объектов должны выполняться вызовами соответствующих процедур очистки в используемых библиотеках, как и в любой программе на C. Не пытайтесь освободить объект, полученный из C-библиотеки с помощью Libc.free в Julia, так как это может привести к тому, что функция free будет вызвана через неправильную библиотеку libc и привести к сбою Julia. Обратное (передача объекта, выделенного в Julia, для освобождения внешней библиотекой) также недопустимо.

Когда использовать T, Ptr{T} и Ref{T}

В коде Julia, оборачивающем вызовы внешних C-процедур, обычные (не указатель) данные должны объявляться типа T внутри ccall, так как они передаются по значению. Для C-кода, принимающего указатели, Ref{T} обычно используется для типов входных аргументов, позволяя использовать указатели на память, управляемую либо Julia, либо C, через неявный вызов Base.cconvert. Напротив, указатели, возвращаемые C-функцией, должны объявляться выходного типа Ptr{T}, отражая то, что управляемая памятью является только C. Указатели, содержащиеся в C-структурах, должны представляться как поля типа Ptr{T} внутри соответствующих типов структур Julia, предназначенных для имитации внутренней структуры соответствующих C-структур.

В коде Julia, оборачивающем вызовы внешних Fortran-процедур, все входные аргументы должны объявляться типа Ref{T}, так как Fortran передает все переменные по указателям на места в памяти. Тип возвращаемого значения должен быть либо Cvoid для Fortran подпрограмм, либо T для Fortran функций, возвращающих тип T.

Сопоставление C-функций с Julia

ccall / @cfunction руководство по переводу аргументов

Для перевода списка аргументов C в Julia:

  • T, где T — один из примитивных типов: char, int, long, short, float, double, complex, enum или любой из их typedef эквивалентов

    • T, где T — эквивалентный тип данных Julia Bits (согласно таблице выше)
    • если T является enum, тип аргумента должен быть эквивалентен Cint или Cuint
    • значение аргумента будет скопировано (передача по значению)
  • struct T (включая typedef структуры)

    • T, где T — листовой тип Julia
    • значение аргумента будет скопировано (передача по значению)
  • void*

    • зависит от того, как используется этот параметр. Сначала переведите его в предполагаемый тип указателя, затем определите эквивалент Julia, используя оставшиеся правила в этом списке
    • этот аргумент может быть объявлен как Ptr{Cvoid}, если он действительно просто неизвестный указатель
  • jl_value_t*

    • Any
    • значение аргумента должно быть допустимым объектом Julia
  • jl_value_t**

    • Ref{Any}
    • значение аргумента должно быть допустимым объектом Julia (или C_NULL)
  • T*

    • Ref{T}, где T — тип Julia, соответствующий T
    • значение аргумента будет скопировано, если это тип isbits, в противном случае значение должно быть допустимым объектом Julia
  • T (*)(...) (например, указатель на функцию)

    • Ptr{Cvoid} (возможно, вам потребуется явно использовать @cfunction для создания этого указателя)
  • ... (например, vararg)

    • T..., где T — тип Julia
    • в настоящее время не поддерживается @cfunction
  • va_arg

    • не поддерживается ccall или @cfunction

ccall / @cfunction руководство по переводу типов возвращаемых значений

Для перевода типа возвращаемого значения C в Julia:

  • void

    • Cvoid (это вернёт экземпляр nothing::Cvoid)
  • T, где T — один из примитивных типов: char, int, long, short, float, double, complex, enum или любой из их typedef эквивалентов

    • T, где T — эквивалентный тип Julia Bits (согласно таблице выше)
    • если T является enum, тип аргумента должен быть эквивалентен Cint или Cuint
    • значение аргумента будет скопировано (возвращается по значению)
  • struct T (включая typedef структуры)

    • T, где T — листовой тип Julia
    • значение аргумента будет скопировано (возвращается по значению)
  • void*

    • зависит от того, как используется этот параметр. Сначала переведите его в предполагаемый тип указателя, затем определите эквивалент Julia, используя оставшиеся правила в этом списке
    • этот аргумент может быть объявлен как Ptr{Cvoid}, если он действительно просто неизвестный указатель
  • jl_value_t*

    • Any
    • значение аргумента должно быть допустимым объектом Julia
  • jl_value_t**

    • Ptr{Any} (Ref{Any} — недопустимый тип возвращаемого значения)
    • значение аргумента должно быть допустимым объектом Julia (или C_NULL)
  • T*

    • Если память уже принадлежит Julia или это тип isbits, и известно, что он не нулевой:

      • Ref{T}, где T — тип Julia, соответствующий T
      • тип возвращаемого значения Ref{Any} недопустим, он должен быть Any (соответствующий jl_value_t* ) или Ptr{Any} (соответствующий jl_value_t** )
      • C НЕ ДОЛЖЕН изменять память, возвращаемую через Ref{T}, если T — тип isbits
    • Если память принадлежит C:

      • Ptr{T}, где T — тип Julia, соответствующий T
  • T (*)(...) (например, указатель на функцию)

    • Ptr{Cvoid} (возможно, вам потребуется явно использовать @cfunction для создания этого указателя)

Передача указателей для изменения входных данных

Поскольку C не поддерживает несколько значений возврата, часто функции C принимают указатели на данные, которые функция будет изменять. Чтобы достичь этого в ccall, необходимо сначала заключить значение в Ref{T} соответствующего типа. Когда вы передаёте этот объект Ref в качестве аргумента, Julia автоматически передаст указатель C на заключённые данные:

width = Ref{Cint}(0)
range = Ref{Cfloat}(0)
ccall(:foo, Cvoid, (Ref{Cint}, Ref{Cfloat}), width, range)

После возврата содержимое width и range можно получить (если они были изменены foo ) с помощью width[] и range[]; они ведут себя как массивы нулевой размерности.

Специальный синтаксис ссылок для ccall (устаревшее):

Синтаксис & устарел, используйте тип аргумента Ref{T} вместо него.

Префикс & используется для аргумента ccall, чтобы указать, что должен быть передан указатель на скалярный аргумент, а не само скалярное значение (требуется для всех аргументов функций Fortran, как указано выше). Следующий пример вычисляет скалярное произведение с использованием функции BLAS.

function compute_dot(DX::Vector{Float64}, DY::Vector{Float64})
    @assert length(DX) == length(DY)
    n = length(DX)
    incx = incy = 1
    product = ccall((:ddot_, "libLAPACK"),
                    Float64,
                    (Ref{Int32}, Ptr{Float64}, Ref{Int32}, Ptr{Float64}, Ref{Int32}),
                    n, DX, incx, DY, incy)
    return product
end

Значение префикса & не совсем такое же, как в C. В частности, любые изменения в ссылаемых переменных не будут видны в Julia, если тип не является изменяемым (объявлен через mutable struct). Однако даже для неизменяемых структур это не приведёт к ошибкам, если вызываемые функции попытаются такие изменения (то есть запись через переданные указатели). Кроме того, & может использоваться с любым выражением, таким как &0 или &f(x).

Когда скалярное значение передаётся с & как аргумент типа Ptr{T}, значение сначала преобразуется в тип T.

Примеры обёртки C

Вот простой пример обёртки C, возвращающей тип Ptr:

mutable struct gsl_permutation
end

# The corresponding C signature is
#     gsl_permutation * gsl_permutation_alloc (size_t n);
function permutation_alloc(n::Integer)
    output_ptr = ccall(
        (:gsl_permutation_alloc, :libgsl), # name of C function and library
        Ptr{gsl_permutation},              # output type
        (Csize_t,),                        # tuple of input types
        n                                  # name of Julia variable to pass in
    )
    if output_ptr == C_NULL # Could not allocate memory
        throw(OutOfMemoryError())
    end
    return output_ptr
end

Библиотека GNU Scientific Library (GNU Scientific Library) (здесь предполагается, что она доступна через :libgsl ) определяет неявный указатель gsl_permutation * в качестве типа возврата функции C gsl_permutation_alloc. Так как коду пользователя никогда не нужно заглядывать внутрь структуры gsl_permutation , соответствующая обёртка Julia просто требует объявления нового типа gsl_permutation, у которого нет внутренних полей и единственная цель которого — размещаться в параметре типа Ptr . Тип возврата ccall объявлен как Ptr{gsl_permutation}, так как память, выделенная и на которую указывает output_ptr , контролируется C (а не Julia).

Вход n передаётся по значению, поэтому входное описание функции просто объявляется как (Csize_t,) без необходимости в Ref или Ptr . (Если вместо этого обёртка вызывала функцию Fortran, соответствующее входное описание функции должно было бы быть (Ref{Csize_t},), так как переменные Fortran передаются по указателям.) Кроме того, n может быть любым типом, который преобразуется в целочисленный тип Csize_t ; ccall неявно вызывает Base.cconvert(Csize_t, n).

Вот второй пример, оборачивающий соответствующий деструктор:

# The corresponding C signature is
#     void gsl_permutation_free (gsl_permutation * p);
function permutation_free(p::Ref{gsl_permutation})
    ccall(
        (:gsl_permutation_free, :libgsl), # name of C function and library
        Cvoid,                             # output type
        (Ref{gsl_permutation},),          # tuple of input types
        p                                 # name of Julia variable to pass in
    )
end

Здесь вход p объявлен как тип Ref{gsl_permutation}, что означает, что память, на которую указывает p , может управляться Julia или C. Указатель на память, выделенную C, должен иметь тип Ptr{gsl_permutation}, но он преобразуется с помощью Base.cconvert и поэтому может использоваться в той же (ковариантной) среде входного аргумента ccall. Указатель на память, выделенную Julia, должен быть типа Ref{gsl_permutation}, чтобы гарантировать, что адрес памяти действителен и что сборщик мусора Julia правильно управляет выделенным блоком памяти. Поэтому объявление Ref{gsl_permutation} позволяет использовать указатели, управляемые C или Julia.

Если обёртка C никогда не ожидает, что пользователь передаст указатели на память, управляемую Julia, то использование p::Ptr{gsl_permutation} для описания метода обёртки и аналогично в ccall также допустимо.

Вот третий пример, передающий массивы Julia:

# The corresponding C signature is
#    int gsl_sf_bessel_Jn_array (int nmin, int nmax, double x,
#                                double result_array[])
function sf_bessel_Jn_array(nmin::Integer, nmax::Integer, x::Real)
    if nmax < nmin
        throw(DomainError())
    end
    result_array = Vector{Cdouble}(nmax - nmin + 1)
    errorcode = ccall(
        (:gsl_sf_bessel_Jn_array, :libgsl), # name of C function and library
        Cint,                               # output type
        (Cint, Cint, Cdouble, Ref{Cdouble}),# tuple of input types
        nmin, nmax, x, result_array         # names of Julia variables to pass in
    )
    if errorcode != 0
        error("GSL error code $errorcode")
    end
    return result_array
end

Функция C, которая обертывается, возвращает целочисленный код ошибки; результаты фактического вычисления функции Бесселя J заполняют массив Julia result_array. Эта переменная может использоваться только с соответствующим типом входных данных Ref{Cdouble}, поскольку её память выделяется и управляется Julia, а не C. Неявный вызов Base.cconvert(Ref{Cdouble}, result_array) распаковывает указатель Julia на структуру данных массива Julia в форму, понятную для C.

Обратите внимание, что для правильной работы этого кода, result_array должен быть объявлен как Ref{Cdouble} тип, а не Ptr{Cdouble}. Память управляется Julia, и подпись Ref уведомляет сборщик мусора Julia о необходимости продолжения управления памятью для result_array во время выполнения ccall. Если Ptr{Cdouble} использовался вместо этого, ccall может всё ещё работать, но сборщик мусора Julia не будет знать, что память, выделенная для result_array используется внешней функцией C. В результате код может вызвать утечку памяти, если result_array никогда не будет освобождён сборщиком мусора, или если сборщик мусора преждевременно освободит result_array, функция C может выбросить исключение неверного доступа к памяти.

Безопасность сборки мусора

При передаче данных в ccall, лучше избегать использования функции pointer. Вместо этого определите метод преобразования и передайте переменные напрямую в ccall. ccall автоматически позаботится о том, чтобы все его аргументы были защищены от сборки мусора до возвращения результата вызова. Если API C будет хранить ссылку на память, выделенную Julia, после возвращения ccall, вы должны позаботиться о том, чтобы объект оставался видимым для сборщика мусора. Рекомендованный способ сделать это — создать глобальную переменную типа Array{Ref,1} для хранения этих значений до тех пор, пока библиотека C не сообщит вам, что она с ними закончила.

Всякий раз, когда вы создаёте указатель на данные Julia, вы должны убедиться, что исходные данные существуют до тех пор, пока вы не закончите работу с указателем. Многие методы в Julia, такие как unsafe_load и String, создают копии данных, а не принимают владение буфером, поэтому можно безопасно освободить (или изменить) исходные данные, не влияя на Julia. Заметным исключением является unsafe_wrap, который по соображениям производительности разделяет (или может быть настроен на принятие владения) базовым буфером.

Сборщик мусора не гарантирует порядок завершения. То есть, если a содержал ссылку на b и оба a и b должны быть удалены сборщиком мусора, нет гарантии, что b будет завершён после a. Если надлежащее завершение a зависит от того, что b остаётся валидным, это необходимо обрабатывать другими способами.

Спецификации функций, не являющихся константами

Спецификация функции (name, library) должна быть константным выражением. Однако, можно использовать вычисленные значения в качестве имён функций, используя eval следующим образом:

@eval ccall(($(string("a", "b")), "lib"), ...

Это выражение создаёт имя с помощью string, затем подставляет это имя в новое выражение ccall, которое затем оценивается. Имейте в виду, что eval работает только на верхнем уровне, поэтому внутри этого выражения локальные переменные недоступны (если их значения не были подставлены с помощью $). По этой причине eval обычно используется только для формирования определений верхнего уровня, например, при упаковке библиотек, содержащих много похожих функций. Аналогичный пример может быть построен для @cfunction.

Однако, это будет очень медленным и вызовет утечку памяти, поэтому следует избегать этого и продолжать чтение. Следующий раздел обсуждает, как использовать косвенные вызовы для эффективного достижения аналогичного результата.

Косвенные вызовы

Первый аргумент ccall также может быть выражением, вычисляемым во время выполнения. В этом случае выражение должно вычисляться в Ptr, который будет использоваться как адрес вызываемой функции. Это происходит, когда первый аргумент ccall содержит ссылки на неконстанты, такие как локальные переменные, аргументы функций или неконстантные глобальные переменные.

Например, вы можете найти функцию с помощью dlsym, затем кэшировать её в общей ссылке для этой сессии. Например:

macro dlsym(func, lib)
    z = Ref{Ptr{Cvoid}}(C_NULL)
    quote
        let zlocal = $z[]
            if zlocal == C_NULL
                zlocal = dlsym($(esc(lib))::Ptr{Cvoid}, $(esc(func)))::Ptr{Cvoid}
                $z[] = $zlocal
            end
            zlocal
        end
    end
end

mylibvar = Libdl.dlopen("mylib")
ccall(@dlsym("myfunc", mylibvar), Cvoid, ())

Закрытые функции cfunctions

Первый аргумент @cfunction может быть помечен $, в этом случае возвращаемое значение будет struct CFunction, который замыкает аргумент. Вы должны убедиться, что этот возвращаемый объект остаётся активным до завершения всех его использований. Содержание и код в указателе cfunction будут удалены с помощью finalizer, когда эта ссылка будет удалена, и atexit. Это обычно не требуется, так как эта функциональность отсутствует в C, но может быть полезной для работы с плохо спроектированными API, которые не предоставляют параметр отдельной среды закрытия.

function qsort(a::Vector{T}, cmp) where T
    isbits(T) || throw(ArgumentError("this method can only qsort isbits arrays"))
    callback = @cfunction $cmp Cint (Ref{T}, Ref{T})
    # Here, `callback` isa Base.CFunction, which will be converted to Ptr{Cvoid}
    # (and protected against finalization) by the ccall
    ccall(:qsort, Cvoid, (Ptr{T}, Csize_t, Csize_t, Ptr{Cvoid}),
        a, length(a), Base.elsize(a), callback)
    # We could instead use:
    #    GC.@preserve callback begin
    #        use(Base.unsafe_convert(Ptr{Cvoid}, callback))
    #    end
    # if we needed to use it outside of a `ccall`
    return a
end

Закрытие библиотеки

Иногда полезно закрыть (разгрузить) библиотеку, чтобы её можно было перезагрузить. Например, при разработке кода C для использования с Julia, может потребоваться скомпилировать, вызвать код C из Julia, затем закрыть библиотеку, внести изменения, перекомпилировать и загрузить новые изменения. Можно либо перезапустить Julia, либо использовать функции Libdl для явного управления библиотекой, например:

lib = Libdl.dlopen("./my_lib.so") # Open the library explicitly.
sym = Libdl.dlsym(lib, :my_fcn)   # Get a symbol for the function to call.
ccall(sym, ...) # Use the pointer `sym` instead of the (symbol, library) tuple (remaining arguments are the same).
Libdl.dlclose(lib) # Close the library explicitly.

Обратите внимание, что при использовании ccall с кортежом в качестве входных данных (например, ccall((:my_fcn, "./my_lib.so"), ...)), библиотека открывается неявно и может не закрываться явно.

Вызов соглашения

Второй аргумент ccall может необязательно содержать спецификатор соглашения вызова (непосредственно перед типом возвращаемого значения). Без указания спецификатора используется платформно-установленное соглашение вызова C. Другие поддерживаемые соглашения: stdcall, cdecl, fastcall, и thiscall (бездействия в 64-битной Windows). Например (из base/libc.jl) мы видим ту же gethostnameccall, что и выше, но с правильной сигнатурой для Windows:

hn = Vector{UInt8}(256)
err = ccall(:gethostname, stdcall, Int32, (Ptr{UInt8}, UInt32), hn, length(hn))

Дополнительную информацию можно найти в Справочнике по языку LLVM.

Существует ещё одно специальное соглашение вызова llvmcall, которое позволяет вставлять вызовы LLVM-интринсиков непосредственно. Это может быть особенно полезно при нацеливании на необычные платформы, такие как GPGPU. Например, для CUDA нам нужно получить индекс потока:

ccall("llvm.nvvm.read.ptx.sreg.tid.x", llvmcall, Int32, ())

Как и в любом ccall, крайне важно получить точную сигнатуру аргументов. Кроме того, обратите внимание, что нет уровня совместимости, который гарантирует, что интринсик имеет смысл и работает на текущей платформе, в отличие от эквивалентных функций Julia, предоставленных Core.Intrinsics.

Доступ к глобальным переменным

Доступ к глобальным переменным, экспортируемым нативными библиотеками, можно получить по имени с помощью функции cglobal. Аргументы cglobal — это спецификация символа, идентичная той, которая используется в ccall, и тип, описывающий значение, хранящееся в переменной:

julia> cglobal((:errno, :libc), Int32)
Ptr{Int32} @0x00007f418d0816b8

Результат — указатель, содержащий адрес значения. Значение можно изменить с помощью этого указателя, используя unsafe_load и unsafe_store!.

Доступ к данным через указатель

Следующие методы описаны как «небезопасные», потому что неверный указатель или объявление типа могут привести к внезапному завершению работы Julia.

Учитывая Ptr{T}, содержимое типа T можно скопировать из памяти в объект Julia с помощью unsafe_load(ptr, [index]). Аргумент индекса необязателен (по умолчанию равен 1) и соответствует julia-конвенции индексирования с началом с 1. Эта функция намеренно похожа на поведение getindex и setindex! (например, синтаксис доступа []).

Возвращаемое значение — новый объект, инициализированный копией содержимого указанной памяти. Указанную память можно безопасно освободить или выпустить.

Если T — это Any, то память предполагается, что содержит ссылку на объект Julia (jl_value_t*), результат будет ссылкой на этот объект, и объект не будет скопирован. В этом случае необходимо следить за тем, чтобы объект всегда был видимым для сборщика мусора (указатели не считаются, но новая ссылка учитывается), чтобы предотвратить преждевременное освобождение памяти. Обратите внимание, что если объект изначально не был выделен Julia, новый объект никогда не будет завершён сборщиком мусора Julia. Если сам Ptr фактически является jl_value_t*, его можно преобразовать обратно в ссылку на объект Julia с помощью unsafe_pointer_to_objref(ptr). (Значения Julia v могут быть преобразованы в указатели jl_value_t*, как Ptr{Cvoid}, вызвав pointer_from_objref(v).)

Обратная операция (запись данных в Ptr{T}), может быть выполнена с помощью unsafe_store!(ptr, value, [index]). В настоящее время это поддерживается только для примитивных типов или других не содержащих указателей (isbits immutable) типов структур.

Любая операция, которая вызывает ошибку, вероятно, в настоящее время не реализована и должна быть сообщена как ошибка, чтобы её можно было исправить.

Если указатель, который нас интересует, является обычным массивом данных (примитивный тип или неизменяемая структура), функция unsafe_wrap(Array, ptr,dims, own = false) может быть более полезной. Последний параметр должен быть true, если Julia должна «принять владение» базовым буфером и вызвать free(ptr) при завершении возвращённого объекта Array. Если параметр own опущен или равен false, вызывающая сторона должна гарантировать, что буфер существует до тех пор, пока все операции доступа не будут завершены.

Арифметика типа Ptr в Julia (например, с использованием +) ведет себя не так, как арифметика указателей в C. Добавление целого числа к Ptr в Julia всегда перемещает указатель на определённое количество байтов, а не элементов. Таким образом, значения адресов, полученные с помощью арифметики указателей, не зависят от типов элементов указателей.

Безопасность потоков

Некоторые библиотеки C выполняют свои обратные вызовы из другого потока, и поскольку Julia не является потокобезопасной, вам необходимо принять дополнительные меры предосторожности. В частности, вам необходимо организовать двухслойную систему: обратный вызов C должен только планировать (через цикл событий Julia) выполнение вашего «действительного» обратного вызова. Для этого создайте объект AsyncCondition и wait на нём:

cond = Base.AsyncCondition()
wait(cond)

Обратный вызов, который вы передаёте в C, должен только выполнить ccall в :uv_async_send, передавая cond.handle в качестве аргумента, позаботившись об избегании выделения памяти или других взаимодействий с исполняемой средой Julia.

Обратите внимание, что события могут быть объединены, поэтому несколько вызовов uv_async_send могут привести к одному уведомлению о пробуждении условия.

Подробнее об обратных вызовах

Для получения более подробной информации о том, как передавать обратные вызовы в библиотеки C, см. эту статью блога.

C++

Для прямого взаимодействия с C++, см. пакет Cxx. Для инструментов создания связей C++, см. пакет CxxWrap.

© 2009–2019 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v0.7.0/manual/calling-c-and-fortran-code/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API