Вызов кода 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:
-
Пара
(:function, "library")(чаще всего),ИЛИ
символ имени
:functionили строка имени"function"(для символов в текущем процессе или libc),ИЛИ
указатель на функцию (например, из
dlsym). Тип возвращаемого значения функции
Кортеж типов входных данных, соответствующих сигнатуре функции
Фактические значения аргументов, которые нужно передать функции (если таковые имеются); каждый — отдельный параметр.
Пара (:function, "library"), тип возвращаемого значения и типы входных данных должны быть буквальными константами (то есть не могут быть переменными, но см. Непостоянные спецификации функций ниже).
Остальные параметры оцениваются во время компиляции, когда определяется содержащий метод.
См. ниже, как сопоставить типы C с типами Julia.
В качестве полного, но простого примера, следующий вызов функции clock из стандартной C-библиотеки на большинстве систем, основанных на Unix:
julia> t = ccall(:clock, Int32, ()) 2292761 julia> t 2292761 julia> typeof(t) Int32
clock не принимает аргументов и возвращает Int32. Распространённой ошибкой является забывание, что кортеж типов аргументов должен быть записан с последующей запятой. Например, чтобы вызвать функцию 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:
- Функция Julia
- Тип возвращаемого значения функции
- Кортеж типов входных данных, соответствующих сигнатуре функции
Как и для ccall, тип возвращаемого значения и кортеж типов входных данных должны быть буквальными константами.
В настоящее время поддерживается только платформа-стандартное соглашение о вызовах C. Это означает, что указатели, сгенерированные с помощью @cfunction, не могут использоваться в вызовах, где WINAPI ожидает функцию stdcall на 32-битной Windows, но могут использоваться на WIN64 (где stdcall унифицирован с соглашением о вызовах C).
Классическим примером является стандартная функция C-библиотеки qsort, объявленная как:
void qsort(void *base, size_t nmemb, size_t size,
int (*compare)(const void*, const void*));
Аргумент base — указатель на массив длиной nmemb с элементами по size байтов каждый. compare — функция обратного вызова, которая принимает указатели на два элемента a и b и возвращает целое число, меньшее или большее нуля, если a должно появиться перед/после b (или ноль, если любой порядок разрешен).
Теперь предположим, что у нас есть одномерный массив A значений в Julia, которые мы хотим отсортировать, используя функцию qsort (а не встроенную функцию Julia sort). Прежде чем рассматривать вызов qsort и передачу аргументов, нам нужно написать функцию сравнения:
julia> function mycompare(a, b)::Cint
return (a < b) ? -1 : ((a > b) ? +1 : 0)
end
mycompare (generic function with 1 method)
qsort ожидает функцию сравнения, которая возвращает C int, поэтому мы аннотируем тип возвращаемого значения как Cint.
Для передачи этой функции в C мы получаем ее адрес с помощью макроса @cfunction:
julia> mycompare_c = @cfunction(mycompare, Cint, (Ref{Cdouble}, Ref{Cdouble}));
@cfunction требует трех аргументов: функции Julia (mycompare), типа возвращаемого значения (Cint) и буквального кортежа типов аргументов входных данных, в данном случае для сортировки массива Cdouble (Float64) элементов.
Окончательный вызов qsort выглядит следующим образом:
julia> A = [1.3, -2.7, 4.4, 3.1]
4-element 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, если завершается символом NUL, или 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-функцию, которая не предполагает завершения символом NUL (например, если вы передаёте явную длину строки), или если вы уверены, что ваша строка 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-функция ожидает строку, завершающуюся символом 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)
Для функций 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 автоматически организует сохранение всех аргументов от сборки мусора до возврата вызова. Если 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) может быть вызовом функции, например, (: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, ())
Закрытые функции cfunctions
Первый аргумент к @cfunction может быть помечен $, в этом случае возвращаемое значение будет struct CFunction, которое замыкает аргумент. Необходимо гарантировать, что этот возвращаемый объект сохраняется до тех пор, пока все его использования не будут завершены. Содержимое и код в указателе cfunction будут удалены с помощью finalizer при высвобождении этой ссылки и atexit. Обычно это не требуется, так как эта функциональность отсутствует в C, но может быть полезна для работы с плохо спроектированными API, не предоставляющими отдельный параметр среды закрытия.
function qsort(a::Vector{T}, cmp) where T
isbits(T) || throw(ArgumentError("this method can only qsort isbits arrays"))
callback = @cfunction $cmp Cint (Ref{T}, Ref{T})
# Here, `callback` isa Base.CFunction, which will be converted to Ptr{Cvoid}
# (and protected against finalization) by the ccall
ccall(:qsort, Cvoid, (Ptr{T}, Csize_t, Csize_t, Ptr{Cvoid}),
a, length(a), Base.elsize(a), callback)
# We could instead use:
# GC.@preserve callback begin
# use(Base.unsafe_convert(Ptr{Cvoid}, callback))
# end
# if we needed to use it outside of a `ccall`
return a
end
Закрытые @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]). Аргумент индекса необязателен (по умолчанию равен 1) и следует соглашению Julia о нумерации с 1.
Возвращаемое значение — это новый объект, инициализированный копией содержимого указанной памяти. Указанная память может быть безопасно освобождена или возвращена.
Если T — Any, то предполагается, что память содержит ссылку на объект Julia (jl_value_t*), результатом будет ссылка на этот объект, и объект не будет скопирован. Следует быть осторожным в этом случае, чтобы убедиться, что объект всегда был виден сборщику мусора (указатели не учитываются, но новая ссылка учитывается), чтобы убедиться, что память не освобождается преждевременно. Обратите внимание, что если объект изначально не был выделен Julia, новый объект никогда не будет окончательно обработан сборщиком мусора Julia. Если само Ptr на самом деле является jl_value_t*, его можно преобразовать обратно в ссылку на объект Julia с помощью unsafe_pointer_to_objref(ptr). (Значения Julia v могут быть преобразованы в указатели jl_value_t*, как Ptr{Cvoid}, вызывая pointer_from_objref(v).)
Обратная операция (запись данных в Ptr{T}) может быть выполнена с помощью unsafe_store!(ptr, value, [index]). В настоящее время это поддерживается только для примитивных типов или других неизменяемых типов структур без указателей (isbits).
Любая операция, которая вызывает ошибку, вероятно, в настоящее время не реализована и должна быть опубликована как ошибка, чтобы её можно было разрешить.
Если указатель, который нас интересует, является обычным массивом данных (примитивный тип или неизменяемая структура), функция unsafe_wrap(Array, ptr,dims, own = false) может быть более полезной. Последний параметр должен быть истинным, если Julia должна «принять во владение» базовый буфер и вызвать free(ptr) при окончательной обработке возвращённого объекта Array. Если параметр own опущен или ложный, вызывающая сторона должна гарантировать, что буфер остаётся существующим до завершения всех обращений.
Арифметика с типом 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.6.0/manual/calling-c-and-fortran-code/