Spec-Zone.ru › Julia 0.5

Встраивание 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(NULL);

    /* 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-функции Julia, — это инициализировать Julia. Это делается путем вызова jl_init, который принимает в качестве аргумента строку C (const char*) до расположения установки Julia. Когда аргумент равен NULL, Julia пытается определить расположение установки автоматически.

Второе утверждение в тестовой программе вычисляет утверждение Julia с помощью вызова jl_eval_string.

Перед завершением программы настоятельно рекомендуется вызвать jl_atexit_hook. Приведенный выше пример программы вызывает это значение перед возвращением из main.

Примечание

В настоящее время для динамической линковки с общим библиотечным файлом libjulia необходимо передать параметр RTLD_GLOBAL. В Python это выглядит следующим образом:

>>> julia=CDLL('./libjulia.dylib',RTLD_GLOBAL)
>>> julia.jl_init.argtypes = [c_char_p]
>>> julia.jl_init('.')
250593296

Примечание

Если программе Julia необходимо получить доступ к символам из основной исполняемой программы, возможно, потребуется добавить -Wl,--export-dynamic флаг линковщика во время компиляции в Linux, помимо флагов, сгенерированных julia-config.jl (описано ниже). Это не нужно при компиляции общего библиотечного файла.

Использование julia-config для автоматического определения параметров сборки

Скрипт julia-config.jl был создан для помощи в определении параметров сборки, необходимых программе, использующей встроенный Julia. Этот скрипт использует параметры сборки и системную конфигурацию конкретного дистрибутива Julia, с которым он вызван, чтобы экспортировать необходимые флаги компилятора для программы встраивания, чтобы взаимодействовать с этим дистрибутивом. Этот скрипт находится в общей директории данных Julia.

Пример

Ниже представлено практически то же самое, что и выше, с одной небольшой корректировкой; аргумент функции jl_init теперь JULIA_INIT_DIR, который определен в julia-config.jl.:

#include <julia.h>

int main(int argc, char *argv[])
{
   jl_init(JULIA_INIT_DIR);
   (void)jl_eval_string("println(sqrt(2.0))");
   jl_atexit_hook(0);
   return 0;
}

В командной строке

Простой способ использования этого скрипта — из командной строки. Предполагая, что julia-config.jl находится в /usr/local/julia/share/julia, его можно вызвать в командной строке непосредственно и передать любые комбинации из 3 флагов:

/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_is_float64(ret)) {
    double ret_unboxed = jl_unbox_float64(ret);
    printf("sqrt(2.0) in C: %e \n", ret_unboxed);
}

Чтобы проверить, является ли ret объекта определенного типа Julia, мы можем использовать функции 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);

На первом шаге, ссылка на функцию Julia sqrt извлекается путем вызова jl_get_function. Первый аргумент, передаваемый в 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, возврат предыдущего состояния как int
jl_gc_enable(1) Включение GC, возврат предыдущего состояния как int
jl_gc_is_enabled() Возврат текущего состояния как int

Работа с массивами

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 хранятся в памяти в порядке следования столбцов. Вот код, который создает двумерный массив и обращается к его свойствам:

// 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 C Julia из языка, поддерживающего исключения (например, Python, C#, C++), имеет смысл обернуть каждый вызов в libjulia с функцией, проверяющей, было ли сгенерировано исключение, а затем повторно генерировать исключение на языке хоста.

Генерация исключений Julia

При написании вызываемых функций Julia может потребоваться валидировать аргументы и генерировать исключения для указания ошибок. Типичная проверка типа выглядит так:

if (!jl_is_float64(val)) {
    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.5/manual/embedding/

Spec-Zone.ru

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