Документация Engee

Вызов кода на 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. argtypes соответствует сигнатуре функции C, а argvalues — это фактические значения аргументов, которые должны быть переданы в функцию.

Сведения о сопоставлении типов 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 имеет следующие аргументы.

  1. Функция Julia

  2. Возвращаемый тип функции

  3. Кортеж входных типов, соответствующий сигнатуре функции

Так же как и в случае с @ccall, возвращаемый тип и входные типы должны быть литеральными константами.

В настоящее время поддерживается только соглашение о вызовах C, принятое по умолчанию для платформы. Это означает, что создаваемые макросом @cfunction указатели нельзя использовать в вызовах, где WINAPI ожидает функцию stdcall, в 32-разрядных системах Windows, но можно использовать в WIN64 (где соглашение stdcall унифицировано с соглашением о вызовах C).

Функции обратного вызова, предоставляемые посредством @cfunction, не должны выдавать ошибок, так как в этом случае управление непредвиденным образом возвращается среде выполнения Julia, из-за чего программа может оказаться в неопределенном состоянии.

Классический пример — функция 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.

Синтаксис или ключевое слово Пример Описание

mutable struct

BitSet

Конечный тип ("Leaf Type"): набор связанных данных, который включает в себя метку типа, управляется сборщиком мусора Julia и определяется идентификатором объекта. Для создания экземпляра конечного типа необходимо полностью определить параметры типа (переменные TypeVars недопустимы).

abstract type

Any, AbstractArray{T, N}, Complex{T}

Супертип ("Super Type"): тип, не являющийся конечным, экземпляры которого создавать нельзя, но который можно использовать для описания группы типов.

T{A}

Vector{Int}

Параметр типа ("Type Parameter"): специализация типа (обычно применяется для диспетчеризации или оптимизации хранения).

"TypeVar": элемент T в объявлении параметра типа называется TypeVar (сокращение от type variable — переменная типа).

primitive type

Int, Float64

Примитивный тип ("Primitive Type"): тип, не имеющий полей, но имеющий определенный размер. Хранится и определяется по значению.

struct

Pair{Int, Int}

Структура ("Struct"): тип, все поля которого определены как константы. Определяется по значению и может храниться с меткой типа.

ComplexF64 (isbits)

Битовый тип ("Is-Bits"): примитивный тип (primitive type) или структура (struct), все поля которой также относятся к битовым типам (isbits). Определяется по значению и хранится без метки типа.

struct ...; end

nothing

Одинарный тип ("Singleton"): конечный тип или структура (struct) без полей.

(...) или tuple(...)

(1, 2, 3)

Кортеж ("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

unsigned char

CHARACTER

Cuchar

UInt8

bool (_Bool в C99+)

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

Union{}

void*

Ptr{Cvoid} (или, что то же самое, Ref{Cvoid})

T* (где T представляет соответствующим образом определенный тип)

Ref{T} (безопасное изменение T возможно, только если это битовый тип)

char* (или char[], например строка)

CHARACTER*N

Cstring, если завершается нулем, или Ptr{UInt8} в противном случае

char** (или *char[])

Ptr{Ptr{UInt8}}

jl_value_t* (любой тип Julia)

Any

jl_value_t* const* (ссылка на значение Julia)

Ref{Any} (константа, так как для записи потребовался бы барьер записи, который невозможно добавить корректно)

va_arg

Не поддерживается

... (спецификация функции с переменным числом аргументов)

T... (где T — один из перечисленных выше типов при использовании функции ccall)

... (спецификация функции с переменным числом аргументов)

; va_arg1::T, va_arg2::S, etc. (поддерживается только с макросом @ccall)

Тип 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)

При вызове кода на Фортране все входные данные должны передаваться в виде указателей на значения, размещенные в «куче» или стеке, поэтому спецификации всех перечисленных выше соответствующих типов должны заключаться в дополнительную оболочку Ptr{..} или Ref{..}.

Для строковых аргументов (char*) следует использовать либо тип Julia Cstring, если ожидаются данные с завершающими нулевыми символами, либо тип Ptr{Cchar} или Ptr{UInt8} в противном случае (эти два типа указателей работают одинаково), как описывалось выше. Не используйте тип String. Аналогичным образом, для аргументов-массивов (T[] или T*) следует использовать тип Julia Ptr{T}, а не Vector{T}.

Тип Char в Julia имеет размер 32 бита, что отличается от размера расширенного символа (wchar_t или wint_t) на некоторых других платформах.

Возвращаемый тип Union{} означает, что функция не возвращает управление (например, jl_throw или longjmp). В C++11 этому соответствует атрибут , а в C11 — _Noreturn. Не используйте этот тип для функций, которые не возвращают значение (void), но возвращают управление. Вместо этого используйте Cvoid.

Для аргументов wchar_t* в Julia следует использовать тип Cwstring, если подпрограмма на C ожидает строку, завершающуюся нулевым символом, или Ptr{Cwchar_t} в противном случае. Обратите также внимание, что строковые данные в кодировке UTF-8 в Julia завершаются нулевым символом, поэтому их можно передавать в функции на C, ожидающие данных с нулевым символом в конце, не создавая копию (однако использование типа Cwstring вызовет ошибку, если нулевые символы содержатся в самой строке).

Функции на C, принимающие аргумент типа char**, можно вызывать с использованием типа Ptr{Ptr{UInt8}} в Julia. Например, функцию на C следующего вида:

int main(int argc, char **argv);

можно вызвать с помощью следующего кода Julia:

argv = [ "a.out", "arg1", "arg2" ]
@ccall main(length(argv)::Int32, argv::Ptr{Ptr{UInt8}})::Int32

Для функций на Фортране, принимающих строки переменной длины типа character(len=*), длины строк указываются в виде скрытых аргументов. Тип и положение этих аргументов в списке зависят от компилятора. Как правило, в качестве типа используется Csize_t, а скрытые аргументы добавляются в конец списка аргументов. В одних компиляторах (например, GNU) это жесткое правило, но в других (Intel, PGI) при необходимости можно указывать скрытые аргументы сразу после аргумента символа. Например, подпрограмму на Фортране следующего вида:

subroutine test(str1, str2)
character(len=*) :: str1,str2

можно вызвать с помощью следующего кода Julia, где длины строк добавлены в конце списка:

str1 = "foo"
str2 = "bar"
ccall(:test, Cvoid, (Ptr{UInt8}, Ptr{UInt8}, Csize_t, Csize_t),
                    str1, str2, sizeof(str1), sizeof(str2))

Компиляторы Фортрана могут также добавлять другие скрытые аргументы для указателей и массивов предполагаемой формы (:) и предполагаемого размера (*). Чтобы избежать этого, можно использовать модуль ISO_C_BINDING и включить атрибут bind(c) в определение подпрограммы, что настоятельно рекомендуется для обеспечения совместимости кода. В этом случае скрытых аргументов не будет, но придется пожертвовать некоторыми функциональными возможностями языка (например, строки можно будет передавать только с помощью character(len=1)).

Функция на C, объявленная как возвращающая Cvoid, будет возвращать nothing в Julia.

Соответствие типов структур

Составные типы, такие как 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

Функция замыкания @cfunction использует трамплины LLVM, которые доступны не на всех платформах (например, недоступны в ARM и PowerPC).

Закрытие библиотеки

Иногда бывает полезно закрыть (выгрузить) библиотеку, чтобы ее можно было загрузить заново. Например, при написании кода на 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 имеет следующие аргументы.

  1. Пара (:function, "library") (чаще всего),

    ИЛИ

    символ имени :function либо строка имени "function" (для символов в текущем процессе или библиотеке libc),

    ИЛИ

    указатель функции (например, из dlsym).

  2. Возвращаемый тип функции

  3. Кортеж входных типов, соответствующий сигнатуре функции. В одноэлементном кортеже типов аргументов не забывайте ставить запятую в конце.

  4. Фактические значения аргументов, которые должны быть переданы в функцию, если они есть; каждое значение — отдельный параметр.

Пара (:function, "library"), возвращаемый тип и входные типы должны быть литеральными константами (то есть это не могут быть переменные; однако обратите внимание на раздел Неконстантные спецификации функций).

Остальные параметры вычисляются во время компиляции, если определен содержащий метод.

Таблица преобразований между интерфейсами макросов и функций приведена ниже.

@ccall ccall

@ccall clock()::Int32

ccall(:clock, Int32, ())

@ccall f(a::Cint)::Cint

ccall(:a, Cint, (Cint,), a)

@ccall "mylib".f(a::Cint, b::Cdouble)::Cvoid

ccall:f, "mylib"), Cvoid, (Cint, Cdouble), (a, b

@ccall $fptr.f()::Cvoid

ccall(fptr, f, Cvoid, ())

@ccall printf("%s = %d\n"::Cstring ; "foo"::Cstring, foo::Cint)::Cint

<unavailable>

@ccall printf("%s = %d\n"::Cstring ; "2 + 2"::Cstring, "5"::Cstring)::Cint

ccall(:printf, Cint, (Cstring, Cstring...), "%s = %s\n", "2 + 2", "5")

<unavailable>

ccall(:gethostname, stdcall, Int32, (Ptr{UInt8}, UInt32), hn, length(hn))

Соглашение о вызовах

Вторым аргументом вызова 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!.

Символ errno может отсутствовать в библиотеке libc: это зависит от особенностей реализации компилятора для вашей системы. Обычно к символам стандартной библиотеки следует обращаться только по имени, чтобы компилятор подставил нужный символ. Кроме того, так как символ errno, показанный в этом примере, является специальным в большинстве компиляторов, в вашем случае значение может быть другим. При компиляции аналогичного кода на C в любой системе, поддерживающей многопоточность, скорее всего, будет вызвана другая функция (посредством перегрузки макропрепроцессора), которая даст результат, отличный от представленного здесь значения.

Доступ к данным посредством указателя

Представленные ниже методы являются небезопасными, так как неверный указатель или объявление типа могут привести к внезапному прекращению работы 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.


1. вызовы небиблиотечных функций на C и Julia можно встраивать, так что итоговые издержки могут быть даже меньше, чем при вызове функций из общих библиотек. Причина в том, что затраты на вызов сторонней функции практически такие же, как на вызов функции на нативном языке.
2. с помощью пакета Clang можно автоматически создать код Julia на основе файла заголовков C.