Spec-Zone.ru › Julia 1.7

Вызов кода 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(t)
Int32

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

julia> path = ccall(:getenv, 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, 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 GC.@preserve hostname unsafe_string(pointer(hostname))
end

Этот пример сначала выделяет массив байтов. Затем он вызывает функцию библиотеки C gethostname для заполнения массива именем хоста. Наконец, он принимает указатель на буфер с именем хоста и преобразует указатель в строку Julia, предполагая, что это строка C с завершением нулевым символом.

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

Обращение к pointer(hostname) с помощью unsafe_string является небезопасной операцией, так как она требует доступа к памяти, выделенной для hostname, которая впоследствии может быть удалена сборщиком мусора. Макрос GC.@preserve предотвращает это, а значит, доступ к недействительному участку памяти.

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

Можно передавать функции 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).

Функции обратного вызова, экспонированные через @cfunction, не должны генерировать ошибки, так как это неожиданно вернет управление в среду выполнения Julia и может оставить программу в неопределенном состоянии.

Классический пример — функция стандартной библиотеки 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 Vector{Float64}:
  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 Vector{Float64}:
 -2.7
  1.3
  3.1
  4.4

Как показывает пример, исходный массив Julia 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} аргумента, не происходит переинтерпретации: 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 (_Bool в C99+) 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} (или аналогично Ref{Cvoid})
T* (где T представляет собой соответствующим образом определённый тип) Ref{T} (T может быть безопасно изменён только в том случае, если T — тип isbits)
char* (или char[], например, строка) CHARACTER*N Cstring если завершается нулём, или Ptr{UInt8} если нет
char** (или *char[]) Ptr{Ptr{UInt8}}
jl_value_t* (любой тип Julia) Any
jl_value_t* const* (ссылка на значение Julia) Ref{Any} (const, поскольку изменение потребовало бы барьер записи, что невозможно правильно вставить)
va_arg Не поддерживается
... (спецификация функции с переменным числом аргументов) T... (где T — один из вышеперечисленных типов при использовании функции ccall)
... (спецификация функции с переменным числом аргументов) ; va_arg1::T, va_arg2::S, etc. (поддерживается только с макросом @ccall)

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

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

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

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

Для строковых аргументов (char*) тип Julia должен быть Cstring (если ожидаются данные, завершённые 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), но возвращают что-то, используйте Cvoid вместо этого.

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

END_OF_DOCUMENT_MARKER

Функции 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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    • [для ccall]: T..., где T — единственный тип Julia всех оставшихся аргументов
    • [для @ccall]: ; va_arg1::T, va_arg2::S, etc, где T и S — тип Julia (то есть разделите обычные аргументы и varargs с помощью ;)
    • в настоящее время не поддерживается @cfunction
  • va_arg

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

ccall / @cfunction руководство по переводу типов возвращаемых значений

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

  • void

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

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

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

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

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

    • Ptr{Any} (Ref{Any} недопустим как тип возвращаемого значения)
  • T*

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

      • Ref{T}, где T — тип Julia, соответствующий T
      • тип возврата Ref{Any} недопустим, он должен быть либо Any (соответствующий jl_value_t*) или Ptr{Any} (соответствующий jl_value_t**)
      • C НЕ ДОЛЖЕН изменять память, возвращаемую через Ref{T}, если T — тип isbits
    • Если память принадлежит C:

      • Ptr{T}, где T — тип Julia, соответствующий T
  • T (*)(...) (например, указатель на функцию)

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

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

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

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

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

Примеры обёртки 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.

Входной параметр 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 автоматически обеспечивает, что все его аргументы сохраняются от сбора мусора до возврата вызова. Если API C будет хранить ссылку на память, выделенную Julia, после возврата ccall, вы должны убедиться, что объект остаётся видимым для сборщика мусора. Рекомендуемый способ сделать это — создать глобальную переменную типа Array{Ref,1} для хранения этих значений до тех пор, пока библиотека C не сообщит вам, что закончила с ними.

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

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

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

В некоторых случаях точное имя или путь к необходимой библиотеке неизвестны заранее и должны быть вычислены во время выполнения. Для обработки таких случаев компонент библиотеки в спецификации (name, library) может быть вызовом функции, например, (:dgemm_, find_blas()). Выражение вызова будет выполнено, когда будет выполнено само ccall. Однако предполагается, что расположение библиотеки не меняется после определения, поэтому результат вызова можно кэшировать и повторно использовать. Поэтому количество раз выполнения выражения не определено, а возврат различных значений для нескольких вызовов приводит к неопределённому поведению.

Если требуется ещё большая гибкость, можно использовать вычисленные значения в качестве имён функций, используя 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

Закрытые @cfunction полагаются на LLVM-трамплины, которые не доступны на всех платформах (например, ARM и PowerPC).

Закрытие библиотеки

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

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

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

Учитывая Ptr{T}, содержимое типа T обычно может быть скопировано из указанной памяти в объект Julia с помощью unsafe_load(ptr, [index]). Аргумент 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.

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

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

Spec-Zone.ru

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