Spec-Zone.ru › Julia 1.3

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

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

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

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

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

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

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

  1. Пара (:function, "library"), (наиболее распространённый вариант),

    ИЛИ

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

    ИЛИ

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

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

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

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

Примечание

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

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

Примечание

См. ниже, как отобразить типы C в типы Julia.

В качестве примера, вызывающего clock функцию из стандартной библиотеки C на большинстве систем Unix-подобных систем:

julia> t = ccall(:clock, Int32, ())
2292761

julia> t
2292761

julia> typeof(ans)
Int32

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

julia> path = ccall(:getenv, Cstring, (Cstring,), "SHELL")
Cstring(@0x00007fff5fbffc45)

julia> unsafe_string(path)
"/bin/bash"

Обратите внимание, что кортеж типов аргументов должен быть написан как (Cstring,), а не как (Cstring). Это потому, что (Cstring) — это просто выражение Cstring в скобках, а не кортеж из одного элемента, содержащий Cstring:

julia> (Cstring)
Cstring

julia> (Cstring,)
(Cstring,)

На практике, особенно при предоставлении многократно используемой функциональности, обычно обертывают ccall в функциях Julia, которые настраивают аргументы и затем проверяют наличие ошибок в том виде, как они указываются функциями C или Fortran, передавая их вызывающей функции Julia в виде исключений. Это особенно важно, так как API C и Fortran отличаются в отношении способов указания условий ошибки. Например, функция getenv библиотеки C обернута в следующую функцию Julia, которая является упрощённой версией фактического определения из env.jl:

function getenv(var::AbstractString)
    val = ccall(:getenv, Cstring, (Cstring,), var)
    if val == C_NULL
        error("getenv: undefined variable: ", var)
    end
    return unsafe_string(val)
end

Функция C getenv указывает на ошибку, возвращая NULL, но другие стандартные функции C обозначают ошибки различными способами, включая возвращение -1, 0, 1 и других специальных значений. Этот обертка вызывает исключение, ясно указывающее на проблему, если вызывающая сторона пытается получить несуществующую переменную среды:

julia> getenv("SHELL")
"/bin/bash"

julia> getenv("FOOBAR")
getenv: undefined variable: FOOBAR

Вот немного более сложный пример, который определяет имя хоста локальной машины. В этом примере предполагается, что код библиотеки сетевых операций находится в общей библиотеке под названием «libc». На практике эта функция обычно является частью стандартной библиотеки C, и поэтому часть «libc» следует опустить, но мы хотим показать здесь использование этого синтаксиса.

function gethostname()
    hostname = Vector{UInt8}(undef, 256) # MAXHOSTNAMELEN
    err = ccall((:gethostname, "libc"), Int32,
                (Ptr{UInt8}, Csize_t),
                hostname, sizeof(hostname))
    Base.systemerror("gethostname", err != 0)
    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 следующие:

  1. Функция Julia
  2. Тип возвращаемого значения функции
  3. Кортеж типов входных данных, соответствующий сигнатуре функции
Примечание

Как и в случае с 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}, вычисление размера типа элемента в байтах и так далее.

Для интереса, попробуйте вставить println("mycompare($a, $b)") строку в mycompare, что позволит вам увидеть сравнения, которые выполняет qsort (и убедиться, что она действительно вызывает функцию Julia, которую вы передали ей).

Сопоставление типов C с Julia

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

Обратите внимание, что никакие заголовочные файлы C не используются нигде в процессе вызова функций C: вы несете ответственность за то, чтобы ваши типы Julia и сигнатуры вызовов точно отражали типы и сигнатуры в заголовочном файле C.[2]

Автоматическое преобразование типов

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. Например, это используется для преобразования Array объектов (например, строк) в массив указателей.

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) "Биты" :: Тип 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}.

    Если массив типа 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 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** (or *char[]) Ptr{Ptr{UInt8}}
jl_value_t* (любой тип Julia) Any
jl_value_t** (ссылка на тип Julia) Ref{Any}
va_arg Не поддерживается
... (спецификация функции с переменным числом аргументов) T... (где T — один из вышеперечисленных типов; функции с переменным числом аргументов разных типов не поддерживаются)

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

Системнозависимые типы

C имя Стандартный псевдоним Julia Тип Julia Base
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 (если ожидаются данные, завершённые символом NULL) или Ptr{Cchar} или Ptr{UInt8} в противном случае (эти два типа указателей имеют одинаковый эффект), как описано выше, а не String. Аналогично, для аргументов массивов (T[] или T*) тип Julia должен снова быть Ptr{T}, а не Vector{T}.

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

Тип Julia Char составляет 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, 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, Ptr{T}, (Ptr{T},), x) допустимо, так как Ptr всегда является примитивным типом размера слова. Но g(x::T) where {T} = ccall(:notvalid, T, (T,), x) недопустимо, так как структура типа T не известна статически.

Значения SIMD

Примечание: Эта функция в настоящее время реализована только на платформах x86-64 и 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 будет вызвана через неправильную библиотеку и приведёт к завершению процесса. Обратное (передача объекта, выделенного в 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**

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

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

      • 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[]; то есть они ведут себя как массивы нулевой размерности.

Примеры оболочек 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 (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::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 и, следовательно,

Теперь, если вы достаточно внимательно посмотрите на этот пример, вы можете заметить, что он неверен, учитывая наше вышеприведенное объяснение предпочтительных типов объявления. Вы видите это? Функция, которую мы вызываем, собирается освободить память. Этот тип операции не может быть применен к объекту Julia (это приведет к сбою или к повреждению памяти). Поэтому предпочтительнее объявить тип p как Ptr{gsl_permutation }, чтобы затруднить пользователю ошибочно передать другой тип объекта, чем тот, который получен с помощью gsl_permutation_alloc.

Если 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. Неявный вызов 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((:ddot_, "libLAPACK"),
                    Float64,
                    (Ref{Int32}, Ptr{Float64}, Ref{Int32}, Ptr{Float64}, Ref{Int32}),
                    n, DX, incx, DY, incy)
    return product
end

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

При передаче данных в 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 является валидным, это должно быть обработано другими способами.

Спецификации функций, не являющихся константами

Спецификация функции (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.

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

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

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

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

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

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

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

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

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

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

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

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

cond = Base.AsyncCondition()
wait(cond)

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

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

Подробнее об обратных вызовах

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

C++

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

[1]

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

[2]

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

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

Spec-Zone.ru

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