Вызов кода 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 все входные данные должны передаваться по ссылке.
Наконец, вы можете использовать ccall() для фактического генерации вызова функции библиотеки. Аргументы для ccall() следующие:
- Пара (:function, “library”) (должна быть константой, но см. ниже).
- Тип возвращаемого значения (см. ниже для отображения объявленного типа C в Julia)
- Этот аргумент будет вычислен во время компиляции.
- Кортеж типов входных данных. Типы входных данных должны быть записаны в виде литерального кортежа, а не в виде кортежа-переменной или выражения.
- Этот аргумент будет вычислен во время компиляции.
- Дополнительные аргументы (если есть) — фактические значения аргументов, передаваемые функции.
В качестве простого, но исчерпывающего примера, следующий код вызывает функцию clock из стандартной библиотеки C:
julia> t = ccall( (:clock, "libc"), Int32, ()) 2292761 julia> t 2292761 julia> typeof(ans) Int32
clock не принимает аргументов и возвращает Int32. Распространённая ошибка заключается в том, что кортеж из одного элемента должен быть записан с последующей запятой. Например, чтобы вызвать функцию getenv для получения указателя на значение переменной окружения, необходимо выполнить вызов так:
julia> path = ccall((:getenv, "libc"), Cstring, (Cstring,), "SHELL") Cstring(@0x00007fff5fbffc45) julia> unsafe_string(path) "/bin/bash"
Обратите внимание, что кортеж типов аргументов должен быть записан как (Cstring,), а не как (Cstring). Это потому, что (Cstring) — это просто выражение Cstring в скобках, а не кортеж из одного элемента, содержащий Cstring.
julia> (Cstring) Cstring julia> (Cstring,) (Cstring,)
На практике, особенно при предоставлении многократно используемых функций, обычно обрамляют ccall() в функциях Julia, которые настраивают аргументы и затем проверяют ошибки, которые функция C или Fortran указывает, распространяя их в качестве исключений вызывающему коду Julia. Это особенно важно, так как API C и Fortran известны своей несогласованностью в отношении способов указания условий ошибки. Например, функция C getenv обрамлена в следующую функцию Julia, которая является упрощённой версией фактического определения из env.jl:
function getenv(var::AbstractString)
val = ccall((:getenv, "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 = Array{UInt8}(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
- Тип возвращаемого значения
- Кортеж типов входных данных
Классическим примером является стандартная функция библиотеки C qsort, объявленная как:
void qsort(void *base, size_t nmemb, size_t size,
int(*compare)(const void *a, const void *b));
Аргумент base — указатель на массив длиной nmemb, с элементами по size байта каждый. compare — функция обратного вызова, которая принимает указатели на два элемента a и b и возвращает целое число меньше/больше нуля, если a должен предшествовать/следовать за b (или ноль, если любой порядок разрешён). Теперь предположим, что у нас есть одномерный массив A значений в Julia, который мы хотим отсортировать с помощью функции qsort (а не встроенной функции Julia sort). Прежде чем беспокоиться о вызове qsort и передаче аргументов, нам нужно написать функцию сравнения, которая работает для произвольного типа T:
function mycompare{T}(a::T, b::T)
return convert(Cint, a < b ? -1 : a > b ? +1 : 0)::Cint
end
Обратите внимание, что нам нужно быть внимательными к типу возвращаемого значения: qsort ожидает функцию, возвращающую C int, поэтому мы должны убедиться, что возвращаем Cint через вызов convert и typeassert.
Для передачи этой функции в C, мы получаем её адрес, используя функцию cfunction:
const mycompare_c = cfunction(mycompare, Cint, (Ref{Cdouble}, Ref{Cdouble}))
cfunction() принимает три аргумента: функцию Julia (mycompare), тип возвращаемого значения (Cint) и кортеж типов аргументов, в данном случае для сортировки массива из Cdouble (Float64) элементов.
Окончательный вызов qsort выглядит следующим образом:
A = [1.3, -2.7, 4.4, 3.1]
ccall(:qsort, Void, (Ptr{Cdouble}, Csize_t, Csize_t, Ptr{Void}),
A, length(A), sizeof(eltype(A)), mycompare_c)
После выполнения этого, 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 автоматически вставляет вызовы функции cconvert() для преобразования каждого аргумента к указанному типу. Например, следующий вызов:
ccall((:foo, "libfoo"), Void, (Int32, Float64), x, y)
будет работать так, как если бы было написано следующее:
ccall((:foo, "libfoo"), Void, (Int32, Float64),
Base.unsafe_convert(Int32, Base.cconvert(Int32, x)),
Base.unsafe_convert(Float64, Base.cconvert(Float64, y)))
cconvert() обычно просто вызывает convert(), но может быть определена так, чтобы возвращать произвольный новый объект, более подходящий для передачи в C. Например, это используется для преобразования Array объектов (например, строк) в массив указателей.
unsafe_convert() обрабатывает преобразование в типы Ptr. Он считается небезопасным, потому что преобразование объекта в указатель нативный может скрыть объект от сборщика мусора, что приведёт к его преждевременному освобождению.
Соответствия типов:
Сначала рассмотрим некоторые относящиеся к этому термины Джулии:
| Синтаксис/Ключевое слово | Пример | Описание |
|---|---|---|
type | String | «Листовой тип» :: Группа связанных данных, включающая тег типа, управляемая сборщиком мусора Джулии и определяемая тождеством объекта. Параметры типа листового типа должны быть полностью определены (не допускаются TypeVars), чтобы экземпляр был создан. |
abstract |
Any, AbstractArray{T,N}, Complex{T}
| «Надтип» :: Надтип (не листовой тип), который нельзя экземплировать, но который можно использовать для описания группы типов. |
{T} | Vector{Int} |
«Параметр типа» :: Специализация типа (как правило, используется для диспетчеризации или оптимизации хранения). «TypeVar» :: |
bitstype |
Int, Float64
| «Тип битов» :: Тип без полей, но с размером. Он хранится и определяется по значению. |
immutable |
|
«Неизменяемый» :: Тип со всеми полями, определёнными как константы. Он определяется по значению. И может храниться с тегом типа. «Is-Bits» :: Тип |
type ...; end | nothing | «Синглтон» :: Листовой тип или неизменяемый тип без полей. |
(...) или tuple(...)`
| (1,2,3) | «Кортеж» :: Неизменяемая структура данных, подобная анонимному неизменяемому типу или постоянному массиву. Представлена как массив или структура. |
typealias | Не применимо здесь | Псевдонимы типов и другие аналогичные механизмы косвенного обращения к типу разрешаются до базового типа (включая присвоение типа другому имени или получение типа из вызова функции). |
Типы битов:
Существует несколько специальных типов, о которых следует знать, так как ни один другой тип не может быть определён таким же образом:
-
Float32 - Точно соответствует типу
floatв C (или типуREAL*4в Fortran). -
Float64 - Точно соответствует типу
doubleв C (или типуREAL*8в Fortran). -
Complex64 - Точно соответствует типу
complex floatв C (или типуCOMPLEX*8в Fortran). -
Complex128 - Точно соответствует типу
complex doubleв C (или типуCOMPLEX*16в Fortran). -
Signed - Точно соответствует аннотации типа
signedв C (или любому типуINTEGERв Fortran). Любой тип Джулии, который не является подтипомSigned, предполагается беззнаковым. -
Ref{T} - Ведёт себя как
Ptr{T}, который владеет своей памятью. -
Array{T,N} -
Когда массив передаётся в C в качестве аргумента
Ptr{T}, он не преобразуется с помощью reinterpret-cast: Джулия требует, чтобы тип элемента массива соответствовалT, и передаётся адрес первого элемента.Поэтому, если
Arrayсодержит данные в неправильном формате, его необходимо явным образом преобразовать, используя вызов, такой какtrunc(Int32,a).Чтобы передать массив
Aкак указатель другого типа без предварительного преобразования данных (например, чтобы передать массивFloat64в функцию, которая работает с неинтерпретированными байтами), вы можете объявить аргумент какPtr{Void}.Если массив с типом элемента
Ptr{T}передаётся как аргументPtr{Ptr{T}},Base.cconvert()попытается сначала создать завершающийся нулём копию массива, заменив каждый элемент егоcconvert()версией. Это позволяет, например, передать массив указателейargvтипаVector{String}в аргумент типаPtr{Ptr{Cchar}}.
На всех поддерживаемых системах базовые типы значений C/C++ могут быть преобразованы в типы Джулии следующим образом. Каждый тип C также имеет соответствующий тип Джулии с тем же именем, но с префиксом C. Это может помочь в написании переносимого кода (и помнить, что int в C не то же самое, что и Int в Джулии).
Независимая от системы:
| Имя в C | Имя в Fortran | Стандартный псевдоним Джулии | Базовый тип Джулии |
|---|---|---|---|
|
| CHARACTER | Cuchar | UInt8 |
short |
| Cshort | Int16 |
unsigned short | Cushort | UInt16 | |
|
|
| Cint | Int32 |
unsigned int | Cuint | UInt32 | |
long long |
| 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 | Complex64 | Complex{Float32} |
complex double | COMPLEX*16 | Complex128 | Complex{Float64} |
ptrdiff_t | Cptrdiff_t | Int | |
ssize_t | Cssize_t | Int | |
size_t | Csize_t | UInt | |
void | Void | ||
void и [[noreturn]] или _Noreturn
| Union{} | ||
void* | Ptr{Void} | ||
T* (где T представляет соответствующий тип) | Ref{T} | ||
char* (или char[], например, строка) | CHARACTER*N |
Cstring если завершается нулём, или Ptr{UInt8} если нет | |
char** (или *char[]) | Ptr{Ptr{UInt8}} | ||
jl_value_t* (любой тип Джулии) | Any | ||
jl_value_t** (ссылка на тип Джулии) | Ref{Any} | ||
va_arg | Не поддерживается | ||
... (спецификация функции с переменным числом аргументов) |
T... (где T — один из вышеперечисленных типов, функции с переменным числом аргументов различных типов аргументов не поддерживаются) |
Тип Cstring по существу является синонимом для Ptr{UInt8}, за исключением того, что преобразование в Cstring генерирует ошибку, если строка Julia содержит вложенные символы NUL (что приведет к неявной обрезке строки, если C-функция интерпретирует NUL как терминатор). Если вы передаёте char* в C-функцию, которая не предполагает завершение NUL (например, потому что вы передаёте явную длину строки), или если вы уверены, что ваша строка Julia не содержит NUL и хотите пропустить проверку, вы можете использовать Ptr{UInt8} в качестве типа аргумента. Cstring также может использоваться в качестве типа возвращаемого значения ccall(), но в этом случае он явно не добавляет дополнительных проверок и предназначен только для повышения читаемости вызова.
Зависимость от системы:
| Имя в C | Стандартный псевдоним Julia | Тип Julia Base |
|---|---|---|
char | Cchar |
|
long | Clong |
|
unsigned long | Culong |
|
wchar_t | Cwchar_t |
|
Примечание
При вызове Fortran-функции все входные данные должны передаваться по ссылке, поэтому все соответствия типов выше должны содержать дополнительный Ptr{..} или Ref{..} обертки вокруг их спецификации типа.
Предупреждение
Для строковых аргументов (char*) тип Julia должен быть Cstring (если ожидаются данные, завершённые NUL) или 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), но возвращают.
Примечание
Для аргументов wchar_t* тип Julia должен быть Cwstring (если C-функция ожидает строку, завершенную NUL) или Ptr{Cwchar_t} в противном случае. Обратите также внимание, что данные строки UTF-8 в Julia внутренне завершаются NUL, поэтому они могут передаваться в C-функции, ожидающие данных, завершенных NUL, без создания копии (но использование типа Cwstring вызовет ошибку, если сама строка содержит символы NUL).
Примечание
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)
Примечание
C-функция, объявленная как возвращающая Void, вернет значение nothing в Julia.
Соответствия типов структур
Составные типы, также известные как struct в C или TYPE в Fortran90 (или STRUCTURE / RECORD в некоторых вариантах F77), могут быть воспроизведены в Julia путём создания определения type или immutable с такой же структурой полей.
При рекурсивном использовании типы isbits хранятся непосредственно в памяти. Все остальные типы хранятся как указатель на данные. При копировании структуры, используемой по значению внутри другой структуры в C, крайне важно не пытаться вручную скопировать поля, так как это не сохранит правильное выравнивание полей. Вместо этого объявляйте неизменяемый тип isbits и используйте его вместо этого. Анонимные структуры невозможны при преобразовании в Julia.
Упакованные структуры и объявления объединений не поддерживаются в Julia.
Вы можете получить приблизительное соответствие union, если заранее знаете поле с наибольшим размером (включая возможные поля заполнения). При преобразовании полей в Julia объявите поле Julia только этого типа.
Массивы параметров необходимо вручную расширять (либо встраивать, либо в неизменяемом вспомогательном типе). Например:
in C:
struct B {
int A[3];
};
b_a_2 = B.A[2];
in Julia:
immutable B_A
A_1::Cint
A_2::Cint
A_3::Cint
end
type B
A::B_A
end
b_a_2 = B.A.(2)
Массивы неизвестного размера не поддерживаются.
В будущем некоторые из этих ограничений могут быть сняты или уменьшены.
Значения 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:
typealias 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 через неявный вызов cconvert(). Напротив, указатели, возвращаемые вызываемой C-функцией, должны быть объявлены как выходной тип Ptr{T}, отражая, что управляемая памятью указателя управляется только C. Указатели, содержащиеся в C-структурах, должны представляться в качестве полей типа Ptr{T} в соответствующих неизменяемых типах Julia, предназначенных для имитации внутренней структуры соответствующих C-структур.
В коде Julia, оборачивающем вызовы внешних Fortran-функций, все входные аргументы должны быть объявлены как типа Ref{T}, так как Fortran передает все переменные по ссылке. Возвращаемый тип должен быть либо Void для Fortran-подпрограмм, либо T для Fortran-функций, возвращающих тип T.
Сопоставление C-функций с Julia
ccall/cfunction руководство по преобразованию аргументов
Для преобразования списка аргументов C в Julia:
-
T, гдеT— один из примитивных типов:char,int,long,short,float,double,complex,enumили любой из ихtypedefэквивалентов-
T, гдеT— эквивалентный тип Julia Bits (по таблице выше) - если
T— этоenum, тип аргумента должен быть эквивалентенCintилиCuint - значение аргумента будет скопировано (передача по значению)
-
-
struct T(включая typedef для структуры)-
T, гдеT— это тип Julia leaf - значение аргумента будет скопировано (передача по значению)
-
-
void*- зависит от того, как этот параметр используется, сначала преобразуйте его в целевой тип указателя, затем определите эквивалент Julia, используя оставшиеся правила в этом списке
- этот аргумент может быть объявлен как
Ptr{Void}, если это действительно просто неизвестный указатель
-
jl_value_t*Any- значение аргумента должно быть допустимым объектом Julia
- в настоящее время не поддерживается
cfunction()
-
jl_value_t**Ref{Any}- значение аргумента должно быть допустимым объектом Julia (или
C_NULL) - в настоящее время не поддерживается
cfunction()
-
T*-
Ref{T}, гдеT— тип Julia, соответствующийT - значение аргумента будет скопировано, если это тип
isbits, в противном случае значение должно быть допустимым объектом Julia
-
-
(T*)(...)(например, указатель на функцию)-
Ptr{Void}(возможно, вам нужно явно использоватьcfunction(), чтобы создать этот указатель)
-
-
...(например, vararg)-
T..., гдеT— тип Julia
-
-
va_arg- не поддерживается
ccall/cfunction руководство по переводу типа возвращаемого значения
Для перевода C-типа возвращаемого значения в Julia:
-
void-
Void(это вернет единственный экземплярnothing::Void)
-
-
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{Void}, если это действительно просто неизвестный указатель
-
jl_value_t*Any- значение аргумента должно быть допустимым объектом Julia
-
jl_value_t**Ref{Any}- значение аргумента должно быть допустимым объектом Julia (или
C_NULL)
-
T*- Если память уже принадлежит Julia или это тип
isbits, и известно, что она не нулевая:-
Ref{T}, гдеT— тип Julia, соответствующийT - тип возвращаемого значения
Ref{Any}недопустим, он должен бытьAny(соответствующийjl_value_t*) илиPtr{Any}(соответствующийPtr{Any}) - C НЕ ДОЛЖЕН изменять память, возвращаемую через
Ref{T}, еслиT— это типisbits
-
- Если память принадлежит C:
-
Ptr{T}, гдеT— тип Julia, соответствующийT
-
- Если память уже принадлежит Julia или это тип
-
(T*)(...)(например, указатель на функцию)-
Ptr{Void}(возможно, вам нужно явно использоватьcfunction(), чтобы создать этот указатель)
-
Передача указателей для изменения входных данных
Поскольку C не поддерживает несколько возвращаемых значений, часто функции C принимают указатели на данные, которые функция будет изменять. Чтобы сделать это в ccall(), вам необходимо сначала упаковать значение в Ref{T} соответствующего типа. При передаче этого объекта Ref в качестве аргумента Julia автоматически передаст C-указатель на упакованные данные:
width = Ref{Cint}(0)
range = Ref{Cfloat}(0)
ccall(:foo, Void, (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,
(Ptr{Int32}, Ptr{Float64}, Ptr{Int32}, Ptr{Float64}, Ptr{Int32}),
&n, DX, &incx, DY, &incy)
return product
end
Значение префикса & немного отличается от C. В частности, любые изменения в ссылаемых переменных не будут видны в Julia, если тип не является изменяемым (объявленным через type). Однако даже для неизменяемых типов вызывающие функции не будут наносить никакого вреда, пытаясь выполнить такие изменения (то есть запись через переданные указатели). Кроме того, & может быть использован с любым выражением, таким как &0 или &f(x).
Когда скалярное значение передается с & как аргумент типа Ptr{T}, значение сначала преобразуется в тип T.
Примеры обёртки для C
Вот простой пример C-обёртки, возвращающей тип Ptr.
type 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
Void, #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}, но он может быть преобразован с помощью 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 = Array{Cdouble}(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() мог бы всё ещё работать, но сборщик мусора Julia не знал бы, что память, выделенная для result_array, используется внешней функцией C. В результате код может привести к утечке памяти, если result_array никогда не освобождается сборщиком мусора, или если сборщик мусора преждевременно освободит result_array, функция C может вызвать исключение некорректного доступа к памяти.
Безопасность сборки мусора
При передаче данных в ccall(), лучше избегать использования функции pointer(). Вместо этого определите метод преобразования и передайте переменные непосредственно в ccall(). ccall() автоматически позаботится о том, чтобы все его аргументы сохранялись от сборки мусора до момента возврата вызова. Если C API будет хранить ссылку на память, выделенную Julia, после возвращения ccall(), вы должны позаботиться о том, чтобы объект оставался видимым для сборщика мусора. Рекомендуемый способ обработки этого - создание глобальной переменной типа Array{Ref,1} для хранения этих значений до тех пор, пока библиотека C не сообщит вам, что закончила с ними.
Всякий раз, когда вы создаёте указатель на данные Julia, вы должны убедиться, что исходные данные существуют до тех пор, пока вы не закончите работу с указателем. Многие методы в Julia, такие как unsafe_load() и String(), создают копии данных, а не берут на себя владение буфером, что позволяет безопасно освободить (или изменить) исходные данные, не затрагивая Julia. Заметным исключением является unsafe_wrap(), который по соображениям производительности может совместно использовать (или может быть указано, чтобы взять на себя владение) основной буфер.
Сборщик мусора не гарантирует никакого порядка завершения. То есть, если a содержал ссылку на b, и оба a и b должны быть собраны мусором, нет гарантии, что b будет завершён после a. Если надлежащее завершение a зависит от того, что b является корректным, это должно обрабатываться другими способами.
Спецификации функций, не являющихся константами
Спецификация функции (name, library) должна быть константным выражением. Тем не менее, можно использовать вычисленные значения в качестве имён функций, переходя через eval следующим образом:
@eval ccall(($(string("a","b")),"lib"), ...
Это выражение строит имя, используя string, затем подставляет это имя в новое выражение ccall(), которое затем вычисляется. Имейте в виду, что eval работает только на верхнем уровне, поэтому внутри этого выражения локальные переменные недоступны (если их значения не подставлены с $). По этой причине eval обычно используется только для формирования определений верхнего уровня, например, при обёртке библиотек, содержащих много похожих функций.
Если ваше использование более динамично, используйте косвенные вызовы, как описано в следующем разделе.
Косвенные вызовы
Первый аргумент ccall() также может быть выражением, вычисляемым во время выполнения. В этом случае выражение должно вычисляться в Ptr, которое будет использоваться в качестве адреса вызываемой функции. Это поведение возникает, когда первый аргумент ccall() содержит ссылки на неконстанты, такие как локальные переменные, аргументы функций или неконстантные глобальные переменные.
Например, вы можете найти функцию с помощью dlsym, затем кешировать её в глобальной переменной для этой сессии. Например:
macro dlsym(func, lib)
z, zlocal = gensym(string(func)), gensym()
eval(current_module(),:(global $z = C_NULL))
z = esc(z)
quote
let $zlocal::Ptr{Void} = $z::Ptr{Void}
if $zlocal == C_NULL
$zlocal = dlsym($(esc(lib))::Ptr{Void}, $(esc(func)))
global $z = $zlocal
end
$zlocal
end
end
end
mylibvar = dlopen("mylib")
ccall(@dlsym("myfunc", mylibvar), Void, ())
Конвенция вызова
Второй аргумент ccall() может быть необязательным спецификатором конвенции вызова (непосредственно перед типом возврата). Без какого-либо спецификатора используется платформа-знаковая конвенция вызова C. Другие поддерживаемые конвенции: stdcall, cdecl, fastcall, и thiscall. Например (из base/libc.jl) мы видим ту же gethostname ccall(), что и выше, но с правильной подписью для Windows:
hn = Array{UInt8}(256)
err = ccall(:gethostname, stdcall, Int32, (Ptr{UInt8}, UInt32), hn, length(hn))
Для получения дополнительной информации, пожалуйста, см. LLVM Language Reference.
Доступ к глобальным переменным
К глобальным переменным, экспортируемым нативными библиотеками, можно получить доступ по имени, используя функцию 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{Void}, вызвав pointer_from_objref(v).)
Обратная операция (запись данных в Ptr{T}), может быть выполнена с помощью unsafe_store!(ptr, value, [index]). В настоящее время это поддерживается только для типов bitstypes или других не имеющих указателей (isbits) неизменяемых типов.
Любая операция, которая приводит к ошибке, вероятно, в настоящее время не реализована и должна быть опубликована как ошибка, чтобы её можно было исправить.
Если указатель, который нас интересует, представляет собой массив обычных данных (bitstype или неизменяемый), функция unsafe_wrap(Array, ptr,dims,[own]) может быть более полезной. Последний параметр должен быть истинным, если Julia должна «взять на себя владение» основным буфером и вызвать free(ptr) при завершении возвращённого объекта Array. Если параметр own опущен или ложный, вызывающий должен убедиться, что буфер остаётся существующим до завершения всех обращений.
Арифметические операции над типом Ptr в Julia (например, используя +) не ведут себя так же, как арифметические операции с указателями в C. Добавление целого числа к Ptr в Julia всегда перемещает указатель на определённое количество байтов, а не элементов. Таким образом, значения адресов, полученные из арифметических операций с указателями, не зависят от типов элементов указателей.
Безопасность потоков
Некоторые библиотеки C выполняют свои обратные вызовы из другого потока, и поскольку Julia не является потокобезопасной, вам необходимо принять дополнительные меры предосторожности. В частности, вам нужно будет создать двухслойную систему: обратный вызов C должен только планировать (через цикл событий Julia) выполнение вашего «реального» обратного вызова. Для этого создайте объект AsyncCondition и дождитесь его:
cond = Base.AsyncCondition() wait(cond)
Обратный вызов, который вы передаёте в C, должен выполнять только ccall() в :uv_async_send, передавая cb.handle в качестве аргумента, избегая при этом любых выделений памяти или других взаимодействий с окружением Julia.
Обратите внимание, что события могут быть объединены, поэтому несколько вызовов uv_async_send могут привести к одному уведомлению о пробуждении для условия.
Дополнительная информация об обратных вызовах
Для получения более подробной информации о том, как передавать обратные вызовы в библиотеки C, см. этот пост в блоге.
C++
Ограниченная поддержка C++ предоставляется пакетами Cpp, Clang и Cxx.
© 2009–2016 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/release-0.5/manual/calling-c-and-fortran-code/