Встраивание Julia
Как мы видели в Вызове кода C и Fortran, в Julia есть простой и эффективный способ вызова функций, написанных на C. Но есть ситуации, когда требуется обратное: вызов функции Julia из кода C. Это может быть использовано для интеграции кода Julia в более крупный проект C/C++, без необходимости переписывать всё на C/C++. Julia имеет C API, чтобы сделать это возможным. Поскольку почти все языки программирования имеют какой-то способ вызова функций C, C API Julia также может быть использован для построения дополнительных мостов между языками (например, вызова Julia из Python или C#).
Встраивание высокого уровня
Начнем с простой программы на C, которая инициализирует Julia и вызывает некоторый код Julia:
#include <julia.h>
int main(int argc, char *argv[])
{
/* required: setup the Julia context */
jl_init();
/* run Julia commands */
jl_eval_string("print(sqrt(2.0))");
/* strongly recommended: notify Julia that the
program is about to terminate. this allows
Julia time to cleanup pending write requests
and run all finalizers
*/
jl_atexit_hook(0);
return 0;
}
Для построения этой программы вам необходимо добавить путь к заголовкам Julia в путь включения и слинковать с libjulia. Например, когда Julia установлена в $JULIA_DIR, можно скомпилировать вышеприведенную тестовую программу test.c с gcc используя:
gcc -o test -fPIC -I$JULIA_DIR/include/julia -L$JULIA_DIR/lib test.c -ljulia $JULIA_DIR/lib/julia/libstdc++.so.6
Затем, если переменная среды JULIA_HOME установлена в $JULIA_DIR/bin, можно запустить программу вывода test.
В качестве альтернативы, посмотрите на программу embedding.c в дереве исходного кода Julia в папке examples/. Файл ui/repl.c является еще одним простым примером того, как установить параметры jl_options при линковке с libjulia.
Первое, что нужно сделать перед вызовом любой другой функции C API Julia, это инициализация Julia. Это делается путем вызова jl_init, который пытается автоматически определить место установки Julia. Если вам нужно указать пользовательское местоположение или указать, какой системный образ загрузить, используйте jl_init_with_image вместо этого.
Второе утверждение в тестовой программе оценивает оператор Julia с помощью вызова jl_eval_string.
Перед завершением программы настоятельно рекомендуется вызвать jl_atexit_hook. В приведенном выше примере программы этот вызов происходит перед возвратом из main.
В настоящее время для динамической линковки с общим библиотечным файлом libjulia требуется передача параметра RTLD_GLOBAL. В Python это выглядит так:
>>> julia=CDLL('./libjulia.dylib',RTLD_GLOBAL)
>>> julia.jl_init.argtypes = []
>>> julia.jl_init()
250593296
Если программе julia необходимо получить доступ к символам из основного исполняемого файла, может потребоваться добавить флаг линковщика -Wl,--export-dynamic во время компиляции на Linux помимо флагов, сгенерированных julia-config.jl, описанных ниже. Это не требуется при компиляции общей библиотеки.
Использование julia-config для автоматического определения параметров сборки
Скрипт julia-config.jl был создан для помощи в определении параметров сборки, необходимых программе, использующей встроенный Julia. Этот скрипт использует параметры сборки и конфигурацию системы конкретного дистрибутива Julia, которым он вызван, для экспорта необходимых флагов компилятора для программы встраивания, чтобы взаимодействовать с этим дистрибутивом. Этот скрипт находится в каталоге общих данных Julia.
Пример
#include <julia.h>
int main(int argc, char *argv[])
{
jl_init();
(void)jl_eval_string("println(sqrt(2.0))");
jl_atexit_hook(0);
return 0;
}
В командной строке
Простое использование этого скрипта из командной строки. Предполагая, что julia-config.jl находится в /usr/local/julia/share/julia, его можно вызвать в командной строке напрямую, и он принимает любое сочетание трех флагов:
/usr/local/julia/share/julia/julia-config.jl Usage: julia-config [--cflags|--ldflags|--ldlibs]
Если вышеприведенный исходный код сохранен в файле embed_example.c, то следующая команда скомпилирует его в работающую программу на Linux и Windows (среда MSYS2) или, если на OS/X, замените clang на gcc.
/usr/local/julia/share/julia/julia-config.jl --cflags --ldflags --ldlibs | xargs gcc embed_example.c
Использование в Makefile
Но в целом, проекты встраивания будут сложнее, чем вышеприведенное, и поэтому следующее позволяет общую поддержку Makefile – предполагая GNU make из-за использования макросов расширения shell. Кроме того, хотя часто julia-config.jl может быть найден в каталоге /usr/local, это не обязательно так, но Julia может использоваться для поиска julia-config.jl тоже, и Makefile может быть использован для использования этого. Приведенный выше пример расширен с использованием Makefile:
JL_SHARE = $(shell julia -e 'print(joinpath(JULIA_HOME,Base.DATAROOTDIR,"julia"))') CFLAGS += $(shell $(JL_SHARE)/julia-config.jl --cflags) CXXFLAGS += $(shell $(JL_SHARE)/julia-config.jl --cflags) LDFLAGS += $(shell $(JL_SHARE)/julia-config.jl --ldflags) LDLIBS += $(shell $(JL_SHARE)/julia-config.jl --ldlibs) all: embed_example
Теперь команда сборки просто make.
Преобразование типов
В реальных приложениях не нужно будет просто выполнять выражения, но также возвращать их значения в программу-хост. jl_eval_string возвращает jl_value_t*, который является указателем на выделенный на куче объект Julia. Хранение простых типов данных, таких как Float64 таким образом называется boxing, а извлечение сохраненных примитивных данных называется unboxing.
Наша улучшенная демонстрационная программа, которая вычисляет квадратный корень из 2 в Julia и считывает результат в C, выглядит следующим образом:
jl_value_t *ret = jl_eval_string("sqrt(2.0)");
if (jl_typeis(ret, jl_float64_type)) {
double ret_unboxed = jl_unbox_float64(ret);
printf("sqrt(2.0) in C: %e \n", ret_unboxed);
}
else {
printf("ERROR: unexpected return type from sqrt(::Float64)\n");
}
Для проверки того, является ли ret объекта определенного типа Julia, мы можем использовать функции jl_isa, jl_typeis или jl_is_.... Введя typeof(sqrt(2.0)) в оболочку Julia, мы можем увидеть, что возвращаемый тип — Float64 (double в C). Для преобразования упакованного значения Julia в двойное значение C в приведенном выше фрагменте кода используется функция jl_unbox_float64.
Соответствующие функции jl_box_... используются для преобразования в обратную сторону:
jl_value_t *a = jl_box_float64(3.0); jl_value_t *b = jl_box_float32(3.0f); jl_value_t *c = jl_box_int32(3);
Как мы увидим далее, упаковка необходима для вызова функций Julia с определенными аргументами.
Вызов функций Julia
В то время как jl_eval_string позволяет C получить результат выражения Julia, это не позволяет передавать аргументы, вычисленные в C, в Julia. Для этого вам необходимо вызывать функции Julia напрямую, используя jl_call:
jl_function_t *func = jl_get_function(jl_base_module, "sqrt"); jl_value_t *argument = jl_box_float64(2.0); jl_value_t *ret = jl_call1(func, argument);
На первом шаге используется jl_get_function, чтобы получить дескриптор функции Julia sqrt. Первым аргументом, передаваемым в jl_get_function является указатель на модуль Base, в котором определена sqrt. Затем двойное значение упаковывается с помощью jl_box_float64. Наконец, на последнем шаге функция вызывается с помощью jl_call1. Функции jl_call0, jl_call2 и jl_call3 также существуют, чтобы удобно обрабатывать различные количество аргументов. Чтобы передать больше аргументов, используйте jl_call:
jl_value_t *jl_call(jl_function_t *f, jl_value_t **args, int32_t nargs)
Его второй аргумент args — массив аргументов jl_value_t*, а nargs — количество аргументов.
Управление памятью
Как мы видели, объекты Julia представлены в C в виде указателей. Это поднимает вопрос о том, кто отвечает за освобождение этих объектов.
Как правило, объекты Julia освобождаются сборщиком мусора (GC), но GC автоматически не знает, что мы держим ссылку на значение Julia из C. Это означает, что GC может освободить объекты, лишив вас доступа к указателям.
GC может работать только при выделении объектов Julia. Вызовы, подобные jl_box_float64, выполняют выделение, а выделение может происходить в любой момент при выполнении кода Julia. Однако в целом безопасно использовать указатели между вызовами jl_....
Однако, чтобы убедиться, что значения могут пережить вызовы jl_..., мы должны сообщить Julia, что мы держим ссылку на значение Julia. Это можно сделать, используя макросы JL_GC_PUSH:
jl_value_t *ret = jl_eval_string("sqrt(2.0)");
JL_GC_PUSH1(&ret);
// Do something with ret
JL_GC_POP();
Вызов JL_GC_POP освобождает ссылки, установленные предыдущим вызовом JL_GC_PUSH.
Обратите внимание, что JL_GC_PUSH работает в стеке, поэтому он должен быть точно парен с JL_GC_POP перед уничтожением кадра стека.
Несколько значений Julia могут быть одновременно добавлены с помощью макросов JL_GC_PUSH2, JL_GC_PUSH3 и JL_GC_PUSH4. Для добавления массива значений Julia можно использовать макрос JL_GC_PUSHARGS, который используется следующим образом:
jl_value_t **args; JL_GC_PUSHARGS(args, 2); // args can now hold 2 `jl_value_t*` objects args[0] = some_value; args[1] = some_other_value; // Do something with args (e.g. call jl_... functions) JL_GC_POP();
Сборщик мусора также предполагает, что он знает о каждом объекте старого поколения, указывающем на объект молодого поколения. В любое время, когда указатель обновляется, нарушая это предположение, необходимо уведомить коллектор с помощью функции jl_gc_wb (барьер записи), как показано ниже:
jl_value_t *parent = some_old_value, *child = some_young_value; ((some_specific_type*)parent)->field = child; jl_gc_wb(parent, child);
В общем случае невозможно предсказать, какие значения будут старыми во время выполнения, поэтому барьер записи должен быть вставлен после всех явных записей.
Исключением является случай, когда объект parent был только что выделен, и сборка мусора с тех пор не выполнялась. Помните, что большинство функций jl_... могут иногда вызывать сборку мусора.
Барьер записи также необходим для массивов указателей при непосредственном обновлении их данных. Например:
jl_array_t *some_array = ...; // e.g. a Vector{Any}
void **data = (void**)jl_array_data(some_array);
jl_value_t *some_value = ...;
data[0] = some_value;
jl_gc_wb(some_array, some_value);
Управление сборщиком мусора
Есть некоторые функции для управления GC. В обычных случаях они не нужны.
| Функция | Описание |
|---|---|
jl_gc_collect() |
Вынужденный запуск GC |
jl_gc_enable(0) |
Отключить GC, вернуть предыдущее состояние в виде целого числа |
jl_gc_enable(1) |
Включить GC, вернуть предыдущее состояние в виде целого числа |
jl_gc_is_enabled() |
Возвращает текущее состояние в виде целого числа |
Работа с массивами
Julia и C могут совместно использовать данные массивов без копирования. Следующий пример продемонстрирует, как это работает.
Массивы Julia представлены в C типом данных jl_array_t*. По сути, jl_array_t — это структура, которая содержит:
Информация о типе данных
Указатель на блок данных
Информация о размерах массива
Для простоты начнем с одномерного массива. Создание массива, содержащего элементы типа Float64 длиной 10, выполняется так:
jl_value_t* array_type = jl_apply_array_type(jl_float64_type, 1); jl_array_t* x = jl_alloc_array_1d(array_type, 10);
В качестве альтернативы, если вы уже выделили массив, вы можете создать тонкий обертка вокруг его данных:
double *existingArray = (double*)malloc(sizeof(double)*10); jl_array_t *x = jl_ptr_to_array_1d(array_type, existingArray, 10, 0);
Последний аргумент — булево значение, указывающее, должна ли Julia взять на себя владение данными. Если этот аргумент не равен нулю, GC вызовет free на указателе данных, когда массив больше не ссылается.
Для доступа к данным x можно использовать jl_array_data:
double *xData = (double*)jl_array_data(x);
Теперь мы можем заполнить массив:
for(size_t i=0; i<jl_array_len(x); i++)
xData[i] = i;
Теперь давайте вызовем функцию Julia, которая выполняет операцию на месте с x:
jl_function_t *func = jl_get_function(jl_base_module, "reverse!"); jl_call1(func, (jl_value_t*)x);
Выведя массив, можно проверить, что элементы x теперь перевернуты.
Доступ к возвращаемым массивам
Если функция Julia возвращает массив, возвращаемое значение jl_eval_string и jl_call можно привести к типу jl_array_t*.
jl_function_t *func = jl_get_function(jl_base_module, "reverse"); jl_array_t *y = (jl_array_t*)jl_call1(func, (jl_value_t*)x);
Теперь содержимое y можно получить как раньше, используя jl_array_data. Как всегда, убедитесь, что у вас есть ссылка на массив во время его использования.
Многомерные массивы
Многомерные массивы Julia хранятся в памяти в порядке следования столбцов. Вот код, который создаёт 2D-массив и обращается к его свойствам:
// Create 2D array of float64 type
jl_value_t *array_type = jl_apply_array_type(jl_float64_type, 2);
jl_array_t *x = jl_alloc_array_2d(array_type, 10, 5);
// Get array pointer
double *p = (double*)jl_array_data(x);
// Get number of dimensions
int ndims = jl_array_ndims(x);
// Get the size of the i-th dim
size_t size0 = jl_array_dim(x,0);
size_t size1 = jl_array_dim(x,1);
// Fill array with data
for(size_t i=0; i<size1; i++)
for(size_t j=0; j<size0; j++)
p[j + size0*i] = i + j;
Обратите внимание, что, хотя массивы Julia используют индексацию с 1, API C использует индексацию с 0 (например, при вызове jl_array_dim) для чтения в стиле C.
Исключения
Код Julia может генерировать исключения. Например, рассмотрим:
jl_eval_string("this_function_does_not_exist()");
Этот вызов, кажется, ничего не делает. Однако можно проверить, было ли сгенерировано исключение:
if (jl_exception_occurred())
printf("%s \n", jl_typeof_str(jl_exception_occurred()));
Если вы используете API Julia C из языка, поддерживающего исключения (например, Python, C#, C++), имеет смысл обернуть каждый вызов в libjulia с функцией, которая проверяет, было ли сгенерировано исключение, а затем повторно генерирует исключение на языке хоста.
Генерация исключений Julia
При написании вызываемых функций Julia может потребоваться валидировать аргументы и генерировать исключения для обозначения ошибок. Типичная проверка типа выглядит следующим образом:
if (!jl_typeis(val, jl_float64_type)) {
jl_type_error(function_name, (jl_value_t*)jl_float64_type, val);
}
Общие исключения можно генерировать, используя функции:
void jl_error(const char *str); void jl_errorf(const char *fmt, ...);
jl_error принимает строку C, а jl_errorf вызывается как printf.
jl_errorf("argument x = %d is too large", x);
В этом примере предполагается, что x является целым числом.
© 2009–2016 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/release-0.6/manual/embedding/