Spec-Zone.ru › Julia 0.6

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

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

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

Динамические библиотеки и функции ссылаются на кортеж вида (:function, "library") или ("function", "library"), где function — имя экспортированной функции C. library — имя динамической библиотеки: доступные в пути загрузки (специфичные для платформы) динамические библиотеки будут разрешаться по имени, а при необходимости может быть указан прямой путь.

Имя функции может быть использовано самостоятельно вместо кортежа (просто :function или "function"). В этом случае имя разрешается в текущем процессе. Этот формат можно использовать для вызова функций библиотеки C, функций среды выполнения Julia или функций приложения, связанного с Julia.

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

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

  1. Пара (:function, "library"), которая должна быть написана как литеральная константа,

    ИЛИ

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

  2. Тип возвращаемого значения (см. ниже для сопоставления объявленного типа C с типом Julia)

    • Этот аргумент будет вычислен во время компиляции, когда определяется содержащий метод.

  3. Кортеж типов входных данных. Типы входных данных должны быть написаны как литеральный кортеж, а не кортеж-переменная или выражение.

    • Этот аргумент будет вычислен во время компиляции, когда определяется содержащий метод.

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

В качестве полного, но простого примера, следующий вызов функции 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,)

На практике, особенно при предоставлении многократно используемой функциональности, обычно используется функция Julia, которая подготавливает аргументы и затем проверяет наличие ошибок в функции C или Fortran, распространяя их вызывающей стороне Julia в виде исключений. Это особенно важно, поскольку API C и Fortran известны своей несовместимостью в отношении того, как они указывают условия возникновения ошибок. Например, функция C getenv C обернута в следующую функцию 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}(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 проверяет наличие нулевых байтов и, таким образом, может вызвать ошибку преобразования.

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

Возможна передача функций Julia в функции нативного C, которые принимают аргументы-указатели на функции. Например, чтобы соответствовать прототипам C вида:

typedef returntype (*functiontype)(argumenttype,...)

Функция cfunction() генерирует указатель на функцию, совместимый с C, для вызова функции библиотеки Julia. Аргументы для cfunction() следующие:

  1. Функция Julia

  2. Тип возвращаемого значения

  3. Кортеж типов входных данных

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

void qsort(void *base, size_t nmemb, size_t size,
           int(*compare)(const void *a, const void *b));

Аргумент base — это указатель на массив длиной nmemb, с элементами по size байт каждый. compare — это функция обратного вызова, которая принимает указатели на два элемента a и b и возвращает целое число, меньшее/большее нуля, если a должен предшествовать/следовать b (или ноль, если любой порядок разрешен). Теперь, предположим, что у нас есть одномерный массив A значений в Julia, который мы хотим отсортировать, используя функцию qsort (а не встроенную функцию Julia sort). Прежде чем беспокоиться о вызове qsort и передаче аргументов, нам нужно написать функцию сравнения, которая работает для произвольного типа T:

julia> function mycompare(a::T, b::T) where T
           return convert(Cint, a < b ? -1 : a > b ? +1 : 0)::Cint
       end
mycompare (generic function with 1 method)

Обратите внимание, что мы должны быть осторожны с типом возвращаемого значения: qsort ожидает функцию, возвращающую тип C int, поэтому мы должны убедиться, что возвращаем Cint с помощью вызова convert и typeassert.

Для передачи этой функции в C мы получаем ее адрес, используя функцию cfunction:

julia> const 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, Void, (Ptr{Cdouble}, Csize_t, Csize_t, Ptr{Void}),
             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"), Void, (Int32, Float64), x, y)

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

ccall((:foo, "libfoo"), Void, (Int32, Float64),
      Base.unsafe_convert(Int32, Base.cconvert(Int32, x)),
      Base.unsafe_convert(Float64, Base.cconvert(Float64, y)))

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

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

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

Прежде всего, обзор некоторых релевантных терминов типов Julia:

Синтаксис/Ключевое слово Пример Описание
mutable struct String «Тип листа» :: Группа связанных данных, включающая тег типа, управляемая сборщиком мусора 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} «Структура» :: Тип со всеми полями, определёнными как константы. Он определяется по значению и может храниться с тегом типа.
Complex128 (isbits) «Биты» :: Тип, в котором все поля являются другими типами isbits. Он определяется по значению и хранится без тега типа.
struct ...; end nothing «Единичный элемент» :: Тип листа или структура без полей.
(...) или tuple(...) (1, 2, 3) «Кортеж» :: Неизменяемая структура данных, похожая на анонимный тип структуры или постоянный массив. Представлена как массив или структура.

Типы битов:

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

  • Float32

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

  • Float64

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

  • Complex64

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

  • Complex128

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

  • Signed

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

  • Ref{T}

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

  • Array{T,N}

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

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

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

    Если массив типа 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
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 Complex64 Complex{Float32}
complex double COMPLEX*16 Complex128 Complex{Float64}
ptrdiff_t Cptrdiff_t Int
ssize_t Cssize_t Int
size_t Csize_t UInt
void Void
void и [[noreturn]] или _Noreturn Union{}
void* Ptr{Void}
T* (где T обозначает соответствующий тип) Ref{T}
char* (или char[], например, строка) CHARACTER*N Cstring если завершена нулём, или Ptr{UInt8} если нет
char** (или *char[]) Ptr{Ptr{UInt8}}
jl_value_t* (любой тип Julia) Any
jl_value_t** (ссылка на тип Julia) Ref{Any}
va_arg Не поддерживается
... (спецификация функции с переменным числом аргументов) T... (где T — один из вышеперечисленных типов; функции с переменным числом аргументов разных типов не поддерживаются)

Тип Cstring по существу является синонимом Ptr{UInt8}, за исключением того, что преобразование в Cstring вызывает ошибку, если строка Julia содержит вставленные нули (что привело бы к неявной обрезке строки, если процедура C обрабатывает NUL как терминатор). Если вы передаёте строку Julia процедуре C, которая не предполагает завершение нулём (например, потому что вы передаёте явную длину строки), или если вы уверены, что ваша строка Julia не содержит нулей и хотите пропустить проверку, вы можете использовать Ptr{UInt8} в качестве типа аргумента. Cstring также может использоваться в качестве типа возвращаемого значения ccall, но в этом случае он, очевидно, не добавляет никаких дополнительных проверок и предназначен только для повышения читабельности вызова.

Зависимые от системы:

Имя в C Стандартное псевдоним Julia Базовый тип Julia
char Cchar Int8 (x86, x86_64), UInt8 (powerpc, arm)
long Clong Int (UNIX), Int32 (Windows)
unsigned long Culong UInt (UNIX), UInt32 (Windows)
wchar_t Cwchar_t Int32 (UNIX), UInt16 (Windows)
Примечание

При вызове функции Fortran все входные данные должны передаваться по ссылке, поэтому все соответствия типов выше должны содержать дополнительную обёртку Ptr{..} или Ref{..} вокруг спецификации типа.

Предупреждение

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

Предупреждение

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

Предупреждение

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

Примечание

Для аргументов 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)
Примечание

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

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

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

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

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

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

Массивы параметров могут быть выражены с помощью NTuple:

in C:
struct B {
    int A[3];
};
b_a_2 = B.A[2];

in 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{Void}
len = unsafe_load(Ptr{Cint}(str))
unsafe_string(str + Core.sizeof(Cint), len)

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

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

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

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

Сопоставление C-функций с Julia

ccall/cfunction руководство по преобразованию аргументов

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

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

    • T, где T — эквивалентный тип бит Julia (согласно таблице выше)

    • если T — это enum, тип аргумента должен быть эквивалентен Cint или Cuint

    • значение аргумента будет скопировано (передача по значению)

  • struct T (включая typedef для структуры)

    • T, где T — это лиственный тип Julia

    • значение аргумента будет скопировано (передача по значению)

  • void*

    • зависит от того, как используется этот параметр, сначала переведите это в соответствующий тип указателя, затем определите эквивалент Julia, используя оставшиеся правила в этом списке

    • этот аргумент может быть объявлен как Ptr{Void}, если это действительно просто неизвестный указатель

  • jl_value_t*

    • Any

    • значение аргумента должно быть допустимым объектом Julia

    • в настоящее время не поддерживается cfunction()

  • jl_value_t**

    • Ref{Any}

    • значение аргумента должно быть допустимым объектом Julia (или C_NULL )

    • в настоящее время не поддерживается cfunction()

  • T*

    • Ref{T}, где T — тип Julia, соответствующий T

    • значение аргумента будет скопировано, если это тип isbits, в противном случае значение должно быть допустимым объектом Julia

  • (T*)(...) (например, указатель на функцию)

    • Ptr{Void} (может потребоваться явное использование cfunction() для создания этого указателя)

  • ... (например, vararg)

    • T..., где T — тип Julia

  • va_arg

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

ccall/cfunction руководство по преобразованию возвращаемого типа

Для перевода возвращаемого типа C в Julia:

  • void

    • Void (это вернёт единственный экземпляр nothing::Void)

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

    • T, где T — эквивалентный тип Julia Bits (см. таблицу выше)

    • если T — enum, тип аргумента должен быть эквивалентен Cint или Cuint

    • значение аргумента будет скопировано (возвращается по значению)

  • struct T (включая typedef для структуры)

    • T, где T — тип Julia Leaf

    • значение аргумента будет скопировано (возвращается по значению)

  • void*

    • зависит от того, как используется этот параметр. Сначала переведите его в целевой тип указателя, затем определите эквивалент в Julia, используя оставшиеся правила в этом списке

    • Этот аргумент может быть объявлен как Ptr{Void}, если он действительно просто неизвестный указатель

  • jl_value_t*

    • Any

    • значение аргумента должно быть допустимым объектом Julia

  • jl_value_t**

    • Ref{Any}

    • значение аргумента должно быть допустимым объектом Julia (или C_NULL)

  • T*

    • Если память уже принадлежит Julia или является типом isbits, и известно, что она не равна null:

      • Ref{T}, где T — тип Julia, соответствующий T

      • возвращаемый тип Ref{Any} недопустим, он должен быть либо Any (соответствующий jl_value_t*) или Ptr{Any} (соответствующий Ptr{Any})

      • C НЕ ДОЛЖЕН изменять память, возвращаемую через Ref{T}, если T — тип isbits

    • Если память принадлежит C:

      • Ptr{T}, где T — тип Julia, соответствующий T

  • (T*)(...) (например, указатель на функцию)

    • Ptr{Void} (возможно, вам нужно явно использовать cfunction() для создания этого указателя)

Передача указателей для изменения входных данных

Поскольку C не поддерживает множественные значения возврата, часто функции C принимают указатели на данные, которые функция будет изменять. Для достижения этого в ccall нужно сначала упаковать значение в Ref{T} соответствующего типа. При передаче этого объекта Ref как аргумента, Julia автоматически передаст C-указатель на упакованные данные:

width = Ref{Cint}(0)
range = Ref{Cfloat}(0)
ccall(:foo, Void, (Ref{Cint}, Ref{Cfloat}), width, range)

После возврата содержимое width и range можно извлечь (если они были изменены foo). Это делается при помощи width[] и range[]; они ведут себя как одномерные массивы.

Специальный синтаксис ссылок для ccall (устаревший):

Синтаксис & устарел, используйте тип аргумента Ref{T} вместо него.

Префикс & используется для аргумента ccall, чтобы указать, что должен быть передан указатель на скалярный аргумент вместо самого скалярного значения (требуется для всех аргументов функций Fortran, как указано выше). Следующий пример вычисляет скалярное произведение, используя функцию BLAS.

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

Значение префикса & немного отличается от C. В частности, любые изменения в ссылаемых переменных не будут видны в Julia, если тип не является изменяемым (объявлен через type). Однако даже для неизменяемых структур вызов функции не приведёт к ошибкам, если она попытается такие изменения произвести (т. е., записать через переданные указатели). Более того, & может быть использовано с любым выражением, таким как &0 или &f(x).

Когда скалярное значение передаётся с & как аргумент типа Ptr{T}, значение сначала преобразуется в тип T.

Примеры обёрток C

Вот простой пример обёртки C, которая возвращает тип Ptr:

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 соответствующая обёртка Julia просто нуждается в новом объявлении типа gsl_permutation, у которого нет внутренних полей и единственная цель которого — размещение в параметре типа Ptr типа. Тип возвращаемого значения ccall объявлен как Ptr{gsl_permutation}, поскольку память, выделенная и указанная output_ptr управляется C (а не Julia).

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

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

# The corresponding C signature is
#     void gsl_permutation_free (gsl_permutation * p);
function permutation_free(p::Ref{gsl_permutation})
    ccall(
        (:gsl_permutation_free, :libgsl), # name of C function and library
        Void,                             # output type
        (Ref{gsl_permutation},),          # tuple of input types
        p                                 # name of Julia variable to pass in
    )
end

Здесь входной параметр p объявлен как Ref{gsl_permutation}, что означает, что память, на которую указывает p, может управляться Julia или C. Указатель на память, выделенную C, должен быть типа Ptr{gsl_permutation}, но он преобразуется с помощью 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}(nmax - nmin + 1)
    errorcode = ccall(
        (:gsl_sf_bessel_Jn_array, :libgsl), # name of C function and library
        Cint,                               # output type
        (Cint, Cint, Cdouble, Ref{Cdouble}),# tuple of input types
        nmin, nmax, x, result_array         # names of Julia variables to pass in
    )
    if errorcode != 0
        error("GSL error code $errorcode")
    end
    return result_array
end

C-функция, обернутая, возвращает целочисленный код ошибки; результаты фактического вычисления функции Бесселя J заполняют массив Julia result_array. Эта переменная может быть использована только с соответствующим объявлением типа входных данных Ref{Cdouble}, поскольку её память выделяется и управляется Julia, а не C. Неявный вызов Base.cconvert(Ref{Cdouble}, result_array) распаковывает Julia-указатель на структуру данных Julia-массива в форму, понятную C.

Обратите внимание, что для корректной работы этого кода result_array должен быть объявлен как Ref{Cdouble}, а не Ptr{Cdouble}. Память управляется Julia, а сигнатура Ref предупреждает сборщик мусора Julia о необходимости продолжения управления памятью для result_array во время выполнения ccall. Если вместо этого был использован Ptr{Cdouble}, ccall может работать, но сборщик мусора Julia не будет знать, что память, выделенная для result_array используется внешней C-функцией. В результате код может привести к утечке памяти, если result_array никогда не будет освобождена сборщиком мусора или если сборщик мусора преждевременно освободит result_array, C-функция может сгенерировать исключение некорректного доступа к памяти.

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

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

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

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

END_OF_DOCUMENT_MARKER

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

Спецификация функции (name, library) должна быть константным выражением. Однако возможно использовать вычисленные значения в качестве имён функций, используя eval следующим образом:

@eval ccall(($(string("a", "b")), "lib"), ...

Это выражение создаёт имя с помощью string, затем подставляет это имя в новое выражение ccall, которое затем оценивается. Имейте в виду, что eval работает только на верхнем уровне, поэтому внутри этого выражения локальные переменные недоступны (если их значения не подставлены с помощью $). По этой причине eval обычно используется только для формирования определений верхнего уровня, например, при оборачивании библиотек, содержащих много похожих функций.

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

Косвенные вызовы

Первый аргумент к ccall также может быть выражением, оцениваемым во время выполнения. В этом случае выражение должно быть оценено в Ptr, которое будет использоваться в качестве адреса вызываемой функции. Это происходит, когда первый аргумент ccall содержит ссылки на неконстанты, такие как локальные переменные, аргументы функций или неконстантные глобальные переменные.

Например, вы можете найти функцию с помощью dlsym, затем кешировать её в глобальной переменной для этой сессии. Например:

macro dlsym(func, lib)
    z, zlocal = gensym(string(func)), gensym()
    eval(current_module(), :(global $z = C_NULL))
    z = esc(z)
    quote
        let $zlocal::Ptr{Void} = $z::Ptr{Void}
            if $zlocal == C_NULL
                $zlocal = dlsym($(esc(lib))::Ptr{Void}, $(esc(func)))
                global $z = $zlocal
            end
            $zlocal
        end
    end
end

mylibvar = Libdl.dlopen("mylib")
ccall(@dlsym("myfunc", mylibvar), Void, ())

Конвенция вызова

Второй аргумент к ccall может необязательно быть спецификатором конвенции вызова (непосредственно перед типом возвращаемого значения). Без спецификатора используется платформа-стандартная конвенция вызова C. Другие поддерживаемые конвенции: stdcall, cdecl, fastcall, и thiscall. Например (из base/libc.jl) мы видим тот же gethostnameccall, что и выше, но с правильной сигнатурой для Windows:

hn = Vector{UInt8}(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!().

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

Следующие методы описаны как "неопасные", поскольку плохой указатель или объявление типа могут привести к неожиданному завершению работы 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{Void}, вызвав pointer_from_objref(v).)

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

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

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

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

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

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

cond = Base.AsyncCondition()
wait(cond)

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

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

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

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

C++

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

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

Spec-Zone.ru

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