Вызов кода 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:
-
Пара
(:function, "library"), (наиболее распространённый вариант),ИЛИ
символ имени
:functionили строка имени"function"(для символов в текущем процессе или libc),ИЛИ
указатель на функцию (например, из
dlsym). Тип возвращаемого значения функции
Кортеж типов входных данных, соответствующий сигнатуре функции
Фактические значения аргументов, которые необходимо передать функции, если таковые имеются; каждый является отдельным параметром.
Пара (:function, "library"), тип возвращаемого значения и типы входных данных должны быть литеральными константами (т.е. они не могут быть переменными, но см. Спецификации функций, не являющихся константами ниже).
Остальные параметры вычисляются во время компиляции, когда определяется содержащий метод.
См. ниже, как отобразить типы C в типы Julia.
В качестве примера, вызывающего clock функцию из стандартной библиотеки C на большинстве систем Unix-подобных систем:
julia> t = ccall(:clock, Int32, ()) 2292761 julia> t 2292761 julia> typeof(ans) Int32
clock не принимает аргументов и возвращает Int32. Распространённая ошибка заключается в том, что кортеж типов аргументов из одного элемента должен быть написан с запятой в конце. Например, чтобы вызвать функцию 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 отличаются в отношении способов указания условий ошибки. Например, функция getenv библиотеки C обернута в следующую функцию 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 следующие:
- Функция Julia
- Тип возвращаемого значения функции
- Кортеж типов входных данных, соответствующий сигнатуре функции
Как и в случае с 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}, вычисление размера типа элемента в байтах и так далее.
Для интереса, попробуйте вставить 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. Например, это используется для преобразования Array объектов (например, строк) в массив указателей.
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 имя | Имя Fortran | Стандартный псевдоним Julia | Базовый тип Julia |
|---|---|---|---|
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** (or *char[]) |
Ptr{Ptr{UInt8}} |
||
jl_value_t* (любой тип Julia) |
Any |
||
jl_value_t** (ссылка на тип Julia) |
Ref{Any} |
||
va_arg |
Не поддерживается | ||
... (спецификация функции с переменным числом аргументов) |
T... (где T — один из вышеперечисленных типов; функции с переменным числом аргументов разных типов не поддерживаются) |
Тип Cstring по сути является синонимом Ptr{UInt8}, за исключением того, что преобразование в Cstring выбросит ошибку, если строка Julia содержит какие-либо вложенные символы NUL (что приведет к неявной обрезке строки, если C-функция рассматривает NUL как разделитель). Если вы передаёте char* в C-функцию, которая не предполагает завершение NUL (например, потому что вы передаёте явную длину строки), или если вы уверены, что ваша строка Julia не содержит NUL и хотите пропустить проверку, вы можете использовать Ptr{UInt8} в качестве типа аргумента. Cstring также может использоваться в качестве типа возвращаемого значения ccall, но в этом случае он, очевидно, не вводит никаких дополнительных проверок и предназначен только для повышения читабельности вызова.
Системнозависимые типы
| C имя | Стандартный псевдоним Julia | Тип Julia Base |
|---|---|---|
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}.
Тип Julia Char составляет 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, 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
Примечание: Эта функция в настоящее время реализована только на платформах x86-64 и 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 Bits (согласно таблице выше) - если
Tявляетсяenumтипом, тип аргумента должен быть эквивалентенCintилиCuint - значение аргумента будет скопировано (передача по значению)
-
-
struct T(включая typedef для структуры)-
T, гдеT— тип Julia leaf - значение аргумента будет скопировано (передача по значению)
-
-
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и известно, что она не равна null:-
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 (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
Закрытие библиотеки
Иногда бывает полезно закрыть (разгрузить) библиотеку, чтобы её можно было перезагрузить. Например, при разработке кода 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 в любой многопоточной системе обычно вызовет другую функцию (через перегрузку макросов препроцессора) и может дать другой результат, чем значение legacy, напечатанное здесь.
Доступ к данным через указатель
Следующие методы описаны как «небезопасные», потому что неверный указатель или объявление типа могут привести к неожиданному завершению работы 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) может быть более полезной. Конечный параметр должен быть 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.
Вызовы функций, не относящихся к библиотеке, как в C, так и в Julia, могут быть встроены и, следовательно, могут иметь ещё меньшую накладную стоимость, чем вызовы функций из общей библиотеки. Суть в том, что стоимость фактического выполнения вызова внешней функции примерно одинакова с выполнением вызова на любом из родных языков.
Пакет 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.3.1/manual/calling-c-and-fortran-code/