Julia C API 嵌入式开发完全指南:从 C/C++ 调用 Julia 的初始化、类型转换、内存管理与多线程实践
2026/9/19 7:07:02 网站建设 项目流程

Julia C API 嵌入式开发完全指南:从 C/C++ 调用 Julia 的初始化、类型转换、内存管理与多线程实践

【免费下载链接】juliaThe Julia Programming Language项目地址: https://gitcode.com/gh_mirrors/ju/julia

本文以 Julia 官方手册 doc/src/manual/embedding.md 为骨架,结合本仓库的 C 头文件 src/julia.h、构建辅助脚本 contrib/julia-config.jl 与官方示例 test/embedding/embedding.c 等源码,系统讲解如何在 C/C++ 程序中嵌入 Julia 运行时:从最小可运行示例、自动获取编译参数、Windows/Visual Studio 配置,到类型装箱/拆箱、直接调用 Julia 函数、GC 根(rooting)与写屏障(write barrier)、数组零拷贝共享、异常处理与线程安全约束。读完本文,你将能够在自己的 C/C++ 工程中完整集成 Julia,实现"宿主程序为主、Julia 加速/脚本化为辅"的混合架构。

为什么需要在 C 中嵌入 Julia

Julia 通过ccall等机制可以高效调用 C/Fortran 函数(参见 Calling C and Fortran Code),但在实际工程中常常存在反向需求:在 C/C++ 程序中调用 Julia 函数。典型场景包括:

  • 将 Julia 实现的算法(数值计算、数据处理、领域脚本)集成进大型 C/C++ 项目中,而不必用 C/C++ 重写全部逻辑;
  • 借助 Julia 的 C API 搭建语言桥接层,从 Python、Rust、C# 等几乎任何能调用 C 函数的语言反向调用 Julia;
  • 虽然 Rust 与 C++ 可以直接使用 C 嵌入 API,但社区也提供了辅助包(例如 C++ 侧的 Jluna)来降低使用门槛。

Julia 为此提供了完整的 C API,声明集中在头文件 src/julia.h 中,宿主程序通过链接libjulia共享库即可使用。

高级嵌入:第一个最小示例

初始化、执行与退出钩子

以下 C 程序完成了嵌入 Julia 的完整生命周期:初始化运行时 → 执行一段 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; }

三个关键调用的作用与源码依据如下:

  • jl_init():在调用任何其他 Julia C 函数之前必须首先完成运行时初始化。其原型位于 src/julia.h,它会自动探测 Julia 的安装位置。如果需要指定自定义安装位置或指定加载的系统镜像,应改用jl_init_with_image_file(原型见 src/julia.h)或jl_init_with_image_handle(见 src/julia.h)。
  • jl_eval_string("..."):求值一段 Julia 字符串表达式并返回jl_value_t*结果,其声明带JL_CANSAFEPOINT标记(见 src/julia.h),意味着该调用内部可能触发 GC 安全点。
  • jl_atexit_hook(status):在程序终止前调用(见 src/julia.h),让 Julia 有时间清理待处理的写入请求并运行所有 finalizer。示例程序在main返回前调用它并传入退出状态 0。

编译与链接

编译时需把 Julia 头文件目录加入 include 路径,并链接libjulia。假设 Julia 安装于$JULIA_DIR,使用gcc编译示例test.c

gcc -o test -fPIC -I$JULIA_DIR/include/julia -L$JULIA_DIR/lib -Wl,-rpath,$JULIA_DIR/lib test.c -ljulia

-fPIC是位置无关代码选项,-Wl,-rpath用于在运行时定位libjulia。更完整的参考实现位于仓库的 test/embedding/embedding.c,该文件由 test/embedding/Makefile 驱动,展示了从初始化、函数调用、数组共享到异常处理的全套用法,并且演示了在调用jl_init()之前设置jl_options.opt_level = 1来调整编译器优化级别(test/embedding/embedding.c)。另一个简单的参考是 cli/loader_exe.c,它演示了如何设置jl_options后链接libjulia启动 REPL。

两个重要注意事项

动态链接需RTLD_GLOBAL:目前动态加载libjulia共享库必须传入RTLD_GLOBAL选项,否则符号无法被全局解析。在 Python 中形如:

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

导出主可执行文件符号:如果嵌入程序需要让 Julia 访问主可执行文件中的符号,在 Linux 上编译时除了下面julia-config.jl生成的参数外,还可能需要追加-Wl,--export-dynamic链接选项。编译共享库(shared library)时则不需要。

使用 julia-config.jl 自动获取构建参数

手工维护 include 与链接路径既繁琐又易错,Julia 为此提供了脚本julia-config.jl。它由当前 Julia 发行版的构建参数与系统配置驱动,输出嵌入程序与该发行版交互所需的全部编译器标志。该脚本位于 Julia 的共享数据目录(在源码树中对应 contrib/julia-config.jl,安装后位于share/julia/julia-config.jl)。

从源码 contrib/julia-config.jl 可以看到它支持五个选项(options数组定义在第 6–12 行):

选项含义源码依据
--cflags编译标志:-std=gnu11、include 路径(Unix 下追加-fPICcontrib/julia-config.jl
--ldflags链接路径:-L<libdir>(Linux 追加-Wl,--export-dynamic,Windows 追加-Wl,--stack,8388608contrib/julia-config.jl
--ldlibs库列表:Unix 下包含-l<libname>与两个-Wl,-rpath;Windows 下为-l<libname> -lopenlibmcontrib/julia-config.jl
--allflags以上三者的组合contrib/julia-config.jl
--frameworkmacOS Framework 打包模式专用(仅在Base.DARWIN_FRAMEWORK为真时生效)contrib/julia-config.jl

此外脚本会根据是否为 debug 构建自动选择julia/julia-debug库名(contrib/julia-config.jl),并在 macOS Framework 场景下输出-F框架搜索路径。

命令行直接使用

假设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

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

以下命令即可在 Linux 和 Windows(MSYS2 环境)上完成编译;macOS 上将gcc替换为clang

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

在 Makefile 中使用

真实项目的嵌入工程通常更复杂,julia-config.jl同样支持通用 Makefile 集成(以下片段依赖 GNU make 的shell宏展开)。即使julia-config.jl不在/usr/local下,也可以用 Julia 自身定位它:

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即可构建。仓库自带的 test/embedding/Makefile 是更完整的实战模板:它通过$(JULIA) -e 'include(joinpath(Sys.BINDIR, Base.DATAROOTDIR, "julia", "julia-config.jl"))' --调用脚本,并额外注入-lm等标志(test/embedding/Makefile),还区分了release/debug两套构建目标。

在 Windows 上使用 Visual Studio 嵌入

Windows 下的流程与 Unix 略有不同。首先确认JULIA_DIR环境变量已在系统面板中设置,且JULIA_DIR\bin目录位于系统 PATH 中。

  1. 打开 Visual Studio,新建"控制台应用程序"(Console Application)项目;
  2. 打开stdafx.h头文件,在末尾追加:
    #include <julia.h>
  3. 用以下代码替换项目中的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; }
  4. 配置项目查找 Julia 头文件与库。务必先确认 Julia 安装是 32 位还是 64 位,并在继续之前移除与安装位数不符的平台配置;
  5. 在项目属性对话框中:
    • C/C++General→ 将$(JULIA_DIR)\include\julia\加入 Additional Include Directories;
    • LinkerGeneral→ 将$(JULIA_DIR)\lib加入 Additional Library Directories;
    • LinkerInput→ 在库列表中追加libjulia.dll.a;libopenlibm.dll.a;

完成上述配置后项目即可构建运行。

类型转换:装箱(boxing)与拆箱(unboxing)

真实应用不仅需要执行表达式,还需要把结果返回给宿主程序。jl_eval_string返回jl_value_t*——一个指向堆上分配的 Julia 对象的指针。将Float64这类简单数据类型以jl_value_t*形式存储称为装箱(boxing),从中提取原始基本数据称为拆箱(unboxing)

改进后的示例程序在 C 中计算 2 的平方根并读回结果:

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_isajl_typeisjl_is_...系列函数。在 Julia 交互环境中输入typeof(sqrt(2.0))可确认返回类型为Float64(即 C 的double)。jl_typeis(v, t)在头文件中的定义是直接比较jl_typeof(v)与目标类型指针(src/julia.h),而jl_isa则是更一般的"是某类型实例"判断(src/julia.h)。反向转换使用对应的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);

如 src/julia.h 所示,装箱函数(如jl_box_float64)声明带JL_CANSAFEPOINT(可能触发 GC),而拆箱函数(如jl_unbox_float64)声明为JL_NOTSAFEPOINT。正如接下来要看到的,装箱是向 Julia 函数传递参数的必经之路

直接调用 Julia 函数

jl_eval_string只能让 C 获得 Julia 表达式的结果,无法把 C 侧计算出的参数传入 Julia。为此需要使用jl_call系列直接调用 Julia 函数:

jl_value_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取得Base模块中sqrt函数的句柄(第一个参数是指向Base模块的指针,inline 实现见 src/julia.h);再用jl_box_float64将 double 装箱;最后用jl_call1调用。jl_call0jl_call2jl_call3分别处理不同数量的参数(原型见 src/julia.h)。参数更多时使用通用形式:

jl_value_t *jl_call(jl_value_t *f, jl_value_t **args, int32_t nargs)

其中argsjl_value_t*参数数组,nargs是参数个数(实际声明中nargsuint32_t,见 src/julia.h)。注意在 test/embedding/embedding.c 中展示了与此完全一致的jl_get_function+jl_call1用法,并且在 test/embedding/embedding.c 中还演示了从jl_main_module获取用户自定义函数my_func并调用的写法——获取jl_eval_string定义的全局函数时应使用jl_main_module而非jl_base_module

使用 @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);

先在 Julia 中定义一个 C 可调用函数,再用jl_unbox_voidpointer提取函数指针,最后直接调用。除了在高层次语言中简化类型转换外,通过@cfunction指针调用还能消除jl_call所需的动态派发开销(jl_call的所有参数都是装箱的),性能可与原生 C 函数指针相当。test/embedding/embedding.c 与 test/embedding/embedding.c 分别演示了@cfunction的基本用法以及"把原生 C 函数句柄保存后反复调用,并在 Julia 侧重定义函数后再次调用"的进阶技巧。

内存管理:GC 根(GC Rooting)

Julia 对象在 C 中以jl_value_t*指针形式呈现,随之而来的问题是:这些对象由谁负责释放?

通常 Julia 对象由垃圾收集器(GC)释放,但GC 并不知道 C 侧持有某个 Julia 值的引用,因此可能在你还在使用时就回收对象,使指针失效。GC 只会在分配新的 Julia 对象时运行——jl_box_float64这类调用会触发分配,但运行 Julia 代码过程中的任何时刻都可能发生分配。

JL_GC_PUSH 宏

在两次jl_...调用之间安全地使用jl_value_t*通常是没问题的(GC 只会被这些调用触发),但要让值跨越jl_...调用存活,就必须告诉 Julia 你仍持有引用,这一过程称为GC rooting。对值进行 rooting 可确保 GC 不会误判其为无用并释放其底层内存。做法是使用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_PUSH所在的块)之前与JL_GC_POP严格配对

一次可以压入多个值,使用JL_GC_PUSH2JL_GC_PUSH9宏(源码中 1–9 参数版本均声明于 src/julia.h):

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

压入一个数组则用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();

嵌套作用域与 NULL 初始化

每个作用域只能有一个JL_GC_PUSH*调用,并只能配对一个JL_GC_POP。如果需要 root 的变量无法在一次JL_GC_PUSH*中全部压入,或变量数超过 6 且不便使用数组,可以用内层块:

jl_value_t *ret1 = jl_eval_string("sqrt(2.0)"); JL_GC_PUSH1(&ret1); jl_value_t *ret2 = 0; { jl_value_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_GC_PUSH*之前并不要求指针已持有合法值。把多个指针初始化为NULL再压入、随后再创建真正的 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*(它依赖 C 栈)。此时必须在 Julia 全局作用域中创建并持有该变量的引用。一种简单的方式是使用全局IdDict保存引用,避免 GC 回收——但该方法仅对可变类型(mutable types)有效

// This functions shall be executed only once, during the initialization. jl_value_t* refs = jl_eval_string("refs = IdDict()"); jl_value_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_value_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 回收某变量时,用delete!refs中删除其引用(前提是没有其他引用):

jl_value_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);

更新 GC 管理对象的字段:写屏障(Write Barrier)

GC 还假定它了解每个老一代对象指向新一代对象的引用关系。任何打破该假设的指针更新都必须通过写屏障通知收集器。

首选jl_gc_write宏,它以正确顺序同时完成屏障与存储:

jl_value_t *parent = some_old_value, *child = some_young_value; jl_gc_write(parent, ((some_specific_type*)parent)->field, jl_value_t, child); jl_gc_write_atomic(parent, ((some_specific_type*)parent)->atomic_field, jl_value_t, child, release);

其中type是字段指向的类型;原子变体的最后一个参数是内存序(relaxedrelease)。宏的实现可参见 src/julia.h:它先读出即将被覆盖的旧值,再执行jl_gc_wb屏障,最后完成赋值。

仅当更新不是单一赋值(如比较并交换、跨越多个字段的批量拷贝,或目标不是普通左值)时,才直接使用底层的jl_gc_wb函数,并且必须在存储之前发出,因为收集器可能需要读取即将被覆盖的引用:

jl_value_t *parent = some_old_value, *child = some_young_value; jl_gc_wb(parent, child); ((some_specific_type*)parent)->field = child;

由于运行时无法预测哪个值会成为老对象,所有显式存储都需要写屏障;唯一例外是parent对象刚刚分配且之后没有运行过 GC。注意大多数jl_...函数有时会触发 GC

直接更新指针数组的数据时同样需要写屏障。通常更推荐调用jl_array_ptr_set(其内部通过jl_gc_write_atomic完成屏障与存储,见 src/julia.h),但也可以直接更新:

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

控制垃圾收集器

以下函数用于显式控制 GC,普通场景下通常不需要:

函数说明
jl_gc_collect(JL_GC_FULL)强制对所有对象执行一次 GC
jl_gc_collect(JL_GC_INCREMENTAL)仅对新生代对象强制执行 GC
jl_gc_collect(JL_GC_AUTO)强制执行 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*表示,其结构基本包含:数据类型信息、指向数据块的指针、数组尺寸信息。

先从最简单的 1D 数组开始。创建一个包含 10 个Float64元素的数组:

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);

jl_apply_array_typejl_alloc_array_1d的声明分别在 src/julia.h 与 src/julia.h。如果你已经分配好数组,可以生成一个薄包装(thin wrapper)包裹现有数据:

double *existingArray = (double*)malloc(sizeof(double)*10); jl_array_t *x = jl_ptr_to_array_1d(array_type, existingArray, 10, 0);

最后一个参数是布尔值,表示 Julia 是否接管数据所有权:若非零,数组不再被引用时 GC 会对数据指针调用freejl_ptr_to_array_1d原型见 src/julia.h)。

访问x的数据使用jl_array_data宏(其实现直接读取数组内部的数据指针字段,见 src/julia.h):

double *xData = jl_array_data(x, double);

填充数组:

for (size_t i = 0; i < jl_array_nrows(x); i++) xData[i] = i;

然后调用一个对x进行原地操作的 Julia 函数:

jl_value_t *func = jl_get_function(jl_base_module, "reverse!"); jl_call1(func, (jl_value_t*)x);

打印数组即可验证元素已被反转。test/embedding/embedding.c 完整演示了 1D 数组的创建、JL_GC_PUSH1rooting、填充、调用reverse!与打印,其中特别强调:数组使用期间必须保持其被 root

访问返回的数组

如果 Julia 函数返回数组,jl_eval_string/jl_call的返回值可以强转为jl_array_t*

jl_value_t *func = jl_get_function(jl_base_module, "reverse"); jl_array_t *y = (jl_array_t*)jl_call1(func, (jl_value_t*)x);

之后照常通过jl_array_data访问y的内容。老规矩:数组使用期间务必持有其引用。

多维数组

Julia 的多维数组在内存中按**列优先(column-major)**顺序存储。以下代码创建 2D 数组并访问其属性:

// 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 为了读起来更像地道 C 代码而使用0 基索引(例如jl_array_dim的参数)。相关宏/函数定义可查 src/julia.h(jl_array_dimjl_array_nrowsjl_array_ndims)与 src/julia.h(jl_alloc_array_nd)。

异常处理

Julia 代码可以抛出异常。例如:

jl_eval_string("this_function_does_not_exist()");

这个调用看起来什么都没发生,但可以检查是否有异常被抛出:

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

jl_exception_occurred返回当前待处理异常(无异常时返回 NULL,见 src/julia.h)。如果从支持异常的语言(如 Python、C#、C++)使用 Julia C API,合理的做法是把每个libjulia调用包进一个检查jl_exception_occurred的包装函数,再在宿主语言中重新抛出异常。test/embedding/embedding.c 中的checked_eval_string正是这种封装:异常时调用showerror打印到 stderr,并带错误码调用jl_atexit_hook(1)exit(1);test/embedding/embedding.c 还展示了用JL_TRY/JL_CATCH宏在 C 侧捕获异常并优雅处理的写法。

抛出 Julia 异常

编写 Julia 可调用函数时,可能需要校验参数并用异常指示错误。典型的类型检查如下:

if (!jl_typeis(val, jl_float64_type)) { jl_type_error(function_name, (jl_value_t*)jl_float64_type, val); }

jl_type_error声明为JL_NORETURN(见 src/julia.h)。通用异常可用以下函数抛出:

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)

总体而言,Julia C API 并非完全线程安全。在多线程应用中嵌入 Julia 时,必须遵守以下限制:

  • jl_init()在应用生命周期内只能调用一次;jl_atexit_hook()同理,且只能在jl_init()之后调用;
  • jl_...()API 函数只能从调用jl_init()的线程调用,或从 Julia 运行时启动的线程调用。从用户自行创建的线程调用 Julia API 不受支持,可能导致未定义行为乃至崩溃。

第二条意味着不能安全地从非 Julia 启动的线程(调用jl_init()的线程除外)调用jl_...()函数。例如以下代码不受支持,很可能段错误:

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

从 Julia 启动的线程中调用 C API

从 Julia 自身启动的线程调用 C API 是被支持的。下面的示例定义了一个 C 函数c_func,它内部调用 Julia 的sqrt;再通过ccall让 Julia 的@threads并行循环反向调用它:

#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_value_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 ID3bfd9c00,线程 2 对应23938640——C 层确实使用了多个线程,且可以安全地从这些线程调用 Julia C API 例程。

更进一步:仓库中的完整参考实现

除了本文逐步讲解的 API 之外,仓库还提供了可直接运行的完整示例,建议读者结合源码深入:

  • test/embedding/embedding.c:覆盖本文几乎所有主题的端到端示例——选项设置、字符串求值、返回值拆箱、函数句柄调用、@cfunction、1D 数组共享、自定义函数定义与调用、ccall反向调用 C 函数、异常处理、JL_TRY/JL_CATCH
  • test/embedding/Makefile:展示如何通过julia-config.jl自动化构建嵌入程序,并提供了make check-embedding测试目标(配合 test/embedding/embedding-test.jl 验证);
  • test/embedding/asan-embedding.c:在 AddressSanitizer 环境下的嵌入测试,用于验证嵌入宿主程序的内存安全;
  • cli/loader_exe.c:Julia 官方 CLI 加载器,演示如何设置jl_options后链接libjulia
  • contrib/julia-config.jl:自动生成编译/链接参数的脚本本体,其--allflags--framework等选项可满足更复杂的构建需求。

围绕上述 API 的声明与宏定义均位于 src/julia.h,包括初始化(L2406-L2412)、求值(L2471)、GC 根宏(L1229-L1287)、写屏障(L1350-L1416)、类型检查与异常(L197、L2030、L2332)、函数调用(L2536-L2542)以及数组与装箱/拆箱接口(L2136-L2211),是编写嵌入代码时最值得查阅的权威参考。

【免费下载链接】juliaThe Julia Programming Language项目地址: https://gitcode.com/gh_mirrors/ju/julia

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询