Spec-Zone.ru › Julia 1.5

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

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

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

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

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

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

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

  1. Пара (:function, "library") (наиболее часто),

    ИЛИ

    имя символа :function или строка имени "function" (для символов в текущем процессе или libc),

    ИЛИ

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

  2. Тип возвращаемого значения функции

  3. Кортеж типов входных данных, соответствующий сигнатуре функции

  4. Фактические значения аргументов, которые нужно передать функции, если таковые имеются; каждое является отдельным параметром.

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

Остальные параметры вычисляются во время компиляции, когда определен содержащий метод.

См. ниже, как отобразить типы C в типы Julia.

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

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

julia> t
2292761

julia> typeof(ans)
Int32

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

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

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

Обратите внимание, что кортеж типов аргументов должен быть записан как (Cstring,), а не (Cstring). Это потому, что (Cstring) — это просто выражение Cstring в скобках, а не кортеж из одного элемента, содержащий 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, Cstring, (Cstring,), var)
    if val == C_NULL
        error("getenv: undefined variable: ", var)
    end
    return unsafe_string(val)
end

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

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

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

Вот несколько более сложный пример, который определяет имя локального компьютера. В этом примере предполагается, что код библиотеки сетевых функций находится в общей библиотеке под названием «libc». На практике эта функция обычно является частью стандартной библиотеки C, поэтому часть «libc» следует опустить, но мы хотим продемонстрировать здесь использование этого синтаксиса.

function gethostname()
    hostname = Vector{UInt8}(undef, 256) # MAXHOSTNAMELEN
    err = ccall((:gethostname, "libc"), Int32,
                (Ptr{UInt8}, Csize_t),
                hostname, sizeof(hostname))
    Base.systemerror("gethostname", err != 0)
    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

Как показывает пример, исходный массив Julia A теперь отсортирован: [-2.7, 1.3, 3.1, 4.4]. Обратите внимание, что Julia заботится о преобразовании массива в Ptr{Cdouble}), вычислении размера типа элемента в байтах и так далее.

Для интереса, попробуйте вставить строку println("mycompare($a, $b)") в mycompare, которая позволит увидеть сравнения, которые выполняет qsort (и убедиться, что она действительно вызывает функцию Julia, которую вы передали).

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

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

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

Автоматическое преобразование типов

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 BitSet "Листовой тип" :: Группа связанных данных, которая включает тег типа, управляется сборщиком мусора 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) "Битовые типы" :: 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} аргумент, он не преобразуется с помощью reinterpret-cast: 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 name Fortran name Standard Julia Alias Julia Base Type
unsigned char CHARACTER Cuchar UInt8
bool (_Bool in C99+) Cuchar UInt8
short INTEGER*2, LOGICAL*2 Cshort Int16
unsigned short Cushort UInt16
int, BOOL (C, typical) 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 если завершается символом NULL, или 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-процедуру, которая не предполагает завершения символом NULL (например, потому что вы передаёте явную длину строки), или если вы уверены, что ваша строка 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 (если ожидаются данные, завершённые символом NULL), или либо 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-процедура ожидает строку, завершённую символом NULL), или Ptr{Cwchar_t} в противном случае. Обратите также внимание, что данные строк UTF-8 в Julia внутренне завершаются символом NULL, поэтому их можно передавать C-функциям, ожидающим данных, завершённых символом NULL, без создания копии (но использование типа Cwstring приведёт к ошибке, если сама строка содержит символы NULL).

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)
END_OF_DOCUMENT_MARKER

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

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

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

str1 = "foo"
str2 = "bar"
ccall(:test, Cvoid, (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. Например, структура в нотации C, написанная как

struct B {
    int A[3];
};

b_a_2 = B.A[2];

может быть записана в 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 будет вызвана через неправильную библиотеку и приведёт к аварийному завершению процесса. Обратное (передача объекта, выделенного в 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 (по таблице выше)
    • если 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 Leaf
    • значение аргумента будет скопировано (возвращается по значению)
  • 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[]; они ведут себя как одномерные массивы.

Примеры обёртки 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 (предполагается, что она доступна через :libgsl) определяет неявный указатель gsl_permutation * в качестве типа возвращаемого значения C-функции gsl_permutation_alloc. Поскольку пользовательский код никогда не должен заглядывать внутрь структуры gsl_permutation, соответствующая обёртка Julia просто нуждается в новом объявлении типа gsl_permutation, не имеющем внутренних полей и предназначенном для размещения в параметре типа Ptr типа. Тип возвращаемого значения ccall объявляется как Ptr{gsl_permutation}, так как память, выделенная и указанная output_ptr управляется C.

Входной параметр 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 и поэтому

Теперь, если вы посмотрите достаточно внимательно на этот пример, вы можете заметить, что он неверный, учитывая наше объяснение предпочтительных типов объявления выше. Вы видите это? Функция, которую мы вызываем, освободит память. Этот тип операций не может быть применён к объекту Julia (он вызовет ошибку или повредит память). Поэтому предпочтительнее объявить тип p как Ptr{gsl_permutation }, чтобы затруднить для пользователя ошибочную передачу другого объекта, чем тот, который получен с помощью gsl_permutation_alloc.

Если обёртка 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}(undef, 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. Неявный вызов Base.cconvert(Ref{Cdouble}, result_array) распаковывает указатель Julia на структуру данных массива Julia в форму, понятную C.

Пример обёртки Fortran

Следующий пример использует ccall для вызова функции из общей библиотеки Fortran (libBLAS) для вычисления скалярного произведения. Обратите внимание, что здесь сопоставление аргументов несколько отличается от приведённого выше, так как нам необходимо сопоставить Julia с Fortran. Для каждого типа аргумента мы указываем Ref или Ptr. Эта соглашение именования может быть специфичным для вашего компилятора Fortran и операционной системы, и, вероятно, не документировано. Тем не менее, упаковка каждого в Ref (или Ptr, где эквивалентно) — частое требование реализации компиляторов Fortran:

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

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

При передаче данных в ccall лучше избегать использования функции pointer. Вместо этого определите метод преобразования и передайте переменные напрямую в ccall. ccall автоматически позаботится о том, чтобы все его аргументы были сохранены от сбора мусора до возвращения результата. Если C API сохранит ссылку на память, выделенную 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, ())

Закрытия cфункций

Первый аргумент функции @cfunction может быть помечен $, в этом случае возвращаемое значение будет struct CFunction, который замыкает аргумент. Необходимо убедиться, что этот возвращаемый объект сохраняется до тех пор, пока все его использования не завершены. Содержимое и код по указателю на cфункцию будут удалены с помощью 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

Закрытия @cfunction полагаются на LLVM-трамплины, которые недоступны на всех платформах (например, ARM и PowerPC).

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

Иногда бывает полезно закрыть (разгрузить) библиотеку, чтобы её можно было перезагрузить. Например, при разработке кода 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}(undef, 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!.

Этот errno символ может отсутствовать в библиотеке под названием "libc", поскольку это деталь реализации вашего компилятора. Обычно символы стандартной библиотеки следует получать по имени, что позволяет компилятору определить правильный символ. Однако символ errno в этом примере является специальным для большинства компиляторов, поэтому полученное значение, вероятно, не соответствует ожидаемому или нужному. Компиляция эквивалентного кода на C в любой многопоточной системе обычно фактически вызывает другую функцию (через макро-препроцессор), и может дать другой результат, чем устаревшее значение, напечатанное здесь.

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

Следующие методы описываются как «небезопасные», потому что неверный указатель или объявление типа могут привести к неожиданному завершению работы 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) неизменяемых типов структур.

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

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

Арифметические операции над типом 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.

  • 1Вызовы функций вне библиотек как в C, так и в Julia, могут быть встроены, и поэтому могут иметь ещё меньшую стоимость, чем вызовы функций из общих библиотек. Суть в том, что стоимость фактического выполнения вызова внешней функции примерно такая же, как и вызова в любом из родных языков.
  • 2Пакет Clang может использоваться для автоматического генерирования кода Julia из заголовка C.

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

Spec-Zone.ru

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