Вызов кода 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"
На практике, особенно при предоставлении повторно используемой функциональности, обычно оборачиваются @ccall функции 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:
- Функция Julia
- Тип возвращаемого значения функции
- Кортеж типов входных данных, соответствующий сигнатуре функции
Как и в случае с @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
будет вести себя так, как если бы он был написан так:
@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. Например, это используется для преобразования массива объектов (например, строк) в массив указателей.
Base.unsafe_convert обрабатывает преобразование в типы Ptr. Это считается небезопасным, так как преобразование объекта в системный указатель может скрыть объект от сборщика мусора, что приведёт к его преждевременному освобождению.
Соответствия типов
Сначала давайте рассмотрим терминологию типов Julia:
| Синтаксис/Ключевое слово | Пример | Описание |
|---|---|---|
mutable struct |
BitSet |
"Тип листа" :: Группа связанных данных, включающая тег типа, управляемая сборщиком мусора Julia и определяемая тождеством объекта. Параметры типа типа листа должны быть полностью определены (не допускаются TypeVars) для создания экземпляра. |
abstract type |
Any, AbstractArray{T, N}, Complex{T}
|
"Супертип" :: Супертип (не тип листа), который нельзя создать, но который можно использовать для описания группы типов. |
T{A} |
Vector{Int} |
"Параметр типа" :: Специализация типа (обычно используется для диспетчеризации или оптимизации хранения). |
"TypeVar" :: T в объявлении параметра типа называется TypeVar (сокращение от типа переменной). |
||
primitive type |
Int, Float64
|
"Примитивный тип" :: Тип без полей, но с размером. Он хранится и определяется по значению. |
struct |
Pair{Int, Int} |
"Структура" :: Тип со всеми полями, определёнными как константы. Он определяется по значению и может храниться с тегом типа. |
ComplexF64 (isbits) |
"Is-Bits" :: Тип primitive type, или тип struct, где все поля являются другими типами isbits. Он определяется по значению и хранится без тега типа. |
|
struct ...; end |
nothing |
"Единичный элемент" :: Тип листа или структура без полей. |
(...) или tuple(...)
|
(1, 2, 3) |
"Кортеж" :: Неизменяемая структура данных, подобная анонимному типу структуры или постоянному массиву. Представлена либо как массив, либо как структура. |
Типы битов
Следует знать несколько специальных типов, так как никакой другой тип не может быть определен таким же образом:
-
Float32Точно соответствует типу
floatв C (илиREAL*4в Fortran). -
Float64Точно соответствует типу
doubleв C (илиREAL*8в Fortran). -
ComplexF32Точно соответствует типу
complex floatв C (илиCOMPLEX*8в Fortran). -
ComplexF64Точно соответствует типу
complex doubleв C (илиCOMPLEX*16в Fortran). -
SignedТочно соответствует аннотации типа
signedв C (или любому типуINTEGERв Fortran). Любой тип Julia, который не является подтипомSigned, предполагается беззнаковым.
-
Ref{T}Ведёт себя как
Ptr{T}, который может управлять своей памятью через сборщик мусора Julia.
-
Array{T,N}При передаче массива в C как аргумент
Ptr{T}, не происходит переинтерпретация: 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 name | Fortran name | Standard Julia Alias | Julia Base Type |
|---|---|---|---|
unsigned char |
CHARACTER |
Cuchar |
UInt8 |
bool (_Bool in C99+) |
Cuchar |
UInt8 |
|
short |
INTEGER*2, LOGICAL*2
|
Cshort |
Int16 |
unsigned short |
Cushort |
UInt16 |
|
int, BOOL (C, typical) |
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 name | Standard Julia Alias | Julia Base Type |
|---|---|---|
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 Bits (согласно таблице выше) - если
Tявляетсяenum, тип аргумента должен быть эквивалентенCintилиCuint - значение аргумента будет скопировано (передаётся по значению)
-
-
struct T(включая typedef структуры)-
T, гдеT— тип Julia leaf - значение аргумента будет скопировано (передаётся по значению)
-
-
void*- зависит от того, как используется этот параметр; сначала переведите его в целевой тип указателя, затем определите эквивалент Julia с помощью оставшихся правил в этом списке
- этот аргумент может быть объявлен как
Ptr{Cvoid}, если он действительно просто неизвестный указатель
-
jl_value_t*Any- значение аргумента должно быть допустимым объектом Julia
-
jl_value_t* const*Ref{Any}- список аргументов должен быть допустимым объектом Julia (или
C_NULL) - не может использоваться для параметра вывода, если пользователь не может отдельно организовать сохранение объекта GC
-
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 (т. е. отдельные регулярные аргументы от varargs с;) - в настоящее время не поддерживается
@cfunction
- [для
-
va_arg- не поддерживается
ccallили@cfunction
- не поддерживается
@ccall / @cfunction руководство по трансляции типов возвращаемых значений
Для перевода типа возвращаемого значения C в Julia:
-
void-
Cvoid(это вернёт единственный экземплярnothing::Cvoid
-
-
T, гдеT— один из примитивных типов:char,int,long,short,float,double,complex,enumили любой из ихtypedefэквивалентов-
T, гдеT— эквивалентный тип Julia Bits (согласно таблице выше) - если
Tявляетсяenum, тип аргумента должен быть эквивалентенCintилиCuint - значение аргумента будет скопировано (возвращается по значению)
-
-
struct T(включая typedef структуры)-
T, гдеT— тип Julia Leaf - значение аргумента будет скопировано (возвращается по значению)
-
-
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 автоматически организует сохранение всех его аргументов от сборки мусора до возврата вызова. Если C-API сохранит ссылку на память, выделенную Julia, после того, как @ccall вернётся, необходимо убедиться, что объект остаётся видимым для сборщика мусора. Рекомендованный способ сделать это — создать глобальную переменную типа Array{Ref,1} для хранения этих значений до тех пор, пока C-библиотека не сообщит вам, что она закончила с ними.
Всякий раз, когда вы создаёте указатель на данные 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, которое будет использоваться как адрес вызываемой функции.
Например, вы можете найти функцию с помощью 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
Замыкания cfunctions
Первый аргумент @cfunction может быть помечен $, в этом случае возвращаемое значение вместо этого будет struct CFunction, который замыкает аргумент. Необходимо гарантировать, что этот возвращаемый объект остаётся активным до тех пор, пока все его использования не завершены. Содержимое и код в указателе cfunction будут удалены с помощью finalizer, когда эта ссылка будет удалена, и atexit. Обычно это не нужно, так как такая функциональность отсутствует в C, но может быть полезна для работы с плохо спроектированными API, которые не предоставляют отдельного параметра среды замыкания.
function qsort(a::Vector{T}, cmp) where T
isbits(T) || throw(ArgumentError("this method can only qsort isbits arrays"))
callback = @cfunction $cmp Cint (Ref{T}, Ref{T})
# Here, `callback` isa Base.CFunction, which will be converted to Ptr{Cvoid}
# (and protected against finalization) by the ccall
@ccall qsort(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:
-
Пара
(:function, "library")(наиболее распространённый вариант),ИЛИ
символьное имя
:functionили строковое имя"function"(для символов в текущем процессе или libc),ИЛИ
указатель на функцию (например, из
dlsym). Тип возвращаемого значения функции
Кортеж типов входных данных, соответствующий сигнатуре функции. Распространённая ошибка – забывать, что кортеж из 1 типа аргумента должен записываться с запятой.
Фактические значения аргументов, передаваемые в функцию, если таковые имеются; каждое значение является отдельным параметром.
Пара (: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). Например (из 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 (a 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) может быть более полезной. Последний параметр должен быть истинным, если Julia должна "взять на себя ответственность" за базовый буфер и вызвать free(ptr) при завершении работы возвращенного Array объекта. Если параметр own опущен или ложный, вызывающий должен убедиться, что буфер сохраняется до тех пор, пока все обращения не будут завершены.
Арифметические операции над типом Ptr в Julia (например, с использованием +) не ведут себя так же, как арифметика указателей в C. Добавление целого числа к Ptr в Julia всегда перемещает указатель на некоторое количество байтов, а не элементов. Таким образом, значения адресов, полученные из арифметики указателей, не зависят от типов элементов указателей.
Безопасность потоков
Некоторые библиотеки C выполняют свои обратные вызовы из другого потока, и поскольку Julia не потокобезопасна, вам нужно будет принять дополнительные меры предосторожности. В частности, вам нужно будет создать двухслойную систему: обратный вызов C должен только планировать (через цикл событий Julia) выполнение вашего "действительного" обратного вызова. Для этого создайте объект AsyncCondition и wait на нём:
cond = Base.AsyncCondition() wait(cond)
Обратный вызов, который вы передаете в C, должен только выполнять ccall к :uv_async_send, передавая cond.handle в качестве аргумента, принимая меры для предотвращения выделения памяти или других взаимодействий с временем выполнения Julia.
Обратите внимание, что события могут быть объединены, поэтому несколько вызовов uv_async_send могут привести к одному уведомлению об активации условия.
Подробнее об обратных вызовах
Дополнительную информацию о том, как передавать обратные вызовы в библиотеки C, см. в этой статье блога.
C++
Для инструментов создания C++ связей см. пакет 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.10/manual/calling-c-and-fortran-code/