Вызов кода на C и Фортране
Хотя на Julia можно писать практически любой код, существует множество готовых, развитых библиотек для числовых вычислений на C и Фортране. Для того чтобы этим существующим кодом можно было легко воспользоваться, Julia поддерживает эффективный вызов функций на C и Фортране. В Julia действует принцип «нет стереотипам»: функции можно вызывать напрямую из Julia без связующего кода, генерации кода или компиляции — даже из интерактивной командной строки. Для этого достаточно выполнить соответствующий вызов с помощью макроса @ccall
(или менее удобного синтаксиса ccall
; см. раздел о синтаксисе ccall
).
Вызываемый код должен быть доступен в виде общей библиотеки. Большинство библиотек на C и Фортране поставляются как уже скомпилированные общие библиотеки, но если вы компилируете код самостоятельно с помощью GCC (или Clang), необходимо использовать параметры -shared
и -fPIC
. Машинные инструкции, генерируемые JIT-компилятором Julia, аналогичны собственному вызову C, поэтому суммарные издержки в итоге будут такими же, как при вызове библиотечной функции из кода на C. [1]
По умолчанию компиляторы Фортрана генерируют скорректированные имена (например, переводят имена функций в нижний или верхний регистр, часто добавляя в конце символ подчеркивания). Поэтому для вызова функции на Фортране необходимо передать имя, скорректированное согласно правилам, применяемым в вашем компиляторе Фортрана. Кроме того, при вызове функции на Фортране все входные данные должны передаваться как указатели на значения, размещенные в "куче" или стеке. Это относится не только к массивам и другим изменяемым объектам, которые обычно размещаются в "куче", но и к скалярным значениям, например целочисленным или с плавающей запятой, которые обычно размещаются в стеке и передаются в регистрах согласно соглашениям о вызовах C или Julia.
Синтаксис @ccall
для создания вызова библиотечной функции имеет следующий вид:
@ccall library.function_name(argvalue1::argtype1, ...)::returntype
@ccall function_name(argvalue1::argtype1, ...)::returntype
@ccall $function_pointer(argvalue1::argtype1, ...)::returntype
где library
является строковой константой или литералом (однако см. раздел Неконстантные спецификации функций ниже). Библиотека может быть опущена, и в этом случае имя функции разрешается в текущем процессе. Такую форму можно использовать для вызова библиотечных функций на C, функций в среде выполнения Julia или функций в приложении, связанном с Julia. Можно также указать полный путь к библиотеке. Кроме того, можно также использовать @ccall
для вызова указателя функции $function_pointer
, например возвращенного Libdl.dlsym
. argtype
s соответствует сигнатуре функции C, а argvalue
s — это фактические значения аргументов, которые должны быть переданы в функцию.
Сведения о сопоставлении типов C и Julia см. ниже. |
Ниже приведен простой, но полнофункциональный пример вызова функции clock
из стандартной библиотеки C в большинстве Unix-подобных систем.
julia> t = @ccall clock()::Int32
2292761
julia> typeof(t)
Int32
Функция clock
не принимает аргументов и возвращает значение типа Int32
. Вызов функции getenv
, возвращающей указатель на значение переменной среды, должен выглядеть так:
julia> path = @ccall getenv("SHELL"::Cstring)::Cstring
Cstring(@0x00007fff5fbffc45)
julia> unsafe_string(path)
"/bin/bash"
На практике, особенно когда требуется возможность повторного использования, вызовы @ccall
обычно заключаются в функции Julia, которые задают аргументы, а затем проверяют наличие ошибок, как то предписывает функция на C или Фортране. Если ошибка происходит, вызывается обычное исключение Julia. Это особенно важно по той общеизвестной причине, что API-интерфейсы C и Фортрана по-разному сообщают о состоянии ошибки. Например, функция getenv
из библиотеки C заключается в следующую функцию Julia, которая является упрощенной версией определения из env.jl
:
function getenv(var::AbstractString)
val = @ccall getenv(var::Cstring)::Cstring
if val == C_NULL
error("getenv: undefined variable: ", var)
end
return unsafe_string(val)
end
Функция C getenv
сообщает об ошибке, возвращая C_NULL
, однако другие стандартные функции C могут делать это иначе, например, возвращать --1, 0, 1 и другие специальные значения. Если вызывающий объект пытается получить несуществующую переменную среды, эта оболочка вызывает исключение, в котором сообщается возникшая проблема:
julia> getenv("SHELL")
"/bin/bash"
julia> getenv("FOOBAR")
ERROR: getenv: undefined variable: FOOBAR
Вот немного более сложный пример, в котором определяется имя хоста локального компьютера.
function gethostname()
hostname = Vector{UInt8}(undef, 256) # MAXHOSTNAMELEN
err = @ccall gethostname(hostname::Ptr{UInt8}, sizeof(hostname)::Csize_t)::Int32
Base.systemerror("gethostname", err != 0)
hostname[end] = 0 # обеспечиваем завершающий нулевой символ
return GC.@preserve hostname unsafe_string(pointer(hostname))
end
В этом примере в памяти сначала размещается массив байтов. Затем вызывается функция gethostname
из библиотеки C для заполнения массива именем хоста. Наконец, берется указатель на буфер с именем хоста (предполагается, что это строка C с завершающим нулевым символом) и преобразуется в строку Julia.
Библиотеки C часто требуют от вызывающего объекта выделить область памяти, которая будет передана вызываемому объекту для заполнения. В Julia для этого обычно создается неинициализированный массив, указатель на данные которого передается в функцию C. Вот почему здесь не используется тип Cstring
: так как массив не инициализирован, он может содержать нулевые байты. При преобразовании в Cstring
в рамках вызова @ccall
проверяется наличие нулевых байтов, из-за чего может произойти ошибка преобразования.
Разыменование указателя pointer(hostname)
с помощью unsafe_string
— небезопасная операция, так как она требует доступа к области памяти, выделенной для hostname
, которая может быть уже очищена сборщиком мусора. Макрос GC.@preserve
блокирует такую очистку и тем самым предотвращает доступ к недопустимой области памяти.
Наконец, вот пример указания библиотеки в виде пути. Мы создаем общую библиотеку со следующим содержимым:
#include <stdio.h>
void say_y(int y)
{
printf("Hello from C: got y = %d.\n", y);
}
и компилируем ее с помощью команды gcc -fPIC -shared -o mylib.so mylib.c
. После этого ее можно вызвать, указав в качестве имени библиотеки путь (абсолютный):
julia> @ccall "./mylib.so".say_y(5::Cint)::Cvoid
Hello from C: got y = 5.
Создание указателей функций Julia, совместимых с C
Функции Julia можно передавать в неуправляемые функции C, которые принимают указатели функций в качестве аргументов. Например, так можно обеспечить соответствие прототипу C следующего вида.
typedef returntype (*functiontype)(argumenttype, ...)
Макрос @cfunction
создает совместимый с C указатель функции для вызова функции Julia. Функция @cfunction
имеет следующие аргументы.
-
Функция Julia
-
Возвращаемый тип функции
-
Кортеж входных типов, соответствующий сигнатуре функции
Так же как и в случае с |
В настоящее время поддерживается только соглашение о вызовах C, принятое по умолчанию для платформы. Это означает, что создаваемые макросом |
Функции обратного вызова, предоставляемые посредством |
Классический пример — функция qsort
из стандартной библиотеки C, объявленная следующим образом.
void qsort(void *base, size_t nitems, size_t size,
int (*compare)(const void*, const void*));
Аргумент base
— это указатель на массив длиной nitems
, каждый элемент которого имеет размер size
байтов. compare
— это функция обратного вызова, которая принимает указатели на два элемента (a
и b
) и возвращает целое число больше или меньше нуля, если элемент a
должен находиться перед элементом b
или после него (либо ноль, если допустим любой порядок).
Теперь предположим, что в Julia есть одномерный массив значений A
, который нужно отсортировать с помощью функции qsort
(а не встроенной функции Julia sort
). Перед вызовом qsort
и передачей аргументов нужно написать функцию сравнения:
julia> function mycompare(a, b)::Cint
return (a < b) ? -1 : ((a > b) ? +1 : 0)
end;
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];
julia> @ccall qsort(A::Ptr{Cdouble}, length(A)::Csize_t, sizeof(eltype(A))::Csize_t, mycompare_c::Ptr{Cvoid})::Cvoid
julia> A
4-element Vector{Float64}:
-2.7
1.3
3.1
4.4
Как показано в примере, исходный массив Julia A
теперь отсортирован: [-2.7, 1.3, 3.1, 4.4]
. Обратите внимание, что Julia выполняет преобразование массива в Ptr{Cdouble}
, вычисляя размер типа элементов в байтах и т. д.
Для интереса попробуйте вставить строку println("mycompare($a, $b)")
в mycompare
. Это позволит увидеть, какие сравнения производит функция qsort
(и проверить, действительно ли она вызывает переданную функцию Julia).
Сопоставление типов C и Julia
Крайне важно, чтобы объявления типов в C и Julia в точности совпадали. Из-за несоответствий код, который правильно работает в одной системе, может завершаться сбоем или давать неопределенные результаты в другой.
Обратите внимание, что в процессе вызова функций на C файлы заголовков C не используются: за соответствие типов и сигнатур вызовов Julia файлу заголовков C отвечаете вы [2].
Автоматическое преобразование типов
Julia автоматически добавляет вызовы функции Base.cconvert
для преобразования каждого аргумента в указанный тип. Например, вызов:
@ccall "libfoo".foo(x::Int32, y::Float64)::Cvoid
будет выполняться так, как если бы он имел следующий вид:
@ccall "libfoo".foo(
Base.unsafe_convert(Int32, Base.cconvert(Int32, x))::Int32,
Base.unsafe_convert(Float64, Base.cconvert(Float64, y))::Float64
)::Cvoid
Функция Base.cconvert
обычно просто вызывает convert
, но ее можно переопределить так, чтобы она возвращала другой произвольный объект, который лучше подходит для передачи в C. Таким образом должно осуществляться выделение всех областей памяти, к которым будет обращаться код на C. Например, таким образом массив Array
объектов (допустим, строк) преобразовывается в массив указателей.
Base.unsafe_convert
осуществляет преобразование в типы Ptr
. Эта операция считается небезопасной, так как преобразование объекта в неуправляемый указатель может сделать его недоступным для сборщика мусора, из-за чего он будет уничтожен раньше времени.
Соответствие типов
Сначала давайте рассмотрим ряд важных терминов, касающихся типов в Julia.
Синтаксис или ключевое слово | Пример | Описание |
---|---|---|
|
|
Конечный тип ("Leaf Type"): набор связанных данных, который включает в себя метку типа, управляется сборщиком мусора Julia и определяется идентификатором объекта. Для создания экземпляра конечного типа необходимо полностью определить параметры типа (переменные |
|
|
Супертип ("Super Type"): тип, не являющийся конечным, экземпляры которого создавать нельзя, но который можно использовать для описания группы типов. |
|
|
Параметр типа ("Type Parameter"): специализация типа (обычно применяется для диспетчеризации или оптимизации хранения). |
"TypeVar": элемент |
||
|
|
Примитивный тип ("Primitive Type"): тип, не имеющий полей, но имеющий определенный размер. Хранится и определяется по значению. |
|
|
Структура ("Struct"): тип, все поля которого определены как константы. Определяется по значению и может храниться с меткой типа. |
|
Битовый тип ("Is-Bits"): примитивный тип ( |
|
|
|
Одинарный тип ("Singleton"): конечный тип или структура (struct) без полей. |
|
|
Кортеж ("Tuple"): неизменяемая структура данных, аналогичная анонимному типу структуры или массиву констант. Представлена массивом или структурой (struct). |
Битовые типы
Особое внимание следует обратить на ряд специальных типов, особенности которых нельзя реализовать в других типах:
-
Float32
В точности соответствует типу
float
в C (илиREAL*4
в Фортране). -
Float64
В точности соответствует типу
double
в C (илиREAL*8
в Фортране). -
ComplexF32
В точности соответствует типу
complex float
в C (илиCOMPLEX*8
в Фортране). -
ComplexF64
В точности соответствует типу
complex double
в C (илиCOMPLEX*16
в Фортране). -
Signed
В точности соответствует аннотации типа
signed
в C (или любому типуINTEGER
в Фортране). Любой тип 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}
.Если массив типа eltype
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 | Имя в Фортране | Стандартный псевдоним в Julia | Базовый тип Julia |
---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
||
|
|||
|
|
||
|
|
||
|
|
|
|
|
|
||
|
|
||
|
|
||
|
Не поддерживается |
||
|
|
||
|
|
Тип Cstring
— это, по сути, синоним Ptr{UInt8}
, за тем исключением, что преобразование в Cstring
вызывает ошибку, если строка Julia содержит внедренные нулевые символы (такая строка была бы автоматически обрезана, если бы подпрограмма на C расценивала нулевой символ как завершающий). Если вы передаете char*
в подпрограмму на C, которая не ожидает завершающих нулевых символов (например, потому, что вы явным образом передаете длину строки), или если вы уверены, что строка Julia не содержит нулевых символов, и хотите пропустить проверку, в качестве типа аргумента можно использовать Ptr{UInt8}
. Cstring
также можно использовать в качестве возвращаемого типа ccall
, но в этом случае дополнительные проверки не проводятся: это делается лишь для повышения удобочитаемости кода.
Типы, зависимые от системы
Имя в C | Стандартный псевдоним в Julia | Базовый тип Julia |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
При вызове кода на Фортране все входные данные должны передаваться в виде указателей на значения, размещенные в «куче» или стеке, поэтому спецификации всех перечисленных выше соответствующих типов должны заключаться в дополнительную оболочку |
Для строковых аргументов ( |
Тип |
Для аргументов |
Функции на C, принимающие аргумент типа
можно вызвать с помощью следующего кода Julia:
|
Для функций на Фортране, принимающих строки переменной длины типа
можно вызвать с помощью следующего кода Julia, где длины строк добавлены в конце списка:
|
Компиляторы Фортрана могут также добавлять другие скрытые аргументы для указателей и массивов предполагаемой формы ( |
Функция на C, объявленная как возвращающая |
Соответствие типов структур
Составные типы, такие как struct
в C или TYPE
в Фортране 90 (либо STRUCTURE
и RECORD
в некоторых вариантах Фортрана 77), можно представить в 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] # обратите внимание на различия в индексировании (в Julia от 1, в C от 0)
Массивы неизвестного размера (структуры переменной длины, соответствующие стандарту 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
, вычисляются статически, когда определяется метод, в котором они используются. Поэтому такие аргументы должны представлять собой литеральный кортеж, а не переменную, и не могут ссылаться на локальные переменные.
Это ограничение может показаться странным, но не забывайте, что, в отличие от Julia, C — это не динамический язык, и его функции могут принимать только такие типы аргументов, которые имеют статичную, фиксированную сигнатуру.
Однако хотя структура типа должна быть задана статически для определения необходимого интерфейса ABI C, статические параметры функции считаются частью этой статической среды. Статические параметры функции можно использовать в качестве параметров типов в сигнатуре вызова, если они не влияют на структуру типа. Например, вызов f(x::T) where {T} = @ccall valid(x::Ptr{T})::Ptr{T}
допустим, так как Ptr
— это всегда примитивный тип размером в машинное слово. В свою очередь, вызов g(x::T) where {T} = @ccall notvalid(x::T)::T
недопустим, так как структура типа 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 "libdist".dist(a::m256, b::m256)::m256
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, в котором вызываются внешние подпрограммы на Фортране, все входные аргументы должны объявляться с типом Ref{T}
, так как в Фортране все переменные передаются по указателям на области памяти. Возвращаемым типом должен быть либо Cvoid
для подпрограмм на Фортране, либо T
для функций на Фортране, возвращающих тип 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* const*
-
Ref{Any}
-
Список аргументов должен быть допустимым объектом Julia (или
C_NULL
) -
Нельзя использовать для выходного параметра, если только пользователь не может организовать сохранение объекта сборщиком мусора
-
-
T*
-
Ref{T}
, гдеT
— тип Julia, соответствующийT
-
Значение аргумента копируется, если это тип
inlinealloc
(куда относятся и типыisbits
), в противном случае значение должно быть допустимым объектом Julia
-
-
T (*)(...)
(например, указатель на функцию)-
Ptr{Cvoid}
(для создания этого указателя может потребоваться использовать макрос@cfunction
явным образом)
-
-
...
(например, vararg)-
Для
ccall
:T...
, гдеT
— единственный тип Julia для всех оставшихся аргументов -
Для
@ccall
:; va_arg1::T, va_arg2::S, etc
, гдеT
иS
— типы 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 (согласно таблице выше) -
Если
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}
не может использоваться как возвращаемый тип)
-
-
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}
, для вызова непосредственно из Julia необходимо передать этот указатель в качестве первого аргумента функции@ccall
. См. раздел Косвенные вызовы.
-
Передача указателей для изменения входных данных
Так как в C не поддерживается несколько возвращаемых значений, функции C часто принимают указатели на данные, которые подлежат изменению. Чтобы реализовать это в вызове @ccall
, необходимо сначала инкапсулировать значение в объекте Ref{T}
соответствующего типа. При передаче этого объекта Ref
в качестве аргумента Julia автоматически передает указатель C на инкапсулированные данные:
width = Ref{Cint}(0)
range = Ref{Cfloat}(0)
@ccall foo(width::Ref{Cint}, range::Ref{Cfloat})::Cvoid
После возврата управления содержимое переменных width
и range
можно получить (если оно было изменено функцией foo
) посредством width[]
и range[]
, то есть они выступают в качестве нульмерных массивов.
Примеры оболочек C
Начнем с простого примера оболочки C, которая возвращает тип Ptr
:
mutable struct gsl_permutation
end
# Соответствующая сигнатура в C имеет вид
# gsl_permutation * gsl_permutation_alloc (size_t n);
function permutation_alloc(n::Integer)
output_ptr = @ccall "libgsl".gsl_permutation_alloc(n::Csize_t)::Ptr{gsl_permutation}
if output_ptr == C_NULL # Не удалось выделить память
throw(OutOfMemoryError())
end
return output_ptr
end
В научной библиотеке GNU (здесь предполагается, что она доступна по имени :libgsl
) в качестве возвращаемого типа функции C gsl_permutation_alloc
определен непрозрачный указатель gsl_permutation *
. Так как пользовательскому коду не нужно обращаться внутрь структуры gsl_permutation
, для реализации соответствующей оболочки Julia достаточно объявить новый тип gsl_permutation
, у которого нет внутренних полей и единственная цель которого — указание в параметре типа Ptr
. Для функции ccall
объявлен возвращаемый тип Ptr{gsl_permutation}
, так как область памяти, которая выделяется с помощью указателя output_ptr
и на которую он указывает, контролируется средой C.
Входной аргумент n
передается по значению, поэтому сигнатура входных данных функции имеет очень простой вид: ::Csize_t
. В Ref
или Ptr
нет необходимости. (Если вы оболочка вместо этого вызывала функцию на Фортране, соответствующая сигнатура входных данных имела бы вид ::Ref{Csize_t}
, так как переменные в Фортране передаются по указателям.) Более того, n
может иметь любой тип, который можно преобразовать в целочисленное значение Csize_t
; функция ccall
неявно вызывает Base.cconvert(Csize_t, n)
.
Вот еще один пример, на этот раз оболочки соответствующего деструктора:
# Соответствующая сигнатура в C имеет вид
# void gsl_permutation_free (gsl_permutation * p);
function permutation_free(p::Ptr{gsl_permutation})
@ccall "libgsl".gsl_permutation_free(p::Ptr{gsl_permutation})::Cvoid
end
Вот третий пример, в котором передаются массивы Julia:
# Соответствующая сигнатура в C имеет вид
# 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 "libgsl".gsl_sf_bessel_Jn_array(
nmin::Cint, nmax::Cint, x::Cdouble, result_array::Ref{Cdouble})::Cint
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.
Пример оболочки для Фортрана
В следующем примере с помощью ccall
вызывается функция из стандартной библиотеки Фортрана (libBLAS) для вычисления скалярного произведения. Обратите внимание: аргументы в этом случае сопоставляются немного не так, как выше, так как сопоставление производится из Julia в Фортран. Для каждого типа аргумента указывается Ref
или Ptr
. Это соглашение о преобразовании может зависеть от конкретного компилятора Фортрана и операционной системы и, скорее всего, не отражается в документации. Однако многие реализации компилятора Фортрана требуют заключать каждый тип аргумента в Ref
(или Ptr
, где применимо):
function compute_dot(DX::Vector{Float64}, DY::Vector{Float64})
@assert length(DX) == length(DY)
n = length(DX)
incx = incy = 1
product = @ccall "libLAPACK".ddot(
n::Ref{Int32}, DX::Ptr{Float64}, incx::Ref{Int32}, DY::Ptr{Float64}, incy::Ref{Int32})::Float64
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
и оба они подлежат сборке мусора, нет никакой гарантии, что объект b
будет ликвидирован после a
. Если для ликвидации объекта a
должен существовать объект b
, требуется иной подход.
Неконстантные спецификации функций
В некоторых случаях имя или путь нужной библиотеки заранее неизвестны и должны определяться во время выполнения. Для таких случаев спецификация компонента library (библиотека) может быть вызовом функции, например find_blas().dgemm
. Выражение вызова будет выполнено при совершении операции ccall
. Однако предполагается, что после определения расположения библиотеки оно не меняется, поэтому результат вызова можно кэшировать и использовать повторно. Таким образом, выражение может выполняться любое количество раз, и возврат разных значений может давать неопределенный результат.
Если требуется еще большая гибкость, в качестве имен функций можно использовать вычисленные значения посредством функции eval
следующим образом.
@eval @ccall "lib".$(string("a", "b"))()::Cint
Это выражение генерирует имя посредством string
, а затем подставляет его в новое выражение @ccall
, которое затем вычисляется. Учтите, что функция eval
является высокоуровневой, поэтому в этом выражении локальные переменные будут недоступны (если их значения не подставляются с $
). По этой причине функция eval
обычно используется для создания только определений верхнего уровня, например при инкапсуляции библиотек, содержащих множество сходных функций. Аналогичный пример возможен для макроса @cfunction
.
Однако такой код будет выполняться очень медленно и с утечками памяти, поэтому старайтесь не использовать такой вариант. В следующем разделе рассказывается, как достичь такого же результата с помощью косвенных вызовов.
Косвенные вызовы
Первый аргумент в вызове @ccall
также может быть выражением, вычисляемым во время выполнения. Результатом такого выражения должен быть указатель Ptr
, который будет использоваться в качестве адреса вызываемой нативной функции. Такое поведение имеет место, когда первый аргумент @ccall
содержит ссылки на неконстантные значения, например локальные переменные, аргументы функции или неконстантные глобальные переменные.
Например, можно определить функцию с помощью dlsym
, а затем кэшировать ее в общей ссылке в рамках сеанса. Пример:
macro dlsym(lib, func)
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(mylibvar, "myfunc"))()::Cvoid
Функции замыкания cfunction
Первый аргумент @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})
# Здесь `callback` — это функция Base.CFunction, которая будет преобразована в Ptr{Cvoid}
# (и защищена от ликвидации) посредством ccall
@ccall qsort(a::Ptr{T}, length(a)::Csize_t, Base.elsize(a)::Csize_t, callback::Ptr{Cvoid})
# Вместо этого можно использовать:
# GC.@preserve callback begin
# use(Base.unsafe_convert(Ptr{Cvoid}, callback))
# end
# если функцию необходимо использовать вне вызова `ccall`
return a
end
Функция замыкания |
Закрытие библиотеки
Иногда бывает полезно закрыть (выгрузить) библиотеку, чтобы ее можно было загрузить заново. Например, при написании кода на C, который будет использоваться с Julia, может потребоваться выполнить компиляцию, вызвать код C из Julia, затем закрыть библиотеку, внести изменения, выполнить компиляцию заново и загрузить новые изменения. Для этого можно либо перезапустить Julia, либо использовать функции Libdl
для управления библиотекой явным образом, например так.
lib = Libdl.dlopen("./my_lib.so") # Открываем библиотеку явным образом.
sym = Libdl.dlsym(lib, :my_fcn) # Получаем символ для вызываемой функции.
@ccall $sym(...) # Используем указатель `sym` вместо кортежа library.symbol.
Libdl.dlclose(lib) # Закрываем библиотеку явным образом.
Обратите внимание, что при использовании @ccall
с входными данными (например, @ccall "./my_lib.so".my_fcn(...)::Cvoid
) библиотека открывается неявным образом и может не закрываться явным образом.
Вызовы функций с переменным числом аргументов
Для вызова функций C с переменным числом аргументов можно разделять обязательные аргументы от аргументов переменного количества в списке аргументов точкой с запятой (semicolon
). Ниже приведен пример вызова функции printf
.
julia> @ccall printf("%s = %d\n"::Cstring ; "foo"::Cstring, foo::Cint)::Cint
foo = 3
8
Интерфейс ccall
Это еще один интерфейс, который может служить альтернативой @ccall
. Он немного менее удобен, но позволяет задать соглашение о вызовах.
Функция ccall
имеет следующие аргументы.
-
Пара
(:function, "library")
(чаще всего),ИЛИ
символ имени
:function
либо строка имени"function"
(для символов в текущем процессе или библиотеке libc),ИЛИ
указатель функции (например, из
dlsym
). -
Возвращаемый тип функции
-
Кортеж входных типов, соответствующий сигнатуре функции. В одноэлементном кортеже типов аргументов не забывайте ставить запятую в конце.
-
Фактические значения аргументов, которые должны быть переданы в функцию, если они есть; каждое значение — отдельный параметр.
Пара |
Остальные параметры вычисляются во время компиляции, если определен содержащий метод.
Таблица преобразований между интерфейсами макросов и функций приведена ниже.
@ccall |
ccall |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Соглашение о вызовах
Вторым аргументом вызова ccall
(непосредственно перед возвращаемым типом) может быть спецификатор соглашения о вызовах (в настоящее время макрос @ccall
не позволяет указывать соглашение о вызовах). Если спецификатор не указан, используется соглашение о вызовах C, принятое по умолчанию для платформы. Кроме того, поддерживаются следующие соглашения: stdcall
, cdecl
, fastcall
и thiscall
(не действует в 64-разрядных системах Windows). Например, так выглядит вызов для функции gethostname``ccall
, аналогичный приведенному выше, но с правильной сигнатурой для Windows (код взят из base/libc.jl
):
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) и индексируется от 1 согласно принятому в Julia соглашению. Поведение этой функции намеренно сделано похожим на поведение функций 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++ можно найти в пакете CxxWrap.