Embedding Julia

Как мы видели в Calling C and Fortran Code, у Julia есть простой и эффективный способ вызывать функции, написанные на C. Но есть ситуации, когда необходимо сделать наоборот: вызывать функции Julia из кода C. Это можно использовать для интеграции кода Julia в более крупный проект на C/C++, без необходимости переписывать все на C/C++. У Julia есть C API, чтобы сделать это возможным. Поскольку почти все языки программирования имеют какой-то способ вызывать функции C, C API Julia также можно использовать для создания дальнейших языковых мостов (например, вызов Julia из Python, Rust или C#). Хотя Rust и C++ могут использовать C embedding API напрямую, у обоих есть пакеты, помогающие с этим, для C++ Jluna полезен.

High-Level Embedding

Примечание: Этот раздел охватывает встраивание кода Julia в C на операционных системах, подобных Unix. Для выполнения этого на Windows, пожалуйста, смотрите следующий раздел, High-Level Embedding on Windows with Visual Studio.

Мы начинаем с простого C-программы, которая инициализирует Julia и вызывает некоторый код на Julia:

#include <julia.h>
JULIA_DEFINE_FAST_TLS // only define this once, in an executable (not in a shared library) if you want fast code.

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 -Wl,-rpath,$JULIA_DIR/lib test.c -ljulia

В качестве альтернативы посмотрите на программу embedding.c в дереве исходного кода Julia в папке test/embedding/. Файл cli/loader_exe.c является еще одним простым примером того, как установить параметры jl_options при линковке с libjulia.

Первое, что необходимо сделать перед вызовом любой другой функции C Julia, это инициализировать Julia. Это делается с помощью вызова jl_init, который пытается автоматически определить местоположение установки Julia. Если вам нужно указать пользовательское местоположение или указать, какой системный образ загрузить, используйте вместо этого jl_init_with_image.

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

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

Note

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

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

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

Using julia-config to automatically determine build parameters

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

Example

#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;
}

On the command line

Простой способ использования этого скрипта — из командной строки. Предполагая, что 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). На macOS замените gcc на clang.

/usr/local/julia/share/julia/julia-config.jl --cflags --ldflags --ldlibs | xargs gcc embed_example.c

Use in Makefiles

В общем, проекты встраивания будут более сложными, чем приведенный выше пример, и поэтому следующее также позволяет общую поддержку makefile – предполагая использование GNU make из-за использования макросов shell. Более того, хотя julia-config.jl обычно находится в директории /usr/local, если это не так, то сам Julia может быть использован для поиска julia-config.jl, и makefile может воспользоваться этим. Приведенный выше пример расширен для использования makefile:

JL_SHARE = $(shell julia -e 'print(joinpath(Sys.BINDIR, 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.

High-Level Embedding on Windows with Visual Studio

Если переменная окружения JULIA_DIR не была настроена, добавьте её с помощью панели Система перед запуском Visual Studio. Папка bin в JULIA_DIR должна быть в системном PATH.

Мы начинаем с открытия Visual Studio и создания нового проекта консольного приложения. Откройте заголовочный файл 'stdafx.h' и добавьте следующие строки в конце:

#include <julia.h>

Затем замените функцию main() в проекте на этот код:

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 и библиотек. Важно знать, является ли установка Julia 32- или 64-битной. Удалите любую конфигурацию платформы, которая не соответствует установке Julia, прежде чем продолжить.

Используя диалоговое окно свойств проекта, перейдите в C/C++ | Общие и добавьте $(JULIA_DIR)\include\julia\ в свойство Дополнительные каталоги включения. Затем перейдите в раздел Компоновщик | Общие и добавьте $(JULIA_DIR)\lib в свойство Дополнительные каталоги библиотек. Наконец, в разделе Компоновщик | Ввод добавьте libjulia.dll.a;libopenlibm.dll.a; в список библиотек.

На данный момент проект должен собираться и запускаться.

Converting Types

Реальные приложения должны не только выполнять выражения, но и возвращать их значения в хост-программу. 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 double, в приведенном выше фрагменте кода используется функция 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 с конкретными аргументами.

Calling Julia Functions

Хотя 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. Затем значение типа double упаковывается с помощью 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, и это через @cfunction. Использование @cfunction позволяет вам выполнять преобразования типов на стороне Julia, что обычно проще, чем делать это на стороне C. Пример с sqrt выше с использованием @cfunction будет записан как:

double (*sqrt_jl)(double) = jl_unbox_voidpointer(jl_eval_string("@cfunction(sqrt, Float64, (Float64,))"));
double ret = sqrt_jl(2.0);

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

Memory Management

Как мы видели, объекты Julia представлены в C в виде указателей типа jl_value_t*. Это поднимает вопрос о том, кто отвечает за освобождение этих объектов.

Обычно объекты Julia освобождаются сборщиком мусора (GC), но GC не знает автоматически, что мы держим ссылку на значение Julia из C. Это означает, что GC может освободить объекты, не уведомив вас, что делает указатели недействительными.

GC будет работать только тогда, когда выделяются новые объекты Julia. Вызовы, такие как jl_box_float64, выполняют выделение, но выделение также может происходить в любой момент выполнения кода Julia.

Когда вы пишете код, который встраивает Julia, обычно безопасно использовать значения jl_value_t* между вызовами jl_... (так как сборка мусора будет вызываться только этими вызовами). Но для того чтобы убедиться, что значения могут пережить вызовы jl_..., мы должны сообщить Julia, что мы все еще держим ссылку на значения Julia root, процесс, называемый "GC rooting". Корневая ссылка на значение обеспечит, что сборщик мусора случайно не определит это значение как неиспользуемое и не освободит память, поддерживающую это значение. Это можно сделать с помощью макросов 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 сохраняет ссылки в стеке C, поэтому он должен быть точно сопоставлен с JL_GC_POP перед выходом из области видимости. То есть, перед тем как функция вернёт значение или управление покинет блок, в котором был вызван JL_GC_PUSH.

Несколько значений Julia можно добавить сразу, используя макросы JL_GC_PUSH2 до JL_GC_PUSH6:

JL_GC_PUSH2(&ret1, &ret2);
// ...
JL_GC_PUSH6(&ret1, &ret2, &ret3, &ret4, &ret5, &ret6);

Чтобы добавить массив значений 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_PUSH* и должен быть сопоставлен только с одним вызовом JL_GC_POP. Если все необходимые переменные, которые вы хотите зафиксировать, не могут быть добавлены одним единственным вызовом JL_GC_PUSH*, или если есть более 6 переменных для добавления, и использование массива аргументов не является вариантом, то можно использовать внутренние блоки:

jl_value_t *ret1 = jl_eval_string("sqrt(2.0)");
JL_GC_PUSH1(&ret1);
jl_value_t *ret2 = 0;
{
    jl_function_t *func = jl_get_function(jl_base_module, "exp");
    ret2 = jl_call1(func, ret1);
    JL_GC_PUSH1(&ret2);
    // Do something with ret2.
    JL_GC_POP();    // This pops ret2.
}
JL_GC_POP();    // This pops ret1.

Обратите внимание, что наличие действительных jl_value_t* значений перед вызовом JL_GC_PUSH* не является обязательным. Можно инициализировать их значением NULL, передать их в JL_GC_PUSH*, а затем создать фактические значения Julia. Например:

jl_value_t *ret1 = NULL, *ret2 = NULL;
JL_GC_PUSH2(&ret1, &ret2);
ret1 = jl_eval_string("sqrt(2.0)");
ret2 = jl_eval_string("sqrt(3.0)");
// Use ret1 and ret2
JL_GC_POP();

Если требуется удерживать указатель на переменную между функциями (или областями блоков), то невозможно использовать JL_GC_PUSH*. В этом случае необходимо создать и сохранить ссылку на переменную в глобальной области видимости Julia. Один простой способ сделать это - использовать глобальный IdDict, который будет хранить ссылки, избегая освобождения памяти сборщиком мусора. Однако этот метод будет работать правильно только с изменяемыми типами.

// This functions shall be executed only once, during the initialization.
jl_value_t* refs = jl_eval_string("refs = IdDict()");
jl_function_t* setindex = jl_get_function(jl_base_module, "setindex!");

...

// `var` is the variable we want to protect between function calls.
jl_value_t* var = 0;

...

// `var` is a `Vector{Float64}`, which is mutable.
var = jl_eval_string("[sqrt(2.0); sqrt(4.0); sqrt(6.0)]");

// To protect `var`, add its reference to `refs`.
jl_call3(setindex, refs, var, var);

Если переменная неизменяемая, то её необходимо обернуть в эквивалентный изменяемый контейнер или, предпочтительно, в RefValue{Any}, прежде чем она будет добавлена в IdDict. В этом подходе контейнер должен быть создан или заполнен с помощью кода на C, используя, например, функцию jl_new_struct. Если контейнер создан с помощью jl_call*, то вам нужно будет перезагрузить указатель для использования в коде на C.

// This functions shall be executed only once, during the initialization.
jl_value_t* refs = jl_eval_string("refs = IdDict()");
jl_function_t* setindex = jl_get_function(jl_base_module, "setindex!");
jl_datatype_t* reft = (jl_datatype_t*)jl_eval_string("Base.RefValue{Any}");

...

// `var` is the variable we want to protect between function calls.
jl_value_t* var = 0;

...

// `var` is a `Float64`, which is immutable.
var = jl_eval_string("sqrt(2.0)");

// Protect `var` until we add its reference to `refs`.
JL_GC_PUSH1(&var);

// Wrap `var` in `RefValue{Any}` and push to `refs` to protect it.
jl_value_t* rvar = jl_new_struct(reft, var);
JL_GC_POP();

jl_call3(setindex, refs, rvar, rvar);

GC может быть разрешено освободить переменную, удалив ссылку на нее из refs с помощью функции delete!, при условии, что никакая другая ссылка на переменную не хранится нигде:

jl_function_t* delete = jl_get_function(jl_base_module, "delete!");
jl_call2(delete, refs, rvar);

В качестве альтернативы для очень простых случаев можно просто создать глобальный контейнер типа Vector{Any} и извлекать элементы из него по мере необходимости, или даже создать одну глобальную переменную на каждый указатель, используя

jl_module_t *mod = jl_main_module;
jl_sym_t *var = jl_symbol("var");
jl_binding_t *bp = jl_get_binding_wr(mod, var, 1);
jl_checked_assignment(bp, mod, var, val);

Updating fields of GC-managed objects

Сборщик мусора также работает на предположении, что он осведомлён о каждом объекте старшего поколения, указывающем на объект младшего поколения. Каждый раз, когда указатель обновляется, нарушая это предположение, это должно быть сигнализировано сборщику с помощью функции 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_ptr_set. Но прямые обновления также возможны. Например:

jl_array_t *some_array = ...; // e.g. a Vector{Any}
void **data = jl_array_data(some_array, void*);
jl_value_t *some_value = ...;
data[0] = some_value;
jl_gc_wb(jl_array_owner(some_array), some_value);

Controlling the Garbage Collector

Существуют некоторые функции для управления сборщиком мусора (GC). В обычных случаях их использование не должно быть необходимым.

FunctionDescription
jl_gc_collect()Force a GC run
jl_gc_enable(0)Disable the GC, return previous state as int
jl_gc_enable(1)Enable the GC, return previous state as int
jl_gc_is_enabled()Return current state as int

Working with Arrays

Джулия и C могут делиться данными массива без копирования. Следующий пример покажет, как это работает.

Массивы Julia представлены в C с помощью типа данных jl_array_t*. В основном, jl_array_t — это структура, которая содержит:

  • Информация о типе данных
  • Указатель на блок данных
  • Информация о размерах массива

Чтобы упростить задачу, мы начнем с одномерного массива. Создать массив, содержащий элементы Float64 длиной 10, можно сделать так:

jl_value_t* array_type = jl_apply_array_type((jl_value_t*)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);

Последний аргумент - это логическое значение, указывающее, должна ли Джулия взять на себя право собственности на данные. Если этот аргумент не равен нулю, сборщик мусора вызовет free для указателя данных, когда массив больше не будет ссылаться на него.

Чтобы получить доступ к данным x, мы можем использовать jl_array_data:

double *xData = jl_array_data(x, double);

Теперь мы можем заполнить массив:

for (size_t i = 0; i < jl_array_nrows(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 теперь перевернуты.

Accessing Returned Arrays

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

Multidimensional Arrays

Многомерные массивы Julia хранятся в памяти в порядке столбцов. Вот код, который создает двумерный массив и получает доступ к его свойствам:

// Create 2D array of float64 type
jl_value_t *array_type = jl_apply_array_type((jl_value_t*)jl_float64_type, 2);
int dims[] = {10,5};
jl_array_t *x  = jl_alloc_array_nd(array_type, dims, 2);

// Get array pointer
double *p = jl_array_data(x, double);
// 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, C API использует индексацию с 0 (например, при вызове jl_array_dim), чтобы читаться как идиоматический код на C.

Exceptions

Код на Julia может вызывать исключения. Например, рассмотрим:

jl_eval_string("this_function_does_not_exist()");

Этот вызов будет казаться бесполезным. Однако возможно проверить, было ли выброшено исключение:

if (jl_exception_occurred())
    printf("%s \n", jl_typeof_str(jl_exception_occurred()));

Если вы используете C API Julia из языка, который поддерживает исключения (например, Python, C#, C++), имеет смысл обернуть каждый вызов в libjulia в функцию, которая проверяет, было ли выброшено исключение, а затем повторно выбрасывает исключение в языке-хозяине.

Throwing Julia Exceptions

Когда вы пишете вызываемые функции на 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 предполагается как целое число.

Thread-safety

В общем, C API Julia не является полностью потокобезопасным. При встраивании Julia в многопоточное приложение необходимо соблюдать следующие ограничения:

  • jl_init() может быть вызван только один раз за время жизни приложения. То же самое касается jl_atexit_hook(), который может быть вызван только после jl_init().
  • jl_...() API функции могут вызываться только из потока, в котором была вызвана jl_init(), или из потоков, запущенных средой выполнения Julia. Вызов функций API Julia из потоков, запущенных пользователем, не поддерживается и может привести к неопределенному поведению и сбоям.

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

void *func(void*)
{
    // Wrong, jl_eval_string() called from thread that was not started by Julia
    jl_eval_string("println(Threads.threadid())");
    return NULL;
}

int main()
{
    pthread_t t;

    jl_init();

    // Start a new thread
    pthread_create(&t, NULL, func, NULL);
    pthread_join(t, NULL);

    jl_atexit_hook(0);
}

Вместо этого выполнение всех вызовов Julia из одного созданного пользователем потока будет работать:

void *func(void*)
{
    // Okay, all jl_...() calls from the same thread,
    // even though it is not the main application thread
    jl_init();
    jl_eval_string("println(Threads.threadid())");
    jl_atexit_hook(0);
    return NULL;
}

int main()
{
    pthread_t t;
    // Create a new thread, which runs func()
    pthread_create(&t, NULL, func, NULL);
    pthread_join(t, NULL);
}

Пример вызова C API Julia из потока, запущенного самой Julia:

#include <julia/julia.h>
JULIA_DEFINE_FAST_TLS

double c_func(int i)
{
    printf("[C %08x] i = %d\n", pthread_self(), i);

    // Call the Julia sqrt() function to compute the square root of i, and return it
    jl_function_t *sqrt = jl_get_function(jl_base_module, "sqrt");
    jl_value_t* arg = jl_box_int32(i);
    double ret = jl_unbox_float64(jl_call1(sqrt, arg));

    return ret;
}

int main()
{
    jl_init();

    // Define a Julia function func() that calls our c_func() defined in C above
    jl_eval_string("func(i) = ccall(:c_func, Float64, (Int32,), i)");

    // Call func() multiple times, using multiple threads to do so
    jl_eval_string("println(Threads.threadpoolsize())");
    jl_eval_string("use(i) = println(\"[J $(Threads.threadid())] i = $(i) -> $(func(i))\")");
    jl_eval_string("Threads.@threads for i in 1:5 use(i) end");

    jl_atexit_hook(0);
}

Если мы запустим этот код с 2 потоками Julia, мы получим следующий вывод (заметьте: вывод будет варьироваться в зависимости от запуска и системы):

$ JULIA_NUM_THREADS=2 ./thread_example
2
[C 3bfd9c00] i = 1
[C 23938640] i = 4
[J 1] i = 1 -> 1.0
[C 3bfd9c00] i = 2
[J 1] i = 2 -> 1.4142135623730951
[C 3bfd9c00] i = 3
[J 2] i = 4 -> 2.0
[C 23938640] i = 5
[J 1] i = 3 -> 1.7320508075688772
[J 2] i = 5 -> 2.23606797749979

Как видно, поток Julia 1 соответствует pthread ID 3bfd9c00, а поток Julia 2 соответствует ID 23938640, что показывает, что действительно используются несколько потоков на уровне C, и что мы можем безопасно вызывать функции C API Julia из этих потоков.