Вызов кода C и Fortran
Хотя большую часть кода можно написать на Julia, существует множество высококачественных и зрелых библиотек для численных вычислений, написанных на C и Fortran. Для простого использования этого существующего кода Julia позволяет легко и эффективно вызывать функции C и Fortran. Julia придерживается философии «без ненужного кода»: функции можно вызывать непосредственно из Julia без какого-либо «склеивающего» кода, генерации кода или компиляции — даже из интерактивного приглашения. Это достигается путем выполнения соответствующего вызова с синтаксисом ccall, который выглядит как обычный вызов функции.
Вызываемый код должен быть доступен как общая библиотека. Большинство библиотек C и Fortran поставляются с уже скомпилированными общими библиотеками, но если вы компилируете код самостоятельно с помощью GCC (или Clang), вам нужно использовать опции -shared и -fPIC. Машинные инструкции, генерируемые JIT Julia, идентичны тому, что будет выполнено при вызове нативного C, поэтому накладные расходы будут такими же, как при вызове функции библиотеки из кода C. (Вызовы функций, не являющихся библиотечными, как в C, так и в Julia, могут быть встроены и, следовательно, могут иметь еще меньшие накладные расходы, чем вызовы функций общей библиотеки. Когда и библиотеки, и исполняемые файлы генерируются LLVM, можно выполнить оптимизацию всего приложения, которая может оптимизировать и через эту границу, но Julia пока не поддерживает это. Однако в будущем это может быть реализовано, что приведёт к ещё большему увеличению производительности.)
На общие библиотеки и функции ссылаются кортежем вида (:function, "library") или ("function", "library"), где function — это имя экспортированной функции C. library относится к имени общей библиотеки: доступные в (зависимой от платформы) пути загрузки общие библиотеки будут разрешаться по имени, а при необходимости может быть указан прямой путь.
Имя функции может быть использовано самостоятельно вместо кортежа (только :function или "function"). В этом случае имя разрешается в текущем процессе. Этот формат можно использовать для вызова функций библиотеки C, функций среды выполнения Julia или функций приложения, связанного с Julia.
По умолчанию компиляторы Fortran генерируют искаженные имена (например, преобразуют имена функций в нижний или верхний регистр, часто добавляя символ нижнего подчеркивания), и поэтому для вызова функции Fortran через ccall необходимо передать искаженный идентификатор, соответствующий правилу, используемому вашим компилятором Fortran. Кроме того, при вызове функции Fortran все входные данные должны передаваться как указатели на выделенные значения в куче или стеке. Это относится не только к массивам и другим изменяемым объектам, которые обычно выделяются в куче, но также и к скалярным значениям, таким как целые числа и числа с плавающей точкой, которые обычно выделяются в стеке и часто передаются в регистрах при использовании соглашений о вызовах C или Julia.
Наконец, вы можете использовать ccall для фактического создания вызова функции библиотеки. Аргументы для ccall следующие:
-
Пара
(:function, "library"), которая должна быть записана как литеральная константа,ИЛИ
символ имени
:functionили строка имени"function", которые разрешаются в текущем процессе,ИЛИ
указатель на функцию (например, из
dlsym). -
Тип возвращаемого значения (см. ниже для сопоставления объявленного типа C с Julia)
- Этот аргумент будет вычисляться на этапе компиляции, когда определён содержащий метод.
-
Кортеж типов входных данных. Типы входных данных должны быть записаны как литеральный кортеж, а не кортеж-переменная или выражение.
- Этот аргумент будет вычисляться на этапе компиляции, когда определён содержащий метод.
Все последующие аргументы, если таковые имеются, являются фактическими значениями аргументов, передаваемыми в функцию.
В качестве простого, но полного примера, следующий вызов функции clock из стандартной библиотеки C:
julia> t = ccall((:clock, "libc"), Int32, ()) 2292761 julia> t 2292761 julia> typeof(ans) Int32
clock не принимает аргументов и возвращает Int32. Распространённая ошибка заключается в том, что 1-кортеж должен быть записан с последующей запятой. Например, для вызова функции getenv для получения указателя на значение переменной среды, делается вызов такого вида:
julia> path = ccall((:getenv, "libc"), Cstring, (Cstring,), "SHELL") Cstring(@0x00007fff5fbffc45) julia> unsafe_string(path) "/bin/bash"
Обратите внимание, что кортеж типов аргументов должен быть записан как (Cstring,), а не (Cstring). Это связано с тем, что (Cstring) — это просто выражение Cstring в скобках, а не 1-кортеж, содержащий Cstring.
julia> (Cstring) Cstring julia> (Cstring,) (Cstring,)
На практике, особенно при предоставлении многоразового функционала, обычно оборачивают ccall вызовы в функции Julia, которые устанавливают аргументы и затем проверяют ошибки, которые обозначают функции C или Fortran, передавая их вызывающей стороне Julia в виде исключений. Это особенно важно, так как API C и Fortran известны своей несогласованностью в том, как они обозначают условия возникновения ошибок. Например, функция C getenv обернута в следующую функцию Julia, являющуюся упрощённой версией фактического определения из env.jl:
function getenv(var::AbstractString)
val = ccall((:getenv, "libc"),
Cstring, (Cstring,), var)
if val == C_NULL
error("getenv: undefined variable: ", var)
end
unsafe_string(val)
end
Функция C getenv указывает на ошибку, возвращая NULL, но другие стандартные функции C указывают на ошибки различными способами, в том числе возвращая -1, 0, 1 и другие специальные значения. Этот обертывающий код генерирует исключение, чётко указывающее на проблему, если вызывающая сторона пытается получить несуществующую переменную среды:
julia> getenv("SHELL")
"/bin/bash"
julia> getenv("FOOBAR")
getenv: undefined variable: FOOBAR
Вот немного более сложный пример, который определяет имя хоста локальной машины:
function gethostname()
hostname = Vector{UInt8}(undef, 128)
ccall((:gethostname, "libc"), Int32,
(Ptr{UInt8}, Csize_t),
hostname, sizeof(hostname))
hostname[end] = 0; # ensure null-termination
return unsafe_string(pointer(hostname))
end
Этот пример сначала выделяет массив байтов, затем вызывает функцию библиотеки C gethostname для заполнения массива именем хоста, принимает указатель на буфер с именем хоста и преобразует указатель в строку Julia, предполагая, что это строка C с завершающим нулём. Для библиотек C распространено использование этого шаблона, требующего, чтобы вызывающая сторона выделила память, которая передаётся вызываемой стороне и заполняется. Выделение памяти из Julia в таком случае обычно достигается созданием неинициализированного массива и передачей указателя на его данные функции C. Вот почему мы не используем тип Cstring здесь: поскольку массив неинициализирован, он может содержать нулевые байты. Преобразование в Cstring как часть ccall проверит наличие содержащихся нулевых байтов и, таким образом, может выбросить ошибку преобразования.
Создание совместимых с C указателей на функции Julia
Возможна передача функций Julia в нативные функции C, которые принимают аргументы-указатели на функции. Например, для соответствия прототипам C в формате:
typedef returntype (*functiontype)(argumenttype, ...)
Макрос @cfunction генерирует совместимый с C указатель на функцию для вызова функции Julia. Аргументы для @cfunction следующие:
- Функция Julia
- Тип возвращаемого значения
- Литеральный кортеж типов входных данных
Как и в случае с ccall, все эти аргументы будут вычисляться на этапе компиляции, когда определён содержащий метод.
В настоящее время поддерживается только платформа-стандартная конвенция вызовов C. Это означает, что указатели, сгенерированные @cfunction, не могут быть использованы в вызовах, где WINAPI ожидает stdcall функцию в 32-битной Windows, но могут быть использованы в WIN64 (где stdcall объединены с конвенцией вызова C).
Классическим примером является стандартная функция библиотеки C qsort, объявленная как:
void qsort(void *base, size_t nmemb, size_t size,
int (*compare)(const void*, const void*));
Аргумент base является указателем на массив длины nmemb, с элементами по 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
mycompare (generic function with 1 method)
Обратите внимание, что мы должны быть внимательны к типу возвращаемого значения: 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]
4-element Array{Float64,1}:
1.3
-2.7
4.4
3.1
julia> ccall(:qsort, Cvoid, (Ptr{Cdouble}, Csize_t, Csize_t, Ptr{Cvoid}),
A, length(A), sizeof(eltype(A)), mycompare_c)
julia> A
4-element Array{Float64,1}:
-2.7
1.3
3.1
4.4
Как видно, A изменяется на отсортированный массив [-2.7, 1.3, 3.1, 4.4]. Обратите внимание, что Julia знает, как преобразовать массив в Ptr{Cdouble}, как вычислить размер типа в байтах (идентично оператору C sizeof ) и так далее. Для интереса, попробуйте вставить строку println("mycompare($a, $b)") в mycompare, которая позволит вам увидеть сравнения, которые выполняет qsort (и убедиться, что она действительно вызывает функцию Julia, которую вы ей передали).
Сопоставление типов C с Julia
Крайне важно точно сопоставлять объявленный тип C с его объявлением в Julia. Несоответствия могут привести к тому, что код, правильно работающий на одной системе, будет работать неправильно или давать неопределённые результаты на другой системе.
Обратите внимание, что ни один заголовок файла C не используется нигде в процессе вызова функций C: вы несёте ответственность за то, чтобы ваши типы Julia и сигнатуры вызовов точно соответствовали типам в заголовке файла C. (Пакет Clang может быть использован для автоматического генерации кода Julia из заголовка файла C.)
Автоматическое преобразование:
Julia автоматически вставляет вызовы функции Base.cconvert для преобразования каждого аргумента к указанному типу. Например, следующий вызов:
ccall((:foo, "libfoo"), Cvoid, (Int32, Float64), x, y)
будет вести себя так, как будто было написано следующее:
ccall((:foo, "libfoo"), Cvoid, (Int32, Float64),
Base.unsafe_convert(Int32, Base.cconvert(Int32, x)),
Base.unsafe_convert(Float64, Base.cconvert(Float64, y)))
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 (сокращение от 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). Любой тип Julia, не являющийся подтипомSigned, предполагается беззнаковым.
-
Ref{T}Ведет себя как
Ptr{T}, который может управлять своей памятью через сборщик мусора Julia.
-
Array{T,N}Когда массив передается в C в качестве
Ptr{T}аргумента, он не преобразуется с помощью reinterpret-cast: Julia требует, чтобы тип элементов массива соответствовалT, и передается адрес первого элемента.Поэтому, если
Arrayсодержит данные в неправильном формате, его необходимо явно преобразовать, используя вызов, например,trunc(Int32, a).Чтобы передать массив
Aв качестве указателя другого типа без предварительного преобразования данных (например, чтобы передать массивFloat64в функцию, которая работает с неинтерпретированными байтами), вы можете объявить аргумент какPtr{Cvoid}.Если массив типа eltype
Ptr{T}передается в качествеPtr{Ptr{T}}аргумента,Base.cconvertпопытается сначала создать завершаемый нулем копию массива, заменив каждый элемент его версиейBase.cconvert. Это позволяет, например, передать массив указателейargvтипаVector{String}в аргумент типаPtr{Ptr{Cchar}}.
На всех поддерживаемых системах базовые типы значений C/C++ могут быть преобразованы в типы Julia следующим образом. Каждый тип C также имеет соответствующий тип Julia с тем же именем, дополненным префиксом C. Это может помочь при написании переносимого кода (и помнить, что int в C не то же самое, что Int в Julia).
Независимо от системы:
| Имя C | Имя Fortran | Стандартный псевдоним Julia | Тип Julia Base |
|---|---|---|---|
unsigned char |
CHARACTER |
Cuchar |
UInt8 |
bool (только в C++) |
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} |
||
T* (где T представляет собой соответствующим образом определенный тип) |
Ref{T} |
||
char* (или char[], например, строка) |
CHARACTER*N |
Cstring если завершается нулем, или Ptr{UInt8} если нет |
|
char** (или *char[]) |
Ptr{Ptr{UInt8}} |
||
jl_value_t* (любой тип Julia) |
Any |
||
jl_value_t** (ссылка на тип Julia) |
Ref{Any} |
||
va_arg |
Не поддерживается | ||
... (спецификация функции с переменным числом аргументов) |
T... (где T — один из вышеперечисленных типов, функции с переменным числом аргументов разных типов не поддерживаются) |
Тип Cstring по существу является синонимом Ptr{UInt8}, за исключением того, что преобразование в Cstring вызывает ошибку, если строка Julia содержит какие-либо вложенные символы NULL (что приведет к неявной обрезке строки, если C-функция обрабатывает символ NULL как разделитель). Если вы передаете char* в C-функцию, которая не предполагает завершение символом NULL (например, потому что вы передаете явную длину строки), или если вы уверены, что ваша строка Julia не содержит символов NULL и хотите пропустить проверку, вы можете использовать Ptr{UInt8} в качестве типа аргумента. Cstring также можно использовать в качестве типа возвращаемого значения ccall, но в этом случае он, очевидно, не вводит дополнительных проверок и предназначен только для повышения удобочитаемости вызова.
Зависимо от системы:
| C name | Стандартный псевдоним 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, Int32, (Int32, Ptr{Ptr{UInt8}}), length(argv), argv)
Для Fortran-функций, принимающих строки переменной длины типа character(len=*), длины строк передаются как скрытые аргументы. Тип и позиция этих аргументов в списке зависят от компилятора, где поставщики компиляторов обычно по умолчанию используют Csize_t в качестве типа и добавляют скрытые аргументы в конец списка аргументов. В то время как это поведение зафиксировано для некоторых компиляторов (GNU), другие по желанию разрешают размещать скрытые аргументы непосредственно после символьного аргумента (Intel, PGI). Например, Fortran-подпрограммы вида
subroutine test(str1, str2) character(len=*) :: str1,str2
можно вызвать с помощью следующего кода Julia, где длины добавляются
str1 = "foo"
str2 = "bar"
ccall(:test, Void, (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, его функции могут принимать только типы аргументов со статически известной, фиксированной сигнатурой.
Однако, хотя расположение типа должно быть известно статически для вычисления предполагаемого ABI C, статические параметры функции считаются частью этой статической среды. Статические параметры функции могут использоваться как параметры типа в сигнатуре вызова, если они не влияют на расположение типа. Например, f(x::T) where {T} = ccall(:valid, Ptr{T}, (Ptr{T},), x) допустимо, так как Ptr всегда является примитивным типом размера слова. Но g(x::T) where {T} = ccall(:notvalid, T, (T,), x) недействительно, поскольку расположение типа 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((:dist, "libdist"), m256, (m256, m256), a, b)
end
println(call_dist(a,b))
Машинная платформа должна иметь необходимые регистры SIMD. Например, код выше не будет работать на платформах без поддержки AVX.
Владение памятью
malloc/free
Выделение и освобождение памяти таких объектов должны обрабатываться вызовами соответствующих процедур очистки в используемых библиотеках, как и в любой C-программе. Не пытайтесь освободить объект, полученный из C-библиотеки с помощью Libc.free в Julia, так как это может привести к тому, что функция free будет вызвана через неправильную библиотеку libc и приведёт к сбою Julia. Аналогично недопустимо передавать объект, выделенный в Julia, для освобождения внешней библиотекой.
Когда использовать T, Ptr{T} и Ref{T}
В коде Julia, оборачивающем вызовы внешних C-процедур, обычные (не указатели) данные должны быть объявлены как T в ccall , так как они передаются по значению. Для C-кода, принимающего указатели, Ref{T} следует использовать для типов аргументов входных данных, позволяя использовать указатели на память, управляемую либо Julia, либо C через неявный вызов 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**Ref{Any}- значение аргумента должно быть допустимым объектом Julia (или
C_NULL)
-
T*-
Ref{T}, гдеT— тип Julia, соответствующийT - значение аргумента будет скопировано, если это тип
isbits, в противном случае значение должно быть допустимым объектом Julia
-
-
T (*)(...)(например, указатель на функцию)-
Ptr{Cvoid}(возможно, вам потребуется явно использовать@cfunctionдля создания этого указателя)
-
-
...(например, vararg)-
T..., гдеT— тип Julia - в настоящее время не поддерживается
@cfunction
-
-
va_arg- не поддерживается
ccallили@cfunction
- не поддерживается
ccall / @cfunction руководство по переводу типов возвращаемых значений
Для перевода типа возвращаемого значения C в Julia:
-
void-
Cvoid(это вернёт одиночный экземплярnothing::Cvoid)
-
-
T, гдеT— один из примитивных типов:char,int,long,short,float,double,complex,enumили любой ихtypedefэквивалент-
T, гдеT— эквивалентный тип битов Julia (согласно таблице выше) - если
Tявляетсяenum, тип аргумента должен быть эквивалентенCintилиCuint - значение аргумента будет скопировано (возвращается по значению)
-
-
struct T(включая typedef к структуре)-
T, гдеT— тип листа Julia - значение аргумента будет скопировано (возвращается по значению)
-
-
void*- зависит от того, как используется этот параметр, сначала переведите его в целевой тип указателя, затем определите эквивалент Julia, используя оставшиеся правила в этом списке
- этот аргумент может быть объявлен как
Ptr{Cvoid}, если он действительно просто неизвестный указатель
-
jl_value_t*Any- значение аргумента должно быть допустимым объектом Julia
-
jl_value_t**-
Ptr{Any}(Ref{Any}недействительно как тип возвращаемого значения) - значение аргумента должно быть допустимым объектом Julia (или
C_NULL)
-
-
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}(возможно, вам потребуется явно использовать@cfunctionдля создания этого указателя)
-
Передача указателей для изменения входных данных
Так как C не поддерживает множественные возвращаемые значения, часто функции C принимают указатели на данные, которые функция будет изменять. Для достижения этого в ccall необходимо сначала поместить значение внутри Ref{T} соответствующего типа. При передаче этого Ref объекта в качестве аргумента Julia автоматически передаст C-указатель на заключённые данные:
width = Ref{Cint}(0)
range = Ref{Cfloat}(0)
ccall(:foo, Cvoid, (Ref{Cint}, Ref{Cfloat}), width, range)
По возвращении содержимое width и range можно получить (если они были изменены foo) с помощью width[] и range[]; то есть они ведут себя как одномерные массивы.
Специальный синтаксис ссылки для ccall (устарел):
Синтаксис & устарел, используйте вместо него тип аргумента Ref{T}.
Префикс & используется для аргументов ccall для указания того, что должен передаваться указатель на скалярный аргумент, а не само скалярное значение (требуется для всех аргументов функций Fortran, как отмечалось выше). Следующий пример вычисляет скалярное произведение с использованием функции BLAS.
function compute_dot(DX::Vector{Float64}, DY::Vector{Float64})
@assert length(DX) == length(DY)
n = length(DX)
incx = incy = 1
product = ccall((:ddot_, "libLAPACK"),
Float64,
(Ref{Int32}, Ptr{Float64}, Ref{Int32}, Ptr{Float64}, Ref{Int32}),
n, DX, incx, DY, incy)
return product
end
Значение префикса & не совсем такое же, как в C. В частности, любые изменения в переменных, на которые ссылаются, не будут видны в Julia, если тип не является изменяемым (объявлен с помощью mutable struct). Однако, даже для неизменяемых структур, функция, вызываемая по указателю, может пытаться произвести подобные изменения (то есть запись через переданные указатели). Более того, & может использоваться с любым выражением, например, &0 или &f(x).
Когда скалярное значение передаётся с & в качестве аргумента типа Ptr{T}, значение сначала преобразуется в тип T.
Примеры 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(
(:gsl_permutation_alloc, :libgsl), # name of C function and library
Ptr{gsl_permutation}, # output type
(Csize_t,), # tuple of input types
n # name of Julia variable to pass in
)
if output_ptr == C_NULL # Could not allocate memory
throw(OutOfMemoryError())
end
return output_ptr
end
GNU Scientific Library (здесь предполагается, что доступна через :libgsl) определяет неявный указатель gsl_permutation * как тип возвращаемого значения C-функции gsl_permutation_alloc. Так как пользовательский код никогда не должен заглядывать внутрь структуры gsl_permutation, соответствующая C-обёртка Julia просто нуждается в новом объявлении типа gsl_permutation, у которого нет внутренних полей, и единственная цель которого — быть помещённым в параметр типа Ptr типа. Тип возвращаемого значения ccall объявляется как Ptr{gsl_permutation}, так как память, выделенная и к которой указывает output_ptr, управляется C (а не Julia).
Вход n передаётся по значению, и поэтому сигнатура функции ввода просто объявляется как (Csize_t,) без каких-либо Ref или Ptr . (Если вместо этого обёртка вызывала функцию Fortran, соответствующая сигнатура ввода функции должна была бы быть (Ref{Csize_t},), так как переменные Fortran передаются по указателю.) Кроме того, n может быть любым типом, преобразуемым в целочисленный тип Csize_t; ccall неявно вызывает Base.cconvert(Csize_t, n).
Вот второй пример, обертывающий соответствующий деструктор:
# The corresponding C signature is
# void gsl_permutation_free (gsl_permutation * p);
function permutation_free(p::Ref{gsl_permutation})
ccall(
(:gsl_permutation_free, :libgsl), # name of C function and library
Cvoid, # output type
(Ref{gsl_permutation},), # tuple of input types
p # name of Julia variable to pass in
)
end
Здесь вход p объявляется типа Ref{gsl_permutation}, что означает, что память, на которую указывает p, может управляться Julia или C. Указатель на память, выделенную C, должен быть типа Ptr{gsl_permutation}, но он преобразуется с помощью Base.cconvert и поэтому может использоваться в той же (ковариантной) ситуации входного аргумента для ccall. Указатель на память, выделенную Julia, должен быть типа Ref{gsl_permutation}, чтобы убедиться, что адрес памяти, на который указывает указатель, действителен и что сборщик мусора Julia правильно управляет выделенным куском памяти. Поэтому объявление Ref{gsl_permutation} позволяет использовать указатели, управляемые C или Julia.
Если C-обёртка никогда не ожидает, что пользователь передаст указатели на память, управляемую Julia, то использование p::Ptr{gsl_permutation} для сигнатуры метода обёртки и аналогичным образом в ccall также приемлемо.
Вот третий пример передачи массивов Julia:
# The corresponding C signature is
# int gsl_sf_bessel_Jn_array (int nmin, int nmax, double x,
# double result_array[])
function sf_bessel_Jn_array(nmin::Integer, nmax::Integer, x::Real)
if nmax < nmin
throw(DomainError())
end
result_array = Vector{Cdouble}(undef, nmax - nmin + 1)
errorcode = ccall(
(:gsl_sf_bessel_Jn_array, :libgsl), # name of C function and library
Cint, # output type
(Cint, Cint, Cdouble, Ref{Cdouble}),# tuple of input types
nmin, nmax, x, result_array # names of Julia variables to pass in
)
if errorcode != 0
error("GSL error code $errorcode")
end
return result_array
end
C-функция, которая обертывается, возвращает целочисленный код ошибки; результаты фактического вычисления функции Бесселя J заполняют массив Julia result_array. Эта переменная может использоваться только с соответствующим объявлением типа ввода Ref{Cdouble}, так как её память выделяется и управляется Julia, а не C. Неявный вызов Base.cconvert(Ref{Cdouble}, result_array) распаковывает указатель Julia на структуру данных массива Julia в форму, понятную для C.
Обратите внимание, что для корректной работы этого кода, result_array должно быть объявлено как Ref{Cdouble} а не как Ptr{Cdouble}. Управление памятью осуществляет Julia, и подпись Ref сигнализирует сборщику мусора Julia о необходимости продолжения управления памятью для result_array во время выполнения ccall. Если вместо этого использовалось бы Ptr{Cdouble}, то ccall могло бы работать, но сборщик мусора Julia не знал бы, что память, выделенная для result_array используется внешней функцией C. В результате код может вызвать утечку памяти, если result_array никогда не будет освобождена сборщиком мусора, или если сборщик мусора преждевременно освободит result_array, функция C может сгенерировать исключение неверного доступа к памяти.
Безопасность сборки мусора
При передаче данных в ccall, лучше избегать использования функции pointer. Вместо этого определите метод преобразования и передайте переменные непосредственно в ccall. ccall автоматически гарантирует, что все его аргументы будут сохранены от сборки мусора до возвращения результата. Если API C будет хранить ссылку на память, выделенную Julia, после возвращения ccall, вы должны позаботиться о том, чтобы объект оставался видимым для сборщика мусора. Предлагаемый способ сделать это — создать глобальную переменную типа Array{Ref,1} для хранения этих значений до тех пор, пока библиотека C не сообщит вам, что она закончила с ними.
Всякий раз, когда вы создаете указатель на данные Julia, вы должны гарантировать, что исходные данные существуют до тех пор, пока вы не закончите с использованием указателя. Многие методы в Julia, такие как unsafe_load и String, делают копии данных вместо того, чтобы взять владение буфером, так что безопасно освободить (или изменить) исходные данные без влияния на Julia. Заметное исключение — unsafe_wrap, который по соображениям производительности разделяет (или может быть настроен на взятие владения) базовый буфер.
Сборщик мусора не гарантирует никакого порядка завершения. То есть, если a содержало ссылку на b и a и b должны быть собраны мусором, нет гарантии, что b будет завершен после a. Если надлежащее завершение a зависит от того, что b является корректным, это нужно обрабатывать другими способами.
Спецификации функций, не являющихся константами
Спецификация функции (name, library) должна быть константным выражением. Однако, возможно использовать вычисленные значения в качестве имён функций, используя eval следующим образом:
@eval ccall(($(string("a", "b")), "lib"), ...
Это выражение строит имя, используя string, затем подставляет это имя в новое выражение ccall, которое затем оценивается. Имейте в виду, что eval работает только на верхнем уровне, поэтому в этом выражении локальные переменные недоступны (если их значения не заменены $). По этой причине eval обычно используется только для формирования определений верхнего уровня, например, при оборачивании библиотек, содержащих множество похожих функций. Аналогичный пример можно построить для @cfunction.
Однако это будет очень медленно и вызовет утечку памяти, поэтому вам следует избегать этого и продолжать чтение. В следующем разделе описывается, как использовать косвенные вызовы для эффективного достижения аналогичного эффекта.
Косвенные вызовы
Первый аргумент к ccall также может быть выражением, вычисляемым во время выполнения. В этом случае выражение должно вычисляться в Ptr, который будет использоваться как адрес вызываемой функции нативном коде. Это поведение возникает, когда первый аргумент ccall содержит ссылки на переменные, которые не являются константами, такие как локальные переменные, аргументы функций или неконстантные глобальные переменные.
Например, вы можете найти функцию с помощью dlsym, а затем кешировать её в общей ссылке для этой сессии. Например:
macro dlsym(func, lib)
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("myfunc", mylibvar), Cvoid, ())
Закрытые cфункции
Первый аргумент @cfunction может быть помечен $, в таком случае возвращаемое значение будет struct CFunction, который закрывает аргумент. Вы должны гарантировать, что этот возвращаемый объект жив до тех пор, пока не будут завершены все операции с ним. Содержимое и код в указателе cфункции будут удалены с помощью 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, Cvoid, (Ptr{T}, Csize_t, Csize_t, Ptr{Cvoid}),
a, length(a), Base.elsize(a), callback)
# 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
Закрытие библиотеки
Иногда полезно закрыть (разархивировать) библиотеку, чтобы её можно было перезагрузить. Например, при разработке кода 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 (symbol, library) tuple (remaining arguments are the same).
Libdl.dlclose(lib) # Close the library explicitly.
Обратите внимание, что при использовании ccall с входными кортежами (например, ccall((:my_fcn, "./my_lib.so"), ...)), библиотека открывается неявно и может не закрываться явно.
Вызов соглашения
Второй аргумент 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 Language Reference.
Существует дополнительное специальное соглашение о вызове llvmcall, которое позволяет вставлять вызовы к LLVM-интринсикам напрямую. Это может быть особенно полезно при работе с необычными платформами, такими как GPGPU. Например, для CUDA нам нужно получить индекс потока:
ccall("llvm.nvvm.read.ptx.sreg.tid.x", llvmcall, Int32, ())
Как и при любом ccall, крайне важно получить точную подпись аргументов. Также обратите внимание, что нет слоя совместимости, который гарантирует, что интринсик имеет смысл и работает на текущей платформе, в отличие от эквивалентных функций Julia, доступных с помощью Core.Intrinsics.
Доступ к глобальным переменным
Доступ к глобальным переменным, экспортируемым нативными библиотеками, можно получить по имени, используя функцию cglobal. Аргументы cglobal — это спецификация символа, идентичная той, что используется в ccall, и тип, описывающий значение, хранящееся в переменной:
julia> cglobal((:errno, :libc), Int32)
Ptr{Int32} @0x00007f418d0816b8
Результат — указатель, дающий адрес значения. Значение можно изменить с помощью этого указателя, используя unsafe_load и unsafe_store!.
Доступ к данным через указатель
Следующие методы описаны как «неопасные», потому что неправильный указатель или объявление типа могут привести к внезапному завершению работы Julia.
Учитывая Ptr{T}, содержимое типа T можно скопировать из указанной памяти в объект Julia, используя unsafe_load(ptr, [index]). Аргумент индекса необязателен (по умолчанию 1) и следует соглашению 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) может быть более полезной. Конечный параметр должен быть истинным, если 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++, см. пакет Cxx. Для инструментов создания связей C++, см. пакет CxxWrap.
© 2009–2019 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.1.1/manual/calling-c-and-fortran-code/