Spec-Zone.ru › Julia 1.11

Вызов кода C и Fortran

Хотя большая часть кода может быть написана на Julia, существует множество высококачественных и зрелых библиотек для численных вычислений, написанных на C и Fortran. Для удобного использования этого существующего кода Julia упрощает и эффективно позволяет вызывать функции C и Fortran. Julia придерживается философии «без ненужного кода»: функции могут быть вызваны непосредственно из Julia без какого-либо «соединительного» кода, генерации кода или компиляции — даже из интерактивного приглашения. Это достигается просто путем выполнения соответствующего вызова с помощью макроса @ccall (или менее удобного синтаксиса ccall, см. раздел синтаксиса ccall).

Вызываемый код должен быть доступен в виде динамически подключаемой библиотеки. Большинство библиотек C и Fortran поставляются уже скомпилированными в виде динамически подключаемых библиотек, но если вы компилируете код самостоятельно с помощью GCC (или Clang), вам нужно использовать параметры -shared и -fPIC. Машинные инструкции, сгенерированные JIT-компилятором Julia, такие же, как и в случае с вызовом нативного C, поэтому накладные расходы будут такими же, как при вызове функции библиотеки из кода C. [1]

По умолчанию компиляторы Fortran генерируют искаженные имена (например, преобразуют имена функций в нижний или верхний регистр, часто добавляя символ подчёркивания), поэтому для вызова функции Fortran необходимо передать искаженный идентификатор, соответствующий правилу, используемому вашим компилятором Fortran. Кроме того, при вызове функции Fortran все входные данные должны передаваться как указатели на выделенные значения в куче или стеке. Это относится не только к массивам и другим изменяемым объектам, которые обычно выделяются в куче, но также к скалярным значениям, таким как целые числа и числа с плавающей запятой, которые обычно выделяются в стеке и обычно передаются в регистрах при использовании соглашений о вызовах 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 соответствуют сигнатуре функции C, а argvalue — фактическим значениям аргументов, которые должны быть переданы функции.

См. ниже, как отобразить типы 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"

На практике, особенно при предоставлении многоразового функционала, обычно создаются функции Julia, которые подготавливают аргументы и затем проверяют ошибки в том виде, как это определяет функция C или Fortran. И если возникает ошибка, она выбрасывается как обычное исключение Julia. Это особенно важно, так как API C и Fortran печально известны несогласованностью в том, как они указывают условия ошибок. Например, функция C getenv обернута в следующую функцию 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 # ensure null-termination
    return GC.@preserve hostname unsafe_string(pointer(hostname))
end

В этом примере сначала выделяется массив байтов. Затем вызывается функция библиотеки C gethostname для заполнения массива именем хоста. Наконец, передается указатель на буфер имени хоста и преобразуется указатель в строку Julia, предполагая, что это строка C с завершением нулём.

Для библиотек 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.

Создание указателей на совместимые с C функции Julia

Можно передавать функции 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 и может оставить программу в неопределённом состоянии.

Классический пример — стандартная функция библиотеки C qsort , объявленная как:

void qsort(void *base, size_t nitems, size_t size,
           int (*compare)(const void*, const void*));

Аргумент base — указатель на массив длины nitems, с элементами по size байт каждый. compare — функция обратного вызова, которая принимает указатели на два элемента a и b и возвращает целое число меньше/больше нуля, если a должно стоять перед/после b (или ноль, если любой порядок разрешен).

Теперь предположим, что у нас есть одномерный массив A значений в Julia, которые мы хотим отсортировать с помощью функции qsort (а не встроенной в Julia функции sort). Прежде чем мы рассмотрим вызов qsort и передачу аргументов, нам нужно написать функцию сравнения:

julia> function mycompare(a, b)::Cint
           return (a < b) ? -1 : ((a > b) ? +1 : 0)
       end;

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

будет вести себя так, как будто он был написан следующим образом:

c_x = Base.cconvert(Int32, x)
c_y = Base.cconvert(Float64, y)
GC.@preserve c_x c_y begin
    @ccall "libfoo".foo(
        Base.unsafe_convert(Int32, c_x)::Int32,
        Base.unsafe_convert(Float64, c_y)::Float64
    )::Cvoid
end

Base.cconvert обычно просто вызывает convert, но может быть определён для возвращения произвольного нового объекта, более подходящего для передачи в C. Это необходимо для выполнения всех выделений памяти, к которым будет обращаться код C. Например, это используется для преобразования массива объектов (например, строк) в массив указателей.

Base.unsafe_convert обрабатывает преобразование в типы Ptr. Он считается небезопасным, потому что преобразование объекта в системный указатель может скрыть объект от сборщика мусора, что приведёт к преждевременному его освобождению.

Соответствия типов

Прежде всего, давайте рассмотрим некоторые ключевые термины Джулии для типов:

Синтаксис/Ключевое слово Пример Описание
mutable struct BitSet "Тип листа" :: Группа связанных данных, включающая метку типа, управляемая сборщиком мусора Джулии и определяемая по идентичности объекта. Параметры типа типа листа должны быть полностью определены (не допускаются TypeVars), чтобы экземпляр можно было создать.
abstract type Any, AbstractArray{T, N}, Complex{T} "Родительский тип" :: Родительский тип (не тип листа), который нельзя создать, но можно использовать для описания группы типов.
T{A} Vector{Int} "Параметр типа" :: Специализация типа (обычно используется для диспатчирования или оптимизации хранения).
"TypeVar" :: T в объявлении параметра типа называется TypeVar (аббревиатура от type variable).
primitive type Int, Float64 "Примитивный тип" :: Тип без полей, но с размером. Хранится и определяется по значению.
struct Pair{Int, Int} "Структура" :: Тип со всеми полями, определёнными как константы. Определяется по значению и может храниться с меткой типа.
ComplexF64 (isbits) "Is-Bits" :: Тип primitive type, или тип struct, где все поля являются другими типами isbits. Определяется по значению и хранится без метки типа.
struct ...; end nothing "Синглтон" :: Тип листа или структура без полей.
(...) или tuple(...) (1, 2, 3) "Кортеж" :: Неизменяемая структура данных, аналогичная анонимному типу структуры или постоянному массиву. Представлена как массив или структура.

Типы бит

Существует несколько специальных типов, о которых нужно знать, так как ни один другой тип не может быть определён для аналогичного поведения:

  • Float32

    Точно соответствует типу float в C (или REAL*4 в Fortran).

  • Float64

    Точно соответствует типу double в C (или REAL*8 в Fortran).

  • ComplexF32

    Точно соответствует типу complex float в C (или COMPLEX*8 в Fortran).

  • ComplexF64

    Точно соответствует типу complex double в C (или COMPLEX*16 в Fortran).

  • Signed

    Точно соответствует аннотации типа signed в C (или любому типу INTEGER в Fortran). Любой тип Джулии, который не является подтипом Signed, предполагается беззнаковым.

  • Ref{T}

    Ведёт себя как Ptr{T} , который может управлять своей памятью через сборщик мусора Джулии.

  • Array{T,N}

    Когда массив передаётся в C как аргумент Ptr{T}, он не переинтерпретируется: Джулия требует, чтобы тип элементов массива совпадал с T, и передаётся адрес первого элемента.

    Поэтому, если массив Array содержит данные в неправильном формате, его необходимо явно преобразовать, используя вызов, такой как trunc.(Int32, A).

    Для передачи массива A в качестве указателя другого типа без предварительного преобразования данных (например, для передачи массива Float64 в функцию, которая работает с необработанными байтами), вы можете объявить аргумент как Ptr{Cvoid}.

    Если массив с типом элементов Ptr{T} передаётся как аргумент Ptr{Ptr{T}}, Base.cconvert попытается сначала создать нуль-терминированную копию массива, при этом каждый элемент будет заменён его значением Base.cconvert. Это позволяет, например, передать массив указателей типа argv с типом Vector{String} в аргумент типа Ptr{Ptr{Cchar}}.

На всех поддерживаемых системах основные значения типов C/C++ могут быть преобразованы в типы Джулии следующим образом. Каждый тип C также имеет соответствующий тип Джулии с тем же именем, но с префиксом C. Это может помочь при написании переносимого кода (и помните, что int в C не то же самое, что и Int в Джулии).

Независимые от системы типы

Имя в C Имя в Fortran Стандартный псевдоним 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]] или _Noreturn Union{}
void* Ptr{Cvoid} (или аналогично Ref{Cvoid})
T* (где T представляет соответствующий определенный тип) Ref{T} (T можно безопасно изменять только если T — тип isbits)
char* (или char[], например, строка) CHARACTER*N Cstring если завершается нулём, или Ptr{UInt8} если нет
char** (или *char[]) Ptr{Ptr{UInt8}}
jl_value_t* (любой тип Julia) Any
jl_value_t* const* (ссылка на значение Julia) Ref{Any} (const, так как изменение потребует барьер записи, что не возможно корректно вставить)
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)

При вызове Fortran все входные данные должны передаваться по указателям на значения, выделенные в куче или стеке, поэтому все вышеперечисленные соответствия типов должны содержать дополнительную оболочку 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{} означает, что функция не вернёт значения, т.е. C++11 [[noreturn]] или C11 _Noreturn (например, jl_throw или longjmp). Не используйте это для функций, которые не возвращают значений (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

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

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

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

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

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

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

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

Составные типы, такие как struct в C или TYPE в Fortran90 (или STRUCTURE / RECORD в некоторых вариантах F77), могут быть воспроизведены в Julia, создав определение struct с таким же расположением полей.

При рекурсивном использовании типы isbits хранятся непосредственно. Все остальные типы хранятся как указатель на данные. При копировании структуры, используемой по значению внутри другой структуры в C, необходимо не пытаться вручную скопировать поля, так как это не сохранит правильное выравнивание полей. Вместо этого объявите тип структуры isbits и используйте его. Безымянные структуры в переводе в Julia невозможны.

Упакованные структуры и объявления объединений не поддерживаются Julia.

Вы можете получить приближение union, если вы знаете заранее поле с наибольшим размером (включая, возможно, заполнение). При переводе полей в Julia объявите поле Julia только этого типа.

Массивы параметров могут быть выражены с помощью NTuple. Например, структура в обозначении C:

struct B {
    int A[3];
};

b_a_2 = B.A[2];

может быть записана в Julia как

struct B
    A::NTuple{3, Cint}
end

b_a_2 = B.A[3]  # note the difference in indexing (1-based in Julia, 0-based in C)

Массивы неизвестного размера (структуры переменной длины, совместимые с C99, заданные [] или [0]) не поддерживаются напрямую. Часто лучший способ справиться с ними — работать напрямую с байтовыми смещениями. Например, если библиотека C объявила правильный тип строки и вернула указатель на него:

struct String {
    int strlen;
    char data[];
};

В Julia мы можем получить доступ к частям независимо, чтобы сделать копию этой строки:

str = from_c::Ptr{Cvoid}
len = unsafe_load(Ptr{Cint}(str))
unsafe_string(str + Core.sizeof(Cint), len)

Параметры типа

Аргументы типа для @ccall и @cfunction оцениваются статически, когда определяется метод, содержащий использование. Поэтому они должны иметь вид буквального кортежа, а не переменной, и не могут ссылаться на локальные переменные.

Это может показаться странным ограничением, но помните, что поскольку C не является динамическим языком, как Julia, его функции могут принимать только типы аргументов со статически известной, фиксированной сигнатурой.

Однако, хотя расположение типа должно быть известно статически для вычисления предполагаемого C ABI, статические параметры функции считаются частью этой статической среды. Статические параметры функции могут быть использованы как параметры типа в сигнатуре вызова, если они не влияют на расположение типа. Например, 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, обертывающем вызовы внешних процедур Fortran, все входные аргументы должны быть объявлены типа Ref{T}, поскольку Fortran передает все переменные по указателям на места в памяти. Возвращаемый тип должен быть либо Cvoid для подпрограмм Fortran, либо T для функций Fortran, возвращающих тип T.

Мапирование функций C в Julia

@ccall / @cfunction руководство по трансляции аргументов

Для перевода списка аргументов C в Julia:

  • T, где T — один из примитивных типов: char, int, long, short, float, double, complex, enum или любой из их typedef эквивалентов

    • T, где T — эквивалентный тип битов Julia (согласно таблице выше)
    • если T является enum, тип аргумента должен быть эквивалентен Cint или Cuint
    • значение аргумента будет скопировано (передаётся по значению)
  • struct T (включая typedef структуры)

    • T, где T — это лиственный тип Julia
    • значение аргумента будет скопировано (передаётся по значению)
  • void*

    • зависит от того, как используется этот параметр. Сначала необходимо преобразовать его к предполагаемому типу указателя, а затем определить эквивалент в Julia, используя оставшиеся правила из этого списка
    • этот аргумент может быть объявлен как Ptr{Cvoid}, если он действительно просто неизвестный указатель
  • jl_value_t*

    • Any
    • значение аргумента должно быть допустимым объектом Julia
  • jl_value_t* 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 (то есть отдельные обычные аргументы от vararg с ;)
    • в настоящее время не поддерживается @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

# The corresponding C signature is
#     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 # Could not allocate memory
        throw(OutOfMemoryError())
    end
    return output_ptr
end

GNU Scientific Library (предполагается, что она доступна через :libgsl) определяет неявный указатель gsl_permutation * в качестве типа возвращаемого значения функции C gsl_permutation_alloc. Поскольку пользовательский код никогда не должен заглядывать внутрь структуры gsl_permutation, соответствующая обёртка Julia просто нуждается в новом объявлении типа gsl_permutation, у которого нет внутренних полей, и единственная цель которого — быть размещённым в параметре типа Ptr типа. Тип возвращаемого значения ccall объявлен как Ptr{gsl_permutation}, поскольку память, выделенная и к которой указывает output_ptr , управляется C.

Входной n передаётся по значению, и поэтому входная сигнатура функции просто объявляется как ::Csize_t без необходимости в Ref или Ptr. (Если обёртка вызывала функцию Fortran вместо этого, соответствующая сигнатура входной функции была бы ::Ref{Csize_t}, поскольку переменные Fortran передаются по указателю.) Кроме того, n может быть любым типом, преобразуемым в целое число Csize_t; ccall неявно вызывает Base.cconvert(Csize_t, n).

Вот второй пример, оборачивающий соответствующий деструктор:

# The corresponding C signature is
#     void gsl_permutation_free (gsl_permutation * p);
function permutation_free(p::Ptr{gsl_permutation})
    @ccall "libgsl".gsl_permutation_free(p::Ptr{gsl_permutation})::Cvoid
end

Вот третий пример, передающий массивы Julia:

# The corresponding C signature is
#    int gsl_sf_bessel_Jn_array (int nmin, int nmax, double x,
#                                double result_array[])
function sf_bessel_Jn_array(nmin::Integer, nmax::Integer, x::Real)
    if nmax < nmin
        throw(DomainError())
    end
    result_array = Vector{Cdouble}(undef, nmax - nmin + 1)
    errorcode = @ccall "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.

Пример обёртки Fortran

Следующий пример использует ccall для вызова функции в общей библиотеке Fortran (libBLAS) для вычисления скалярного произведения. Обратите внимание, что сопоставление аргументов здесь немного отличается от вышеприведённого, поскольку нам необходимо перейти от Julia к Fortran. Для каждого типа аргумента мы указываем Ref или Ptr. Эта конвенция именования может быть специфичной для вашего компилятора Fortran и операционной системы и, скорее всего, не документирована. Однако обертывание каждого в Ref (или Ptr, где эквивалентно) является частым требованием реализаций компиляторов Fortran:

function compute_dot(DX::Vector{Float64}, DY::Vector{Float64})
    @assert length(DX) == length(DY)
    n = length(DX)
    incx = incy = 1
    product = @ccall "libLAPACK".ddot(
        n::Ref{Int32}, DX::Ptr{Float64}, incx::Ref{Int32}, DY::Ptr{Float64}, incy::Ref{Int32})::Float64
    return product
end

Безопасность сборки мусора

При передаче данных в @ccall лучше избегать использования функции pointer. Вместо этого определите метод Base.cconvert и передавайте переменные непосредственно в @ccall. @ccall автоматически организует сохранение всех аргументов от сборки мусора до возвращения вызова. Если API C будет хранить ссылку на память, выделенную Julia, после возвращения @ccall, вы должны убедиться, что объект остаётся видимым для сборщика мусора. Предлагаемый способ сделать это — создать глобальную переменную типа Array{Ref,1} для хранения этих значений до тех пор, пока библиотека C не уведомит вас о том, что закончила с ними.

END_OF_DOCUMENT_MARKER

Всякий раз, когда вы создаете указатель на данные Julia, вы должны убедиться, что исходные данные существуют до тех пор, пока вы не закончите использовать указатель. Многие методы в Julia, такие как unsafe_load и String, создают копии данных вместо того, чтобы взять владение буфером, чтобы можно было безопасно освободить (или изменить) исходные данные без влияния на Julia. Заметным исключением является unsafe_wrap, который по соображениям производительности разделяет (или может быть настроен на взятие владения) базовым буфером.

Сборщик мусора не гарантирует никакого порядка финализации. То есть, если a содержал ссылку на b и оба a и b должны быть удалены сборщиком мусора, нет никакой гарантии, что b будет финализирован после a. Если надлежащая финализация a зависит от того, что b является допустимым, это должно быть обработано другими способами.

Спецификации неконстантных функций

В некоторых случаях точное имя или путь к необходимой библиотеке заранее неизвестны и должны быть вычислены во время выполнения. Для обработки таких случаев спецификация компонента библиотеки может быть вызовом функции, например, 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

Закрытие cфункций

Первый аргумент к @cfunction может быть помечен с помощью $, в этом случае возвращаемое значение вместо этого будет struct CFunction, который закрыт по аргументу. Вы должны убедиться, что этот возвращаемый объект хранится до тех пор, пока все его использования не будут закончены. Содержимое и код в указателе cфункции будут удалены с помощью finalizer при снятии этой ссылки и аtexit. Это обычно не требуется, так как эта функциональность отсутствует в C, но может быть полезной для работы с плохо спроектированными API, которые не предоставляют параметр отдельной среды закрытия.

function qsort(a::Vector{T}, cmp) where T
    isbits(T) || throw(ArgumentError("this method can only qsort isbits arrays"))
    callback = @cfunction $cmp Cint (Ref{T}, Ref{T})
    # Here, `callback` isa Base.CFunction, which will be converted to Ptr{Cvoid}
    # (and protected against finalization) by the ccall
    @ccall qsort(a::Ptr{T}, length(a)::Csize_t, Base.elsize(a)::Csize_t, callback::Ptr{Cvoid})
    # We could instead use:
    #    GC.@preserve callback begin
    #        use(Base.unsafe_convert(Ptr{Cvoid}, callback))
    #    end
    # if we needed to use it outside of a `ccall`
    return a
end

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

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

Иногда полезно закрыть (разгрузить) библиотеку, чтобы ее можно было перезагрузить. Например, при разработке кода C для использования с Julia, может потребоваться скомпилировать, вызвать код C из Julia, затем закрыть библиотеку, внести правку, перекомпилировать и загрузить новые изменения. Можно либо перезапустить Julia, либо использовать функции Libdl для явного управления библиотекой, например:

lib = Libdl.dlopen("./my_lib.so") # Open the library explicitly.
sym = Libdl.dlsym(lib, :my_fcn)   # Get a symbol for the function to call.
@ccall $sym(...) # Use the pointer `sym` instead of the library.symbol tuple.
Libdl.dlclose(lib) # Close the library explicitly.

Обратите внимание, что при использовании @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. Кортеж типов входных данных, соответствующий сигнатуре функции. Одной из распространённых ошибок является забывание о том, что кортеж из 1 типа аргумента должен быть написан с запятой.

  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 = %s\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). Например (из base/libc.jl), мы видим те же gethostnameccall как выше, но с правильной сигнатурой для Windows:

hn = Vector{UInt8}(undef, 256)
err = ccall(:gethostname, stdcall, Int32, (Ptr{UInt8}, UInt32), hn, length(hn))

Для получения дополнительной информации см. Справочник по языку LLVM.

Существует одна дополнительная специальная конвенция вызова llvmcall, которая позволяет вставлять вызовы LLVM-интринсиков напрямую. Это может быть особенно полезно при нацеливании на необычные платформы, такие как GPGPU. Например, для CUDA, нам необходимо уметь читать индекс потока:

ccall("llvm.nvvm.read.ptx.sreg.tid.x", llvmcall, Int32, ())

Как и в случае с любым ccall, крайне важно получить точную сигнатуру аргументов. Также обратите внимание, что нет слоя совместимости, который гарантирует, что интринсик имеет смысл и работает на текущей цели, в отличие от эквивалентных функций Julia, предоставляемых Core.Intrinsics.

Доступ к глобальным переменным

Глобальные переменные, экспортируемые родными библиотеками, могут быть обращены по имени с помощью функции cglobal. Аргументы cglobal — это спецификация символа, идентичная той, которая используется в ccall, и тип, описывающий значение, хранящееся в переменной:

julia> cglobal((:errno, :libc), Int32)
Ptr{Int32} @0x00007f418d0816b8

Результат — указатель, предоставляющий адрес значения. Значение можно изменить через этот указатель с помощью unsafe_load и unsafe_store!.

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

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

Следующие методы описаны как «небезопасные», поскольку неправильный указатель или объявление типа могут привести к внезапному завершению работы Julia.

Учитывая Ptr{T}, содержимое типа T в целом можно скопировать из указанной памяти в объект Julia, используя unsafe_load(ptr, [index]). Аргумент индекса необязателен (по умолчанию 1) и следует соглашению Julia о нумерации с 1. Эта функция намеренно похожа на поведение getindex и setindex! (например, синтаксис доступа []).

Возвращаемое значение будет новым объектом, инициализированным копией содержимого указанной памяти. Указанная память может безопасно быть освобождена или высвобождена.

Если T равно Any, то память предполагается, что содержит ссылку на объект Julia (объект jl_value_t*), результат будет ссылкой на этот объект, и объект не будет скопирован. В этом случае необходимо убедиться, что объект всегда виден сборщику мусора (указатели не учитываются, но новая ссылка учитывается), чтобы гарантировать, что память не будет преждевременно освобождена. Обратите внимание, что если объект изначально не был выделен Julia, новый объект никогда не будет завершен сборщиком мусора Julia. Если сам Ptr фактически является jl_value_t*, его можно преобразовать обратно в ссылку на объект Julia с помощью unsafe_pointer_to_objref(ptr). (Значения Julia v можно преобразовать в указатели jl_value_t*, как Ptr{Cvoid}, вызвав pointer_from_objref(v).)

Обратное действие (запись данных в Ptr{T}) можно выполнить с помощью unsafe_store!(ptr, value, [index]). В настоящее время это поддерживается только для примитивных типов или других типов неизменяемых структур без указателей (isbits).

Любая операция, которая вызывает ошибку, вероятно, в настоящее время не реализована и должна быть опубликована как ошибка, чтобы её можно было исправить.

Если указатель, который нас интересует, представляет собой массив обычных данных (примитивный тип или неизменяемая структура), функция unsafe_wrap(Array, ptr,dims, own = false) может быть более полезной. Последний параметр должен быть true, если Julia должна «присвоить права собственности» на базовый буфер и вызвать free(ptr) при завершении возвращённого Array объекта. Если параметр own опущен или имеет значение false, вызывающий метод должен гарантировать, что буфер существует до тех пор, пока все обращения к нему не будут завершены.

Арифметические операции с типом Ptr в Julia (например, с использованием +) не ведут себя так же, как арифметические операции с указателями в C. Добавление целого числа к Ptr в Julia всегда перемещает указатель на определённое количество байтов, а не элементов. Таким образом, значения адресов, полученные с помощью арифметики указателей, не зависят от типов элементов указателей.

Безопасность потоков

Некоторые библиотеки C выполняют свои обратные вызовы из другого потока, и так как Julia не является потокобезопасной, вам понадобятся дополнительные меры предосторожности. В частности, вам нужно будет создать двухслойную систему: обратный вызов C должен только планировать (через цикл событий Julia) выполнение вашего «действительного» обратного вызова. Для этого создайте объект AsyncCondition и вызовите wait на нём:

cond = Base.AsyncCondition()
wait(cond)

Обратный вызов, который вы передаёте в C, должен только выполнять ccall к :uv_async_send, передавая cond.handle в качестве аргумента, избегая при этом выделения памяти или других взаимодействий с рабочим процессом Julia.

Обратите внимание, что события могут быть объединены, поэтому несколько вызовов uv_async_send могут привести к одному уведомлению о пробуждении для условия.

Больше о обратных вызовах

Дополнительную информацию о том, как передавать обратные вызовы в библиотеки C, см. в этой статье блога.

C++

Для создания привязок C++ см. пакет CxxWrap.

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

© 2009–2024 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.11/manual/calling-c-and-fortran-code/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API