Вызов кода C и Fortran
Хотя большая часть кода может быть написана на Julia, существует много высококачественных, зрелых библиотек для численных вычислений, уже написанных на C и Fortran. Для простого использования этого существующего кода Julia делает вызов функций C и Fortran простым и эффективным. Философия Julia — "без лишнего кода": функции можно вызывать непосредственно из Julia без какого-либо "соединительного" кода, генерации кода или компиляции — даже из интерактивного приглашения. Это достигается путем совершения соответствующего вызова с помощью синтаксиса ccall, который выглядит как обычный вызов функции.
Вызываемый код должен быть доступен как общая библиотека. Большинство библиотек C и Fortran поставляются уже скомпилированными как общие библиотеки, но если вы сами компилируете код с помощью GCC (или Clang), вам нужно будет использовать опции -shared и -fPIC. Машинные инструкции, сгенерированные JIT Julia, такие же, как и при вызове нативного C, поэтому накладные расходы такие же, как и при вызове функции из библиотеки из кода C. (Вызовы функций, не относящихся к библиотеке, как в C, так и в Julia, могут быть встроены, и, таким образом, могут иметь еще меньше накладных расходов, чем вызовы функций из общей библиотеки. Когда и библиотеки, и исполняемые файлы генерируются LLVM, возможна оптимизация всего приложения, которая может оптимизировать даже через этот барьер, но Julia пока не поддерживает это. Однако в будущем это может быть сделано, что приведет к еще большим приростам производительности.)
Общие библиотеки и функции ссылаются на кортеж вида (:function, "library") или ("function", "library"), где function — имя экспортируемой функции C. library относится к имени общей библиотеки: доступные в (платформенно-специфическом) пути загрузки общие библиотеки будут разрешаться по имени, и при необходимости может быть указан прямой путь.
Имя функции может быть использовано само по себе вместо кортежа (только :function или "function"). В этом случае имя разрешается внутри текущего процесса. Этот формат можно использовать для вызова функций библиотеки C, функций среды выполнения Julia или функций приложения, связанного с Julia.
По умолчанию компиляторы Fortran генерируют замаскированные имена (например, преобразуют имена функций в нижний или верхний регистр, часто добавляя подчеркивание), и поэтому, чтобы вызвать функцию Fortran через ccall, необходимо передать замаскированный идентификатор, соответствующий правилу, используемому вашим компилятором Fortran. Кроме того, при вызове функции Fortran все входные данные должны передаваться как указатели на выделенные значения в куче или стеке. Это относится не только к массивам и другим изменяемым объектам, которые обычно выделяются в куче, но и к скалярным значениям, таким как целые числа и числа с плавающей точкой, которые обычно выделяются в стеке и часто передаются в регистрах при использовании соглашений о вызовах C или Julia.
Наконец, вы можете использовать ccall для фактического генерации вызова функции библиотеки. Аргументы для ccall следующие:
-
Пара
(:function, "library"), которая должна быть написана как буквальный константа,ИЛИ
указатель на функцию (например, из
dlsym). -
Тип возвращаемого значения (см. ниже сопоставление объявленного типа C с типом Julia)
- Этот аргумент будет вычислен во время компиляции, когда определен содержащий метод.
-
Кортеж типов входных данных. Типы входных данных должны быть написаны в виде буквального кортежа, а не кортежа-переменной или выражения.
- Этот аргумент будет вычислен во время компиляции, когда определен содержащий метод.
Следующие аргументы, если таковые имеются, — фактические значения аргументов, передаваемые в функцию.
В качестве полного, но простого примера, следующий вызов функции clock из стандартной библиотеки C:
julia> t = ccall((:clock, "libc"), Int32, ()) 2292761 julia> t 2292761 julia> typeof(ans) Int32
clock не принимает аргументов и возвращает Int32. Распространённая проблема — 1-кортеж должен быть написан с запятой в конце. Например, для вызова функции getenv для получения указателя на значение переменной среды, выполняется вызов такого вида:
julia> path = ccall((:getenv, "libc"), Cstring, (Cstring,), "SHELL") Cstring(@0x00007fff5fbffc45) julia> unsafe_string(path) "/bin/bash"
Обратите внимание, что кортеж типов аргументов должен быть записан как (Cstring,), а не как (Cstring). Это потому, что (Cstring) — это просто выражение Cstring в скобках, а не 1-кортеж, содержащий Cstring:
julia> (Cstring) Cstring julia> (Cstring,) (Cstring,)
На практике, особенно при предоставлении повторно используемой функциональности, обычно оборачивают ccall вызовы в функции Julia, которые устанавливают аргументы и затем проверяют наличие ошибок в той форме, в которой их указывает функция C или Fortran, распространяя их вызывающей стороне Julia в виде исключений. Это особенно важно, поскольку API C и Fortran печально известны своей несогласованностью в том, как они указывают условия возникновения ошибки. Например, функция C getenv обернута в следующую функцию Julia, которая является упрощенной версией фактического определения из env.jl:
function getenv(var::AbstractString)
val = ccall((:getenv, "libc"),
Cstring, (Cstring,), var)
if val == C_NULL
error("getenv: undefined variable: ", var)
end
unsafe_string(val)
end
Функция C getenv указывает ошибку, возвращая NULL, но другие стандартные функции C указывают ошибки различными способами, в том числе, возвращая -1, 0, 1 и другие специальные значения. Эта обёртка генерирует исключение, чётко указывающее на проблему, если вызывающая сторона пытается получить несуществующую переменную среды:
julia> getenv("SHELL")
"/bin/bash"
julia> getenv("FOOBAR")
getenv: undefined variable: FOOBAR
Вот несколько более сложный пример, который определяет имя хоста локального компьютера:
function gethostname()
hostname = Vector{UInt8}(undef, 128)
ccall((:gethostname, "libc"), Int32,
(Ptr{UInt8}, Csize_t),
hostname, sizeof(hostname))
hostname[end] = 0; # ensure null-termination
return unsafe_string(pointer(hostname))
end
В этом примере сначала выделяется массив байтов, затем вызывается функция библиотеки C gethostname для заполнения массива именем хоста, берется указатель на буфер имени хоста и преобразуется указатель в строку Julia, предполагая, что это строка C, завершённая нулём. Для библиотек C обычно используется эта схема, требующая от вызывающей стороны выделения памяти, которая передаётся вызываемой функции и заполняется. Выделение памяти из Julia таким образом обычно выполняется путём создания неинициализированного массива и передачи указателя на его данные функции C. Вот почему мы здесь не используем тип Cstring: поскольку массив не инициализирован, он может содержать нулевые байты. Преобразование в Cstring как часть ccall проверок на наличие вставленных нулевых байтов, и поэтому может выбросить ошибку преобразования.
Создание совместимых с C указателей на функции Julia
Можно передавать функции Julia в нативные функции C, которые принимают аргументы указателей на функции. Например, для соответствия прототипам C вида:
typedef returntype (*functiontype)(argumenttype, ...)
Макрос @cfunction генерирует совместимый с C указатель на функцию для вызова функции Julia. Аргументы для @cfunction следующие:
- Функция 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}, как вычислить размер типа в байтах (тождественно оператору C sizeof ) и так далее. Для интереса, попробуйте вставить строку println("mycompare($a, $b)") в mycompare, что позволит вам увидеть сравнения, которые выполняет qsort (и убедиться, что она действительно вызывает переданную вами функцию Julia).
Сопоставление типов C с типами Julia
Важно точно соответствовать объявленному типу C его объявлению в Julia. Несоответствия могут привести к тому, что код, правильно работающий на одной системе, будет работать неправильно или давать неопределённые результаты на другой.
Обратите внимание, что ни один заголовочный файл C не используется нигде в процессе вызова функций C: вы несете ответственность за обеспечение того, что ваши типы Julia и сигнатуры вызовов точно отражают типы и сигнатуры в заголовочном файле C. (Пакет Clang может использоваться для автоматической генерации кода Julia из заголовочного файла C.)
Автоматическое преобразование:
Julia автоматически вставляет вызовы функции Base.cconvert для преобразования каждого аргумента в указанный тип. Например, следующий вызов:
ccall((:foo, "libfoo"), Cvoid, (Int32, Float64), x, y)
поведет себя так, как будто было написано следующее:
ccall((:foo, "libfoo"), Cvoid, (Int32, Float64),
Base.unsafe_convert(Int32, Base.cconvert(Int32, x)),
Base.unsafe_convert(Float64, Base.cconvert(Float64, y)))
Base.cconvert обычно просто вызывает convert, но может быть определена для возврата произвольного нового объекта, более подходящего для передачи в C. Это следует использовать для выполнения всех выделений памяти, к которым будет обращаться код C. Например, это используется для преобразования 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) |
«Is-Bits» :: primitive type, или тип struct, где все поля являются другими типами isbits. Он определяется по значению и хранится без тега типа. |
|
struct ...; end |
nothing |
«Одиночный экземпляр» :: Листовой тип или структура без полей. |
(...) или tuple(...)
|
(1, 2, 3) |
«Кортеж» :: Неизменяемая структура данных, подобная анонимному типу структуры или массиву констант. Представлена как массив или структура. |
Типы битов
Существует несколько специальных типов, о которых следует знать, поскольку другие типы не могут быть определены с таким же поведением:
-
Float32Точно соответствует типу
floatв C (или типуREAL*4в Fortran). -
Float64Точно соответствует типу
doubleв C (или типуREAL*8в Fortran). -
ComplexF32Точно соответствует типу
complex floatв C (или типуCOMPLEX*8в Fortran). -
ComplexF64Точно соответствует типу
complex doubleв C (или типуCOMPLEX*16в Fortran). -
SignedТочно соответствует аннотации типа
signedв C (или любому типуINTEGERв Fortran). Любой тип Julia, который не является подтипомSigned, предполагается беззнаковым.
-
Ref{T}Ведет себя как
Ptr{T}и может управлять своей памятью с помощью сборщика мусора Julia.
-
Array{T,N}Когда массив передаётся в C как аргумент
Ptr{T}, он не преобразуется с помощью 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** (или *char[]) |
Ptr{Ptr{UInt8}} |
||
jl_value_t* (любой тип Julia) |
Any |
||
jl_value_t** (ссылка на тип Julia) |
Ref{Any} |
||
va_arg |
Не поддерживается | ||
... (спецификация функции с переменным числом аргументов) |
T... (где T — один из вышеперечисленных типов, функции с переменным числом аргументов разных типов не поддерживаются) |
Тип Cstring по существу является синонимом для Ptr{UInt8}, за исключением того, что преобразование в Cstring приводит к ошибке, если строка Julia содержит вложенные нули (что приведёт к неявной обрезке строки, если процедура C интерпретирует нуль как терминатор). Если вы передаёте char* в процедуру C, которая не предполагает завершения нулём (например, потому что вы передаёте явную длину строки), или если вы уверены, что ваша строка Julia не содержит нули и хотите пропустить проверку, вы можете использовать Ptr{UInt8} в качестве типа аргумента. Cstring также можно использовать в качестве типа возвращаемого значения ccall, но в этом случае он, очевидно, не вводит дополнительных проверок и предназначен только для повышения удобочитаемости вызова.
Зависимое от системы:
| Имя переменной в C | Стандартный псевдоним в Julia | Базовый тип в Julia |
|---|---|---|
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-функция ожидает строку, завершённую нулём) или Ptr{Cwchar_t} в противном случае. Обратите также внимание, что данные UTF-8 строк в Julia внутренне завершаются нулём, поэтому их можно передать в C-функции, ожидающие данные, завершённые нулём, без создания копии (но использование типа 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, Void, (Ptr{UInt8}, Ptr{UInt8}, Csize_t, Csize_t),
str1, str2, sizeof(str1), sizeof(str2))
Компиляторы Fortran могут также добавлять другие скрытые аргументы для указателей, массивов с предполагаемой формой (:) и массивов с предполагаемым размером (*). Такое поведение можно избежать, используя ISO_C_BINDING и включая bind(c) в определении подпрограммы, что настоятельно рекомендуется для совместимого кода. В этом случае не будет скрытых аргументов, в ущерб некоторым функция языка (например, только character(len=1) будет разрешено передавать строки).
C-функция, объявленная как возвращающая Cvoid , вернёт значение nothing в Julia.
Соответствие типов структур
Составные типы, также известные как struct в C или TYPE в Fortran90 (или STRUCTURE / RECORD в некоторых вариантах F77), могут быть отражены в Julia путём создания определения struct с той же структурой полей.
При рекурсивном использовании типы isbits хранятся inline. Все остальные типы хранятся как указатель на данные. При отображении структуры, используемой по значению внутри другой структуры в C, крайне важно не пытаться вручную скопировать поля, так как это не сохранит правильное выравнивание полей. Вместо этого, объявляйте тип структуры isbits и используйте его вместо этого. Неименованные структуры в переводе на Julia невозможны.
Упакованные структуры и объявления union не поддерживаются Julia.
Вы можете получить приблизительное представление union, если знаете заранее поле, которое будет иметь наибольший размер (включая потенциальный padding). При переводе ваших полей в Julia, объявите поле Julia только этого типа.
Массивы параметров могут быть выражены с использованием NTuple:
в C:
struct B {
int A[3];
};
b_a_2 = B.A[2];
в Julia:
struct B
A::NTuple{3, Cint}
end
b_a_2 = B.A[3] # note the difference in indexing (1-based in Julia, 0-based in C)
Массивы неизвестного размера (соответствующие C99-совместимые переменные длины, указанные [] или [0]) не поддерживаются напрямую. Часто лучший способ справиться с ними — это работать непосредственно с байтовыми смещениями. Например, если C-библиотека объявила правильный тип строки и вернула указатель на него:
struct String {
int strlen;
char data[];
};
В Julia мы можем обращаться к частям независимо, чтобы сделать копию этой строки:
str = from_c::Ptr{Cvoid}
len = unsafe_load(Ptr{Cint}(str))
unsafe_string(str + Core.sizeof(Cint), len)
Параметры типов
Аргументы типа ccall и @cfunction вычисляются статически, когда определяется метод, содержащий их использование. Поэтому они должны принимать вид литерального кортежа, а не переменной, и не могут ссылаться на локальные переменные.
Это может показаться странным ограничением, но помните, что так как C не является динамическим языком, как Julia, его функции могут принимать только типы аргументов со статически известной, фиксированной сигнатурой.
Однако, хотя макет типа должен быть статически известен для вычисления предполагаемого C ABI, статические параметры функции считаются частью этой статической среды. Статические параметры функции могут использоваться в качестве параметров типа в сигнатуре вызова, если они не влияют на макет типа. Например, f(x::T) where {T} = ccall(:valid, Ptr{T}, (Ptr{T},), x) допустимо, так как Ptr всегда является примитивным типом размера слова. Но g(x::T) where {T} = ccall(:notvalid, T, (T,), x) не допустимо, так как макет типа T не статически известен.
SIMD-значения
Примечание: эта функция в настоящее время реализована только на 64-битных платформах x86 и AArch64.
Если C/C++-процедура имеет аргумент или возвращаемое значение, являющееся родным типом SIMD, соответствующий тип Julia — это однородный кортеж из VecElement, который естественным образом отображается на тип SIMD. Конкретно:
- Кортеж должен иметь такой же размер, как и тип SIMD. Например, кортеж, представляющий
__m128на x86, должен иметь размер 16 байтов.- Тип элемента кортежа должен быть экземпляром
VecElement{T}, гдеT— это примитивный тип, размер которого составляет 1, 2, 4 или 8 байтов.
Например, рассмотрим эту C-процедуру, использующую AVX-инструкции:
#include <immintrin.h>
__m256 dist( __m256 a, __m256 b ) {
return _mm256_sqrt_ps(_mm256_add_ps(_mm256_mul_ps(a, a),
_mm256_mul_ps(b, b)));
}
Следующий код Julia вызывает dist с использованием ccall:
const m256 = NTuple{8, VecElement{Float32}}
a = m256(ntuple(i -> VecElement(sin(Float32(i))), 8))
b = m256(ntuple(i -> VecElement(cos(Float32(i))), 8))
function call_dist(a::m256, b::m256)
ccall((:dist, "libdist"), m256, (m256, m256), a, b)
end
println(call_dist(a,b))
Компьютер должен иметь необходимые регистры SIMD. Например, код выше не будет работать на компьютерах без поддержки AVX.
Владение памятью
malloc/free
Выделение и освобождение памяти таких объектов должны выполняться с помощью вызовов соответствующих процедур очистки в используемых библиотеках, как и в любой программе C. Не пытайтесь освободить объект, полученный из C-библиотеки с помощью Libc.free в Julia, так как это может привести к вызову функции free через неправильную библиотеку libc и привести к зависанию Julia. Аналогично, обратное (передача объекта, выделенного в Julia, для освобождения внешней библиотекой) также недопустимо.
Когда использовать T, Ptr{T} и Ref{T}
В коде Julia, оборачивающем вызовы внешних C-процедур, обычные (не указатель) данные должны быть объявлены как тип T внутри ccall, так как они передаются по значению. Для C-кода, принимающего указатели, Ref{T} обычно следует использовать для типов входных аргументов, позволяя использовать указатели на память, управляемую либо Julia, либо C, через неявный вызов Base.cconvert. В отличие от этого, указатели, возвращаемые C-функцией, должны быть объявлены как выходной тип Ptr{T}, отражая тот факт, что управляемая памятью C. Указатели, содержащиеся в C-структурах, должны быть представлены как поля типа Ptr{T} в соответствующих типах структур Julia, предназначенных для имитации внутренней структуры соответствующих C-структур.
В коде Julia, оборачивающем вызовы внешних Fortran-процедур, все входные аргументы должны быть объявлены типа Ref{T}, так как Fortran передает все переменные как указатели на места в памяти. Возвращаемый тип должен быть либо Cvoid для Fortran-подпрограмм, либо T для Fortran-функций, возвращающих тип T.
Сопоставление C-функций с Julia
ccall / @cfunction руководство по переводу аргументов
Для перевода списка аргументов C в Julia:
-
T, гдеT— один из примитивных типов:char,int,long,short,float,double,complex,enumили любой из ихtypedefэквивалентов-
T, гдеT— эквивалентный тип данных Julia Bits (согласно таблице выше) - если
T—enum, тип аргумента должен быть эквивалентенCintилиCuint - значение аргумента будет скопировано (передача по значению)
-
-
struct T(включая typedef для структуры)-
T, гдеT— тип данных Julia лист - значение аргумента будет скопировано (передача по значению)
-
-
void*- зависит от того, как используется этот параметр; сначала переведите его в целевой тип указателя, а затем определите эквивалент в Julia, используя оставшиеся правила в этом списке
- этот аргумент может быть объявлен как
Ptr{Cvoid}, если он действительно является просто неизвестным указателем
-
jl_value_t*Any- значение аргумента должно быть допустимым объектом Julia
-
jl_value_t**Ref{Any}- значение аргумента должно быть допустимым объектом Julia (или
C_NULL)
-
T*-
Ref{T}, гдеT— тип данных Julia, соответствующийT - значение аргумента будет скопировано, если это тип
isbits, в противном случае значение должно быть допустимым объектом Julia
-
-
T (*)(...)(например, указатель на функцию)-
Ptr{Cvoid}(возможно, вам потребуется явно использовать@cfunctionдля создания этого указателя)
-
-
...(например, vararg)-
T..., гдеT— тип данных Julia - в настоящее время не поддерживается
@cfunction
-
-
va_arg- не поддерживается
ccallили@cfunction
- не поддерживается
ccall / @cfunction руководство по преобразованию типа возвращаемого значения
Для преобразования типа возвращаемого значения C в Julia:
-
void-
Cvoid(это вернёт экземпляр синглтонаnothing::Cvoid)
-
-
T, гдеT— один из примитивных типов:char,int,long,short,float,double,complex,enumили любой из ихtypedefэквивалентов-
T, гдеT— эквивалентный тип данных Julia Bits (согласно таблице выше) - если
T—enum, тип аргумента должен быть эквивалентенCintилиCuint - значение аргумента будет скопировано (возвращается по значению)
-
-
struct T(включая typedef для структуры)-
T, гдеT— тип данных Julia лист - значение аргумента будет скопировано (возвращается по значению)
-
-
void*- зависит от того, как используется этот параметр; сначала переведите его в целевой тип указателя, а затем определите эквивалент в Julia, используя оставшиеся правила в этом списке
- этот аргумент может быть объявлен как
Ptr{Cvoid}, если он действительно является просто неизвестным указателем
-
jl_value_t*Any- значение аргумента должно быть допустимым объектом Julia
-
jl_value_t**-
Ptr{Any}(Ref{Any}недопустим в качестве типа возвращаемого значения) - значение аргумента должно быть допустимым объектом Julia (или
C_NULL)
-
-
T*-
Если память уже принадлежит Julia или это тип
isbits, и известно, что он не нулевой:-
Ref{T}, гдеT— тип данных Julia, соответствующийT - тип возвращаемого значения
Ref{Any}недопустим; он должен бытьAny(соответствующийjl_value_t*), илиPtr{Any}(соответствующийjl_value_t**) - C НЕЛЬЗЯ изменять память, возвращаемую через
Ref{T}, еслиT— типisbits
-
-
Если память принадлежит C:
-
Ptr{T}, гдеT— тип данных Julia, соответствующийT
-
-
-
T (*)(...)(например, указатель на функцию)-
Ptr{Cvoid}(возможно, вам потребуется явно использовать@cfunctionдля создания этого указателя)
-
Передача указателей для изменения входных данных
Так как C не поддерживает множество возвращаемых значений, часто функции C принимают указатели на данные, которые функция будет изменять. Для достижения этого в ccall необходимо сначала заключить значение в Ref{T} соответствующего типа. При передаче этого объекта Ref в качестве аргумента Julia автоматически передаст указатель C на заключённые данные:
width = Ref{Cint}(0)
range = Ref{Cfloat}(0)
ccall(:foo, Cvoid, (Ref{Cint}, Ref{Cfloat}), width, range)
При возвращении содержимое width и range может быть извлечено (если они были изменены foo), используя width[] и range[]; они ведут себя как нульмерные массивы.
Специальный синтаксис ссылок для ccall (устаревший):
Синтаксис & устарел; используйте вместо него тип аргумента Ref{T}.
Префикс & используется для аргумента ccall для обозначения того, что должен быть передан указатель на скалярный аргумент, а не само скалярное значение (требуется для всех аргументов функций Fortran, как указано выше). Следующий пример вычисляет скалярное произведение с помощью функции BLAS.
function compute_dot(DX::Vector{Float64}, DY::Vector{Float64})
@assert length(DX) == length(DY)
n = length(DX)
incx = incy = 1
product = ccall((:ddot_, "libLAPACK"),
Float64,
(Ref{Int32}, Ptr{Float64}, Ref{Int32}, Ptr{Float64}, Ref{Int32}),
n, DX, incx, DY, incy)
return product
end
Значение префикса & не совсем такое же, как в C. В частности, любые изменения в ссылаемых переменных не будут видны в Julia, если тип не является изменяемым (объявлен через mutable struct). Однако даже для неизменяемых структур это не нанесёт никакого вреда вызываемым функциям, если они попытаются произвести такие изменения (то есть запись через переданные указатели). Кроме того, & может быть использован с любым выражением, таким как &0 или &f(x).
Когда скалярное значение передаётся с префиксом & как аргумент типа Ptr{T}, значение сначала преобразуется к типу T.
Примеры обёрток C
Вот простой пример обёртки C, которая возвращает тип Ptr:
mutable struct gsl_permutation
end
# The corresponding C signature is
# gsl_permutation * gsl_permutation_alloc (size_t n);
function permutation_alloc(n::Integer)
output_ptr = ccall(
(:gsl_permutation_alloc, :libgsl), # name of C function and library
Ptr{gsl_permutation}, # output type
(Csize_t,), # tuple of input types
n # name of Julia variable to pass in
)
if output_ptr == C_NULL # Could not allocate memory
throw(OutOfMemoryError())
end
return output_ptr
end
Библиотека GNU Scientific Library (здесь предполагается, что она доступна через :libgsl определяет неявный указатель gsl_permutation * как тип возвращаемого значения функции C gsl_permutation_alloc. Так как пользователю никогда не нужно заглядывать внутрь структуры gsl_permutation, соответствующей обёртке Julia требуется только объявление нового типа gsl_permutation, у которого нет внутренних полей и чья единственная цель состоит в том, чтобы поместить его в параметр типа Ptr типа. Тип возвращаемого значения ccall объявляется как Ptr{gsl_permutation}, поскольку память, выделенная и указанная output_ptr управляется C (а не Julia).
Входной параметр n передаётся по значению, и поэтому сигнатура входных данных функции просто объявляется как (Csize_t,) без необходимости в Ref или Ptr. (Если бы обёртка вызывала функцию Fortran вместо этого, соответствующая сигнатура входных данных функции должна была бы быть (Ref{Csize_t},), так как переменные Fortran передаются по указателю.) Кроме того, n может быть любым типом, преобразуемым в целое число Csize_t; ccall неявно вызывает Base.cconvert(Csize_t, n).
Вот второй пример обёртки соответствующего деструктора:
# The corresponding C signature is
# void gsl_permutation_free (gsl_permutation * p);
function permutation_free(p::Ref{gsl_permutation})
ccall(
(:gsl_permutation_free, :libgsl), # name of C function and library
Cvoid, # output type
(Ref{gsl_permutation},), # tuple of input types
p # name of Julia variable to pass in
)
end
Здесь входной параметр p объявляется как тип Ref{gsl_permutation}, означая, что память, на которую указывает p, может управляться Julia или C. Указатель на память, выделенную C, должен быть типа Ptr{gsl_permutation}, но он преобразуется с помощью Base.cconvert и поэтому может использоваться в том же (ковариантном) контексте входного аргумента ccall. Указатель на память, выделенную Julia, должен быть типа Ref{gsl_permutation}, чтобы гарантировать, что адрес памяти, на который указывает указатель, корректен, и что сборщик мусора Julia управляет выделенным блоком памяти должным образом. Таким образом, объявление Ref{gsl_permutation} позволяет использовать указатели, управляемые C или Julia.
Если обёртка C никогда не ожидает, что пользователь передаст указатели на память, управляемую Julia, то использование p::Ptr{gsl_permutation} для сигнатуры метода обёртки и аналогично в ccall также допустимо.
Вот третий пример, передающий массивы Julia:
# The corresponding C signature is
# int gsl_sf_bessel_Jn_array (int nmin, int nmax, double x,
# double result_array[])
function sf_bessel_Jn_array(nmin::Integer, nmax::Integer, x::Real)
if nmax < nmin
throw(DomainError())
end
result_array = Vector{Cdouble}(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, а не C. Неявный вызов Base.cconvert(Ref{Cdouble}, result_array) распаковывает указатель Julia на структуру данных массива Julia в форму, понятную для C.
Обратите внимание, что для корректной работы этого кода, result_array должен быть объявлен как Ref{Cdouble}, а не как Ptr{Cdouble}. Управление памятью выполняется Julia, и сигнатура Ref уведомляет сборщик мусора Julia о необходимости продолжать управление памятью для result_array во время выполнения ccall. Если вместо Ptr{Cdouble} использовался бы ccall , то ccall может всё ещё работать, но сборщик мусора Julia не будет знать, что память, выделенная для result_array , используется внешней функцией C. В результате код может привести к утечке памяти, если result_array никогда не будет освобожден сборщиком мусора, или если сборщик мусора преждевременно освободит result_array, функция C может сгенерировать исключение недействительного доступа к памяти.
Безопасность сборки мусора
При передаче данных в ccall, лучше избегать использования функции pointer. Вместо этого определите метод преобразования и передайте переменные напрямую в ccall. ccall автоматически позаботится о том, чтобы все его аргументы были сохранены от сборки мусора до возврата результата вызова. Если API C будет хранить ссылку на память, выделенную Julia, после возврата ccall, вы должны обеспечить, чтобы объект оставался видимым для сборщика мусора. Рекомендуемый способ сделать это — создать глобальную переменную типа Array{Ref,1} для хранения этих значений до тех пор, пока библиотека C не сообщит вам, что она закончила с ними.
Всякий раз, когда вы создаете указатель на данные Julia, вы должны гарантировать, что исходные данные существуют до тех пор, пока вы не закончите использовать указатель. Многие методы в Julia, такие как unsafe_load и String, делают копии данных вместо того, чтобы брать на себя ответственность за буфер, так что безопасно освободить (или изменить) исходные данные, не затрагивая Julia. Заметным исключением является unsafe_wrap, который по соображениям производительности может совместно использовать (или ему можно сказать, что он берёт на себя ответственность за) базовый буфер.
Сборщик мусора не гарантирует какой-либо порядок финализации. То есть, если a содержала ссылку на b и a и b должны быть собраны сборщиком мусора, нет никакой гарантии, что b будет завершен после a . Если правильная финализация a зависит от того, является ли b допустимым, это должно обрабатываться другими способами.
Спецификации функций, не являющихся константами
Спецификация функции (name, library) должна быть константным выражением. Однако, возможно использовать вычисляемые значения в качестве имён функций, используя eval следующим образом:
@eval ccall(($(string("a", "b")), "lib"), ...
Это выражение создаёт имя, используя string, затем подставляет это имя в новое выражение ccall, которое затем вычисляется. Имейте в виду, что eval работает только на верхнем уровне, поэтому внутри этого выражения локальные переменные недоступны (если их значения не подставлены с помощью $). По этой причине eval обычно используется только для формирования определений верхнего уровня, например, при обертывании библиотек, содержащих много подобных функций. Аналогичный пример может быть построен для @cfunction.
Однако, это также будет очень медленным и вызовет утечку памяти, поэтому вы обычно должны этого избегать и продолжать чтение. Следующий раздел описывает, как использовать косвенные вызовы для эффективного достижения подобного эффекта.
Косвенные вызовы
Первый аргумент к ccall также может быть выражением, вычисляемым во время выполнения. В этом случае выражение должно вычисляться в Ptr, которое будет использоваться в качестве адреса вызываемой функции. Это поведение происходит, когда первый аргумент ccall содержит ссылки на неконстанты, такие как локальные переменные, аргументы функций или неконстантные глобальные переменные.
Например, вы можете найти функцию с помощью dlsym, а затем кешировать её в общей ссылке для этой сессии. Например:
macro dlsym(func, lib)
z = Ref{Ptr{Cvoid}}(C_NULL)
quote
let zlocal = $z[]
if zlocal == C_NULL
zlocal = dlsym($(esc(lib))::Ptr{Cvoid}, $(esc(func)))::Ptr{Cvoid}
$z[] = $zlocal
end
zlocal
end
end
end
mylibvar = Libdl.dlopen("mylib")
ccall(@dlsym("myfunc", mylibvar), Cvoid, ())
Закрытия cфункций
Первый аргумент к @cfunction может быть помечен $, в этом случае возвращаемое значение будет struct CFunction, который замыкает аргумент. Вы должны убедиться, что этот возвращаемый объект сохраняется до тех пор, пока все его использования не закончатся. Содержимое и код в указателе cfunction будут удалены с помощью finalizer при удалении этой ссылки и atexit. Это обычно не нужно, так как такая функциональность отсутствует в C, но может быть полезной для работы с плохо спроектированными API, которые не предоставляют отдельный параметр среды закрытия.
function qsort(a::Vector{T}, cmp) where T
isbits(T) || throw(ArgumentError("this method can only qsort isbits arrays"))
callback = @cfunction $cmp Cint (Ref{T}, Ref{T})
# Here, `callback` isa Base.CFunction, which will be converted to Ptr{Cvoid}
# (and protected against finalization) by the ccall
ccall(:qsort, Cvoid, (Ptr{T}, Csize_t, Csize_t, Ptr{Cvoid}),
a, length(a), Base.elsize(a), callback)
# We could instead use:
# GC.@preserve callback begin
# use(Base.unsafe_convert(Ptr{Cvoid}, callback))
# end
# if we needed to use it outside of a `ccall`
return a
end
Закрытие библиотеки
Иногда полезно закрыть (разгрузить) библиотеку, чтобы её можно было перезагрузить. Например, при разработке кода C для использования с Julia, может потребоваться скомпилировать, вызвать код C из Julia, затем закрыть библиотеку, внести правки, перекомпилировать и загрузить новые изменения. Вы можете либо перезапустить Julia, либо использовать функции Libdl для явного управления библиотекой, например:
lib = Libdl.dlopen("./my_lib.so") # Open the library explicitly.
sym = Libdl.dlsym(lib, :my_fcn) # Get a symbol for the function to call.
ccall(sym, ...) # Use the pointer `sym` instead of the (symbol, library) tuple (remaining arguments are the same).
Libdl.dlclose(lib) # Close the library explicitly.
Обратите внимание, что при использовании ccall с кортежем в качестве входных данных (например, ccall((:my_fcn, "./my_lib.so"), ...)), библиотека открывается неявно и может не закрываться явно.
Вызов соглашения
Второй аргумент к ccall необязательно может быть спецификатором соглашения о вызовах (непосредственно перед типом возвращаемого значения). Без спецификатора используется платформное по умолчанию соглашение о вызовах C. Другие поддерживаемые соглашения: stdcall, cdecl, fastcall, и thiscall (пустая операция в 64-битной Windows). Например (из base/libc.jl) мы видим ту же gethostnameccall, что и выше, но с правильной сигнатурой для Windows:
hn = Vector{UInt8}(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!.
Доступ к данным через указатель
Следующие методы описываются как «небезопасные», потому что неверный указатель или объявление типа могут привести к неожиданному завершению работы Julia.
Учитывая Ptr{T}, содержимое типа T обычно может быть скопировано из указанной памяти в объект Julia с помощью unsafe_load(ptr, [index]). Аргумент индекса является необязательным (по умолчанию равен 1) и следует соглашению Julia об индексировании с началом от 1. Эта функция преднамеренно похожа на поведение getindex и setindex! (например, синтаксис доступа []).
Возвращаемое значение — это новый объект, инициализированный копией содержимого указанной памяти. Указанную память можно безопасно освободить или высвободить.
Если T — Any, то предполагается, что память содержит ссылку на объект Julia (jl_value_t*), результат будет ссылкой на этот объект, и объект не будет скопирован. Вы должны быть осторожны в этом случае, чтобы убедиться, что объект всегда был виден сборщику мусора (указатели не учитываются, но новая ссылка учитывается), чтобы гарантировать, что память не будет преждевременно освобождена. Обратите внимание, что если объект изначально не был выделен Julia, новый объект никогда не будет завершен сборщиком мусора Julia. Если сам Ptr фактически является jl_value_t*, его можно преобразовать обратно в ссылку на объект Julia с помощью unsafe_pointer_to_objref(ptr). (Значения Julia v могут быть преобразованы в указатели jl_value_t*, как Ptr{Cvoid}, вызвав pointer_from_objref(v).)
Обратная операция (запись данных в Ptr{T}) может быть выполнена с помощью unsafe_store!(ptr, value, [index]). В настоящее время это поддерживается только для примитивных типов или других указателей без указателей (isbits). immutable) типов структур.
Любая операция, которая генерирует ошибку, вероятно, в настоящее время не реализована и должна быть зарегистрирована как ошибка, чтобы её можно было исправить.
Если указатель, который нас интересует, является массивом простых данных (примитивного типа или неизменяемой структуры), функция unsafe_wrap(Array, ptr,dims, own = false) может быть более полезной. Последний параметр должен быть истинным, если Julia должна «взять на себя ответственность» за базовый буфер и вызвать free(ptr) при финализации возвращённого объекта Array. Если параметр own опущен или ложный, вызывающая функция должна гарантировать, что буфер останется существующим до завершения всех обращений.
Арифметические операции с типом Ptr в Julia (например, используя +) не ведут себя так же, как арифметика указателей в C. Добавление целого числа к Ptr в Julia всегда перемещает указатель на определённое количество байтов, а не элементов. Таким образом, значения адресов, полученные из арифметики указателей, не зависят от типов элементов указателей.
Безопасность потоков
Некоторые библиотеки C выполняют свои обратные вызовы из другого потока, и поскольку Julia не является потокобезопасной, вам необходимо принять дополнительные меры предосторожности. В частности, вам необходимо настроить двухслойную систему: обратный вызов C должен только планировать (через цикл событий Julia) выполнение вашего «действительного» обратного вызова. Для этого создайте объект AsyncCondition и wait на нём:
cond = Base.AsyncCondition() wait(cond)
Обратный вызов, который вы передаёте в C, должен только выполнить ccall в :uv_async_send, передавая cond.handle в качестве аргумента, позаботившись об избежании выделения памяти или других взаимодействий с окружением Julia.
Обратите внимание, что события могут быть объединены, поэтому несколько вызовов uv_async_send могут привести к одному уведомлению о пробуждении условия.
Подробнее об обратных вызовах
Для получения более подробной информации о том, как передавать обратные вызовы в библиотеки C, ознакомьтесь с этим постом в блоге.
C++
Для прямого взаимодействия с C++, см. пакет Cxx. Для инструментов создания связей C++, см. пакет CxxWrap.
© 2009–2019 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.0.4/manual/calling-c-and-fortran-code/