Spec-Zone.ru › Julia 1.2

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

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

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

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

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

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

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

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

    ИЛИ

    символ имени :function или строка имени "function", которые разрешаются в текущем процессе,

    ИЛИ

    указатель на функцию (например, из 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,)

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

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

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

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

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

Вот немного более сложный пример, который определяет имя хоста локальной машины:

function gethostname()
    hostname = Vector{UInt8}(undef, 128)
    ccall((:gethostname, "libc"), Int32,
          (Ptr{UInt8}, Csize_t),
          hostname, sizeof(hostname))
    hostname[end] = 0; # ensure null-termination
    return unsafe_string(pointer(hostname))
end

Этот пример сначала выделяет массив байтов, затем вызывает функцию библиотеки C gethostname для заполнения массива именем хоста, принимает указатель на буфер с именем хоста и преобразует указатель в строку Julia, предполагая, что это строка C с завершающим нулём. Для библиотек C распространено использование этого шаблона, требующего, чтобы вызывающая сторона выделила память, которая передаётся вызываемой стороне и заполняется. Выделение памяти из Julia в таком случае обычно достигается созданием неинициализированного массива и передачей указателя на его данные функции C. Вот почему мы не используем тип Cstring здесь: поскольку массив неинициализирован, он может содержать нулевые байты. Преобразование в Cstring как часть ccall проверит наличие содержащихся нулевых байтов и, таким образом, может выбросить ошибку преобразования.

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

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

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

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

  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}, как вычислить размер типа в байтах (идентично оператору C sizeof ) и так далее. Для интереса, попробуйте вставить строку println("mycompare($a, $b)") в mycompare, которая позволит вам увидеть сравнения, которые выполняет qsort (и убедиться, что она действительно вызывает функцию Julia, которую вы ей передали).

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

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

Обратите внимание, что ни один заголовок файла C не используется нигде в процессе вызова функций C: вы несёте ответственность за то, чтобы ваши типы Julia и сигнатуры вызовов точно соответствовали типам в заголовке файла C. (Пакет Clang может быть использован для автоматического генерации кода Julia из заголовка файла C.)

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

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

ccall((:foo, "libfoo"), Cvoid, (Int32, Float64), x, y)

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

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

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

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

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

Сначала рассмотрим некоторые релевантные термины типов Julia:

Синтаксис/Ключевое слово Пример Описание
mutable struct BitSet "Тип-лист" :: Группа связанных данных, включающая метку типа, управляемая GC 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} , который может управлять своей памятью с помощью GC 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 name Fortran name Standard Julia Alias Julia Base Type
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 если завершается символом NULL, или Ptr{UInt8} если нет
char** (или *char[]) Ptr{Ptr{UInt8}}
jl_value_t* (любой тип Julia) Any
jl_value_t** (ссылка на тип Julia) Ref{Any}
va_arg Не поддерживается
... (спецификация функции с переменным числом аргументов) T... (где T - один из вышеперечисленных типов, функции с переменным числом аргументов разных типов не поддерживаются)

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

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

C name Standard Julia Alias Julia Base Type
char Cchar Int8 (x86, x86_64), UInt8 (powerpc, arm)
long Clong Int (UNIX), Int32 (Windows)
unsigned long Culong UInt (UNIX), UInt32 (Windows)
wchar_t Cwchar_t Int32 (UNIX), UInt16 (Windows)
Примечание

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

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

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

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

Тип Char Julia имеет 32 бита, что не соответствует типу широких символов (wchar_t или wint_t) на всех платформах.

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

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

Примечание

Для аргументов wchar_t*, тип Julia должен быть Cwstring (если C-функция ожидает строку, завершенную символом NULL) или Ptr{Cwchar_t} в противном случае. Также обратите внимание, что данные UTF-8 строк в Julia внутренне завершаются символом NULL, поэтому их можно передавать C-функциям, ожидающим данных, завершенных символом NULL, без создания копии (но использование типа Cwstring вызовет ошибку, если сама строка содержит символы NULL).

Примечание

C-функции, которые принимают аргумент типа char**, можно вызывать, используя тип Ptr{Ptr{UInt8}} в Julia. Например, C-функции вида:

int main(int argc, char **argv);

можно вызвать с помощью следующего кода Julia:

argv = [ "a.out", "arg1", "arg2" ]
ccall(:main, Int32, (Int32, Ptr{Ptr{UInt8}}), length(argv), argv)
Примечание

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

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

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

str1 = "foo"
str2 = "bar"
ccall(:test, Void, (Ptr{UInt8}, Ptr{UInt8}, Csize_t, Csize_t),
                    str1, str2, sizeof(str1), sizeof(str2))
Предупреждение

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

Примечание

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

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

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

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

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

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

Массивы параметров могут быть выражены с использованием NTuple.

в C:

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

в Julia:

struct B
    A::NTuple{3, Cint}
end
b_a_2 = B.A[3]  # note the difference in indexing (1-based in Julia, 0-based in C)

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

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

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

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

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

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

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

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

Примечание: Эта функция в настоящее время реализована только на 64-битных платформах x86 и AArch64.

Если у C/C++ процедуры есть аргумент или возвращаемое значение, являющееся родным типом SIMD, соответствующий тип Julia представляет собой однородный кортеж VecElement , естественно отображаемый на тип SIMD. В частности:

  • Кортеж должен иметь тот же размер, что и тип SIMD. Например, кортеж, представляющий __m128 на x86, должен иметь размер 16 байт.
  • Тип элемента кортежа должен быть экземпляром VecElement{T} , где T - примитивный тип, размер которого составляет 1, 2, 4 или 8 байт.

Например, рассмотрим эту C процедуру, использующую AVX инструкции:

#include <immintrin.h>

__m256 dist( __m256 a, __m256 b ) {
    return _mm256_sqrt_ps(_mm256_add_ps(_mm256_mul_ps(a, a),
                                        _mm256_mul_ps(b, b)));
}

Следующий код Julia вызывает dist с использованием ccall.

const m256 = NTuple{8, VecElement{Float32}}

a = m256(ntuple(i -> VecElement(sin(Float32(i))), 8))
b = m256(ntuple(i -> VecElement(cos(Float32(i))), 8))

function call_dist(a::m256, b::m256)
    ccall((:dist, "libdist"), m256, (m256, m256), a, b)
end

println(call_dist(a,b))

Машина-хост должна иметь необходимые регистры SIMD. Например, код выше не будет работать на хостах без поддержки AVX.

Владение памятью

malloc/free

Выделение и освобождение памяти таких объектов должны выполняться с помощью вызовов соответствующих процедур очистки в используемых библиотеках, как и в любом C-программе. Не пытайтесь освободить объект, полученный из C библиотеки с помощью Libc.free в Julia, так как это может привести к вызову функции free через неправильную библиотеку libc и привести к сбою Julia. Обратное (передача объекта, выделенного в Julia, для освобождения внешней библиотекой) также неверно.

Когда использовать T, Ptr{T} и Ref{T}

В коде Julia, обертывающем вызовы внешних C процедур, обычные (неуказательнные) данные должны быть объявлены типа T внутри ccall, так как они передаются по значению. Для C кода, принимающего указатели, Ref{T} обычно используется для типов входных аргументов, что позволяет использовать указатели на память, управляемую либо Julia, либо C через неявный вызов Base.cconvert. Напротив, указатели, возвращаемые вызываемой C функцией, должны быть объявлены типа выходного Ptr{T}, отражая, что управляемая памятью управляется только C. Указатели, содержащиеся в C структурах, должны быть представлены как поля типа Ptr{T} внутри соответствующих типов Julia структур, предназначенных для имитации внутренней структуры соответствующих структур C.

В коде Julia, обертывающем вызовы внешних Fortran процедур, все входные аргументы должны быть объявлены типа Ref{T}, так как Fortran передает все переменные через указатели на места в памяти. Тип возвращаемого значения должен быть либо Cvoid для Fortran подпрограмм, либо T для Fortran функций, возвращающих тип T.

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

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

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

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

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

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

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

    • Any
    • значение аргумента должно быть действительным объектом Julia
  • jl_value_t**

    • Ref{Any}
    • значение аргумента должно быть допустимым объектом Julia (или C_NULL
  • T*

    • Ref{T}, где T - тип Julia, соответствующий T
    • значение аргумента будет скопировано, если это тип isbits, в противном случае значение должно быть допустимым объектом Julia
  • T (*)(...) (например, указатель на функцию)

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

    • T..., где T - тип Julia
    • в настоящее время не поддерживается @cfunction
  • va_arg

    • не поддерживается ccall или @cfunction

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

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

  • void

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

    • T, где T — эквивалентный тип Julia 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[]; они действуют как массивы нулевой размерности.

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

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

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

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

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

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

Некоторые примеры C-обёрток

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

mutable struct gsl_permutation
end

# The corresponding C signature is
#     gsl_permutation * gsl_permutation_alloc (size_t n);
function permutation_alloc(n::Integer)
    output_ptr = ccall(
        (:gsl_permutation_alloc, :libgsl), # name of C function and library
        Ptr{gsl_permutation},              # output type
        (Csize_t,),                        # tuple of input types
        n                                  # name of Julia variable to pass in
    )
    if output_ptr == C_NULL # Could not allocate memory
        throw(OutOfMemoryError())
    end
    return output_ptr
end

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

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

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

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

Здесь вход p объявлен как Ref{gsl_permutation}, что означает, что память, на которую указывает p, может управляться Julia или C. Указатель на память, выделенную C, должен быть типа Ptr{gsl_permutation}, но он преобразуется с помощью Base.cconvert и поэтому может использоваться в том же (ковариантном) контексте входного аргумента ccall. Указатель на память, выделенную Julia, должен быть типа Ref{gsl_permutation}, чтобы гарантировать, что адрес памяти, на который указывает указатель, действителен и что сборщик мусора Julia правильно управляет выделенным куском памяти. Поэтому объявление Ref{gsl_permutation} позволяет использовать указатели, управляемые C или Julia.

Если C-обёртка никогда не ожидает, что пользователь передаст указатели на память, управляемую Julia, то использование p::Ptr{gsl_permutation} для сигнатуры метода обёртки и аналогично в ccall также допустимо.

Вот третий пример передачи массивов Julia:

# The corresponding C signature is
#    int gsl_sf_bessel_Jn_array (int nmin, int nmax, double x,
#                                double result_array[])
function sf_bessel_Jn_array(nmin::Integer, nmax::Integer, x::Real)
    if nmax < nmin
        throw(DomainError())
    end
    result_array = Vector{Cdouble}(undef, nmax - nmin + 1)
    errorcode = ccall(
        (:gsl_sf_bessel_Jn_array, :libgsl), # name of C function and library
        Cint,                               # output type
        (Cint, Cint, Cdouble, Ref{Cdouble}),# tuple of input types
        nmin, nmax, x, result_array         # names of Julia variables to pass in
    )
    if errorcode != 0
        error("GSL error code $errorcode")
    end
    return result_array
end

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

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

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

При передаче данных в ccall лучше избегать использования функции pointer. Вместо этого определите метод преобразования и передайте переменные непосредственно в ccall. ccall автоматически позаботится о том, что все его аргументы будут сохранены от сборки мусора до возврата вызова. Если 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, который закрывает аргумент. Вы должны убедиться, что этот возвращаемый объект остается активным до завершения всех его применений. Содержимое и код по указателю cfunction будут удалены с помощью finalizer при удалении этой ссылки и atexit. Это обычно не требуется, так как такая функциональность отсутствует в C, но может быть полезной при работе с плохо спроектированными API, которые не предоставляют отдельный параметр для среды закрытия.

function qsort(a::Vector{T}, cmp) where T
    isbits(T) || throw(ArgumentError("this method can only qsort isbits arrays"))
    callback = @cfunction $cmp Cint (Ref{T}, Ref{T})
    # Here, `callback` isa Base.CFunction, which will be converted to Ptr{Cvoid}
    # (and protected against finalization) by the ccall
    ccall(:qsort, 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!.

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

Следующие методы описаны как «неопасные», поскольку неверный указатель или объявление типа могут привести к внезапному завершению 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 immutable типов структур).

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

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

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

Spec-Zone.ru

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