Spec-Zone.ru › Julia 0.5

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

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

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

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

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

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

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

  1. Пара (:function, “library”) (должна быть константой, но см. ниже).
  2. Тип возвращаемого значения (см. ниже для отображения объявленного типа C в Julia)
    • Этот аргумент будет вычислен во время компиляции.
  3. Кортеж типов входных данных. Типы входных данных должны быть записаны в виде литерального кортежа, а не в виде кортежа-переменной или выражения.
    • Этот аргумент будет вычислен во время компиляции.
  4. Дополнительные аргументы (если есть) — фактические значения аргументов, передаваемые функции.

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

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

julia> t
2292761

julia> typeof(ans)
Int32

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

julia> path = ccall((:getenv, "libc"), 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 известны своей несогласованностью в отношении способов указания условий ошибки. Например, функция 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 = Array{UInt8}(128)
  ccall((:gethostname, "libc"), Int32,
        (Ptr{UInt8}, Csize_t),
        hostname, sizeof(hostname))
  hostname[end] = 0; # ensure null-termination
  return unsafe_string(pointer(hostname))
end

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

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

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

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

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

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

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

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

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

function mycompare{T}(a::T, b::T)
    return convert(Cint, a < b ? -1 : a > b ? +1 : 0)::Cint
end

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

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

const mycompare_c = cfunction(mycompare, Cint, (Ref{Cdouble}, Ref{Cdouble}))

cfunction() принимает три аргумента: функцию Julia (mycompare), тип возвращаемого значения (Cint) и кортеж типов аргументов, в данном случае для сортировки массива из Cdouble (Float64) элементов.

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

A = [1.3, -2.7, 4.4, 3.1]
ccall(:qsort, Void, (Ptr{Cdouble}, Csize_t, Csize_t, Ptr{Void}),
      A, length(A), sizeof(eltype(A)), mycompare_c)

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

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

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

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

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

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

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

Сначала рассмотрим некоторые относящиеся к этому термины Джулии:

Синтаксис/Ключевое слово Пример Описание
type String «Листовой тип» :: Группа связанных данных, включающая тег типа, управляемая сборщиком мусора Джулии и определяемая тождеством объекта. Параметры типа листового типа должны быть полностью определены (не допускаются TypeVars), чтобы экземпляр был создан.
abstract Any, AbstractArray{T,N}, Complex{T} «Надтип» :: Надтип (не листовой тип), который нельзя экземплировать, но который можно использовать для описания группы типов.
{T} Vector{Int}

«Параметр типа» :: Специализация типа (как правило, используется для диспетчеризации или оптимизации хранения).

«TypeVar» :: T в объявлении параметра типа называется TypeVar (сокращение от type variable).

bitstype Int, Float64 «Тип битов» :: Тип без полей, но с размером. Он хранится и определяется по значению.
immutable

Pair{Int,Int}

Complex128 (isbits)

«Неизменяемый» :: Тип со всеми полями, определёнными как константы. Он определяется по значению. И может храниться с тегом типа.

«Is-Bits» :: Тип bitstype, или тип immutable, где все поля являются другими типами isbits. Он определяется по значению и хранится без тега типа.

type ...; end nothing «Синглтон» :: Листовой тип или неизменяемый тип без полей.
(...) или tuple(...)` (1,2,3) «Кортеж» :: Неизменяемая структура данных, подобная анонимному неизменяемому типу или постоянному массиву. Представлена как массив или структура.
typealias Не применимо здесь Псевдонимы типов и другие аналогичные механизмы косвенного обращения к типу разрешаются до базового типа (включая присвоение типа другому имени или получение типа из вызова функции).

Типы битов:

Существует несколько специальных типов, о которых следует знать, так как ни один другой тип не может быть определён таким же образом:

Float32
Точно соответствует типу float в C (или типу REAL*4 в Fortran).
Float64
Точно соответствует типу double в C (или типу REAL*8 в Fortran).
Complex64
Точно соответствует типу complex float в C (или типу COMPLEX*8 в Fortran).
Complex128
Точно соответствует типу complex double в C (или типу COMPLEX*16 в Fortran).
Signed
Точно соответствует аннотации типа signed в C (или любому типу INTEGER в Fortran). Любой тип Джулии, который не является подтипом Signed, предполагается беззнаковым.
Ref{T}
Ведёт себя как Ptr{T}, который владеет своей памятью.
Array{T,N}

Когда массив передаётся в C в качестве аргумента Ptr{T}, он не преобразуется с помощью reinterpret-cast: Джулия требует, чтобы тип элемента массива соответствовал T, и передаётся адрес первого элемента.

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

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

Если массив с типом элемента Ptr{T} передаётся как аргумент Ptr{Ptr{T}}, Base.cconvert() попытается сначала создать завершающийся нулём копию массива, заменив каждый элемент его cconvert() версией. Это позволяет, например, передать массив указателей argv типа Vector{String} в аргумент типа Ptr{Ptr{Cchar}}.

На всех поддерживаемых системах базовые типы значений C/C++ могут быть преобразованы в типы Джулии следующим образом. Каждый тип C также имеет соответствующий тип Джулии с тем же именем, но с префиксом C. Это может помочь в написании переносимого кода (и помнить, что int в C не то же самое, что и Int в Джулии).

Независимая от системы:

Имя в C Имя в Fortran Стандартный псевдоним Джулии Базовый тип Джулии

unsigned char

bool (C++)

CHARACTER Cuchar UInt8
short

INTEGER*2

LOGICAL*2

Cshort Int16
unsigned short Cushort UInt16

int

BOOL (C, типично)

INTEGER*4

LOGICAL*4

Cint Int32
unsigned int Cuint UInt32
long long

INTEGER*8

LOGICAL*8

Clonglong Int64
unsigned long long Culonglong UInt64
intmax_t Cintmax_t Int64
uintmax_t Cuintmax_t UInt64
float REAL*4i Cfloat Float32
double REAL*8 Cdouble Float64
complex float COMPLEX*8 Complex64 Complex{Float32}
complex double COMPLEX*16 Complex128 Complex{Float64}
ptrdiff_t Cptrdiff_t Int
ssize_t Cssize_t Int
size_t Csize_t UInt
void Void
void и [[noreturn]] или _Noreturn Union{}
void* Ptr{Void}
T* (где T представляет соответствующий тип) Ref{T}
char* (или char[], например, строка) CHARACTER*N Cstring если завершается нулём, или Ptr{UInt8} если нет
char** (или *char[]) Ptr{Ptr{UInt8}}
jl_value_t* (любой тип Джулии) Any
jl_value_t** (ссылка на тип Джулии) 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 (если ожидаются данные, завершённые NUL) или 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), но возвращают.

Примечание

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

Примечание

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

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

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

argv = [ "a.out", "arg1", "arg2" ]
ccall(:main, Int32, (Int32, Ptr{Ptr{UInt8}}), length(argv), argv)

Примечание

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

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

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

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

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

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

Массивы параметров необходимо вручную расширять (либо встраивать, либо в неизменяемом вспомогательном типе). Например:

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

in Julia:
immutable B_A
    A_1::Cint
    A_2::Cint
    A_3::Cint
end
type B
    A::B_A
end
b_a_2 = B.A.(2)

Массивы неизвестного размера не поддерживаются.

В будущем некоторые из этих ограничений могут быть сняты или уменьшены.

Значения 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:

typealias 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 через неявный вызов cconvert(). Напротив, указатели, возвращаемые вызываемой C-функцией, должны быть объявлены как выходной тип Ptr{T}, отражая, что управляемая памятью указателя управляется только C. Указатели, содержащиеся в C-структурах, должны представляться в качестве полей типа Ptr{T} в соответствующих неизменяемых типах Julia, предназначенных для имитации внутренней структуры соответствующих C-структур.

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

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

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

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

  • T, где T — один из примитивных типов: char, int, long, short, float, double, complex, enum или любой из их typedef эквивалентов
    • T, где T — эквивалентный тип Julia Bits (по таблице выше)
    • если T — это enum, тип аргумента должен быть эквивалентен Cint или Cuint
    • значение аргумента будет скопировано (передача по значению)
  • struct T (включая typedef для структуры)
    • T, где T — это тип Julia leaf
    • значение аргумента будет скопировано (передача по значению)
  • void*
    • зависит от того, как этот параметр используется, сначала преобразуйте его в целевой тип указателя, затем определите эквивалент Julia, используя оставшиеся правила в этом списке
    • этот аргумент может быть объявлен как Ptr{Void}, если это действительно просто неизвестный указатель
  • jl_value_t*
    • Any
    • значение аргумента должно быть допустимым объектом Julia
    • в настоящее время не поддерживается cfunction()
  • jl_value_t**
    • Ref{Any}
    • значение аргумента должно быть допустимым объектом Julia (или C_NULL)
    • в настоящее время не поддерживается cfunction()
  • T*
    • Ref{T}, где T — тип Julia, соответствующий T
    • значение аргумента будет скопировано, если это тип isbits, в противном случае значение должно быть допустимым объектом Julia
  • (T*)(...) (например, указатель на функцию)
    • Ptr{Void} (возможно, вам нужно явно использовать cfunction(), чтобы создать этот указатель)
  • ... (например, vararg)
    • T..., где T — тип Julia
  • va_arg
    • не поддерживается

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

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

  • void
    • Void (это вернет единственный экземпляр nothing::Void)
  • T, где T — один из примитивных типов: char, int, long, short, float, double, complex, enum или любой из их typedef эквивалентов
    • T, где T — эквивалентный тип Julia Bits (по таблице выше)
    • если T — это enum, тип аргумента должен быть эквивалентен Cint или Cuint
    • значение аргумента будет скопировано (возврат по значению)
  • struct T (включая typedef для структуры)
    • T, где T — тип Julia Leaf
    • значение аргумента будет скопировано (возврат по значению)
  • void*
    • зависит от того, как используется этот параметр, сначала преобразуйте его в целевой тип указателя, затем определите эквивалент Julia, используя оставшиеся правила в этом списке
    • этот аргумент может быть объявлен как Ptr{Void}, если это действительно просто неизвестный указатель
  • jl_value_t*
    • Any
    • значение аргумента должно быть допустимым объектом Julia
  • jl_value_t**
    • Ref{Any}
    • значение аргумента должно быть допустимым объектом Julia (или C_NULL)
  • T*
    • Если память уже принадлежит Julia или это тип isbits, и известно, что она не нулевая:
      • Ref{T}, где T — тип Julia, соответствующий T
      • тип возвращаемого значения Ref{Any} недопустим, он должен быть Any (соответствующий jl_value_t*) или Ptr{Any} (соответствующий Ptr{Any})
      • C НЕ ДОЛЖЕН изменять память, возвращаемую через Ref{T}, если T — это тип isbits
    • Если память принадлежит C:
      • Ptr{T}, где T — тип Julia, соответствующий T
  • (T*)(...) (например, указатель на функцию)
    • Ptr{Void} (возможно, вам нужно явно использовать cfunction(), чтобы создать этот указатель)

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

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

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

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

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

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

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

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

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

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

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

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

type gsl_permutation
end

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

hn = Array{UInt8}(256)
err = ccall(:gethostname, stdcall, Int32, (Ptr{UInt8}, UInt32), hn, length(hn))

Для получения дополнительной информации, пожалуйста, см. LLVM Language Reference.

Доступ к глобальным переменным

К глобальным переменным, экспортируемым нативными библиотеками, можно получить доступ по имени, используя функцию 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{Void}, вызвав pointer_from_objref(v).)

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

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

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

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

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

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

cond = Base.AsyncCondition()
wait(cond)

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

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

Дополнительная информация об обратных вызовах

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

C++

Ограниченная поддержка C++ предоставляется пакетами Cpp, Clang и Cxx.

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

Spec-Zone.ru

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