Вызов кода 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. Одна распространенная проблема заключается в том, что кортеж типов аргументов длиной 1 должен быть написан с последующей запятой. Например, для вызова функции getenv для получения указателя на значение переменной среды выполняется вызов такого типа:
julia> path = ccall(:getenv, Cstring, (Cstring,), "SHELL") Cstring(@0x00007fff5fbffc45) julia> unsafe_string(path) "/bin/bash"
Обратите внимание, что кортеж типов аргументов должен быть написан как (Cstring,), а не как (Cstring). Это связано с тем, что (Cstring) — это просто выражение Cstring в скобках, а не кортеж длиной 1, содержащий Cstring:
julia> (Cstring) Cstring julia> (Cstring,) (Cstring,)
На практике, особенно при предоставлении многоразовых функциональных возможностей, обычно создаются оболочки ccall в функциях Julia, которые настраивают аргументы и затем проверяют наличие ошибок в соответствии с тем, как функция C или Fortran их указывает, распространяя их в вызывающую функцию Julia в виде исключений. Это особенно важно, так как API C и Fortran известны своей несовместимостью в способах указания условий возникновения ошибок. Например, функция C getenv обернута в следующую функцию Julia, которая представляет собой упрощенную версию фактического определения из env.jl:
function getenv(var::AbstractString)
val = ccall(:getenv, 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 проверяет наличие нулевых байтов и, следовательно, может вызвать ошибку преобразования.
Создание указателей на функции Julia, совместимых с C
Возможна передача функций 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. Например, это используется для преобразования массива объектов (например, строк) в массив указателей.
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}, происходит не переинтерпретация: 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 если завершается нулём, или Ptr{UInt8} если нет |
|
char** (или *char[]) |
Ptr{Ptr{UInt8}} |
||
jl_value_t* (любой тип Julia) |
Any |
||
jl_value_t** (ссылка на тип Julia) |
Ref{Any} |
||
va_arg |
Не поддерживается | ||
... (спецификация функции с переменным числом аргументов) |
T... (где T — один из вышеперечисленных типов; функции с переменным числом аргументов разных типов аргументов не поддерживаются) |
Тип Cstring по существу является синонимом для Ptr{UInt8}, за исключением того, что преобразование в Cstring вызывает ошибку, если строка Julia содержит вложенные символы NULL (что приведёт к неявной обрезке строки, если C-функция обрабатывает NULL как терминатор). Если вы передаёте char* в C-функцию, которая не предполагает завершения нулём (например, потому, что вы передаёте явную длину строки), или если вы уверены, что ваша строка Julia не содержит NULL и хотите пропустить проверку, вы можете использовать Ptr{UInt8} в качестве типа аргумента. Cstring также может использоваться в качестве типа возвращаемого значения ccall, но в этом случае, очевидно, не вводит дополнительных проверок и предназначен только для повышения читабельности вызова.
Зависимые от системы типы
| C name | Standard Julia Alias | Julia Base Type |
|---|---|---|
char |
Cchar |
Int8 (x86, x86_64), UInt8 (powerpc, arm) |
long |
Clong |
Int (UNIX), Int32 (Windows) |
unsigned long |
Culong |
UInt (UNIX), UInt32 (Windows) |
wchar_t |
Cwchar_t |
Int32 (UNIX), UInt16 (Windows) |
При вызове Fortran все входные данные должны передаваться через указатели на значения, выделенные в куче или на стеке, поэтому все соответствия типов выше должны содержать дополнительный Ptr{..} или Ref{..} обертки вокруг их спецификации типа.
Для строковых аргументов (char*) тип Julia должен быть Cstring (если ожидаются данные, завершённые 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)
Для функций 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, его функции могут принимать только типы аргументов со статически известной, фиксированной сигнатурой.
Однако, хотя структура типа должна быть известна статически для вычисления предполагаемого ABI C, статические параметры функции считаются частью этой статической среды. Статические параметры функции могут быть использованы в качестве параметров типа в сигнатуре вызова, если они не влияют на структуру типа. Например, 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, и известно, что он не равен 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 (здесь предполагается, что она доступна через :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 автоматически организует сохранение всех своих аргументов от сбора мусора до возврата вызова. Если API C сохранит ссылку на память, выделенную Julia, после возврата ccall, вы должны обеспечить, чтобы объект оставался видимым для сборщика мусора. Рекомендуемый способ сделать это — создать глобальную переменную типа Array{Ref,1} для хранения этих значений до тех пор, пока библиотека C не сообщит вам, что закончила с ними.
Всякий раз, когда вы создаёте указатель на данные Julia, вы должны убедиться, что исходные данные существуют до тех пор, пока вам не понадобится использовать указатель. Многие методы в Julia, такие как unsafe_load и String, создают копии данных вместо взятия владения буфером, чтобы было безопасно освобождать (или изменять) исходные данные без влияния на Julia. Заметное исключение — unsafe_wrap, который по соображениям производительности разделяет (или может быть настроен на взятие владения) базовым буфером.
Сборщик мусора не гарантирует никакого порядка завершения. То есть, если a содержала ссылку на b и оба a и b должны быть собраны мусором, нет гарантии, что b будет завершён после a. Если надлежащее завершение a зависит от того, что b действителен, это должно быть обработано другими способами.
Спецификации функций, не являющихся константными
Спецификация функции (name, library) должна быть константным выражением. Однако можно использовать вычисленные значения в качестве имён функций, используя eval следующим образом:
@eval ccall(($(string("a", "b")), "lib"), ...
Это выражение строит имя с использованием string, затем подставляет это имя в новое выражение ccall, которое затем оценивается. Имейте в виду, что eval работает только на верхнем уровне, поэтому внутри этого выражения локальные переменные недоступны (если их значения не подставлены с помощью $). По этой причине eval обычно используется только для формирования определений верхнего уровня, например, при обертывании библиотек, содержащих много похожих функций. Аналогичный пример можно построить для @cfunction.
Однако, выполнение этого также будет очень медленным и приведет к утечке памяти, поэтому обычно следует этого избегать и продолжать чтение. В следующем разделе рассматривается, как использовать косвенные вызовы для эффективного достижения аналогичного эффекта.
Косвенные вызовы
Первый аргумент функции ccall также может быть выражением, вычисляемым во время выполнения. В этом случае выражение должно вычисляться в Ptr, который будет использоваться как адрес вызываемой нативной функции. Это поведение возникает, когда первый аргумент функции ccall содержит ссылки на неконстанты, такие как локальные переменные, аргументы функций или неконстантные глобальные переменные.
Например, вы можете найти функцию с помощью dlsym, затем кешировать её в общей ссылке для этой сессии. Например:
macro dlsym(func, lib)
z = Ref{Ptr{Cvoid}}(C_NULL)
quote
let zlocal = $z[]
if zlocal == C_NULL
zlocal = dlsym($(esc(lib))::Ptr{Cvoid}, $(esc(func)))::Ptr{Cvoid}
$z[] = $zlocal
end
zlocal
end
end
end
mylibvar = Libdl.dlopen("mylib")
ccall(@dlsym("myfunc", mylibvar), Cvoid, ())
Закрытия cfunctions
Первый аргумент функции @cfunction может быть помечен $, в этом случае возвращаемое значение будет struct CFunction, который замыкает аргумент. Вы должны убедиться, что этот возвращаемый объект сохраняется до тех пор, пока все его использования не завершены. Содержимое и код в указателе cfunction будут удалены с помощью finalizer при отбрасывании этой ссылки и выходе из приложения. Обычно это не требуется, так как эта функциональность отсутствует в 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) мы видим тот же gethostname ccall, но с правильной сигнатурой для 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) может быть более полезной. Последний параметр должен быть 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.
- 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.4.2/manual/calling-c-and-fortran-code/