
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 中嵌入 JuliaJulia 通过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.cgcc -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 中形如 juliaCDLL(./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编译标志-stdgnu11、include 路径Unix 下追加-fPICcontrib/julia-config.jl--ldflags链接路径-LlibdirLinux 追加-Wl,--export-dynamicWindows 追加-Wl,--stack,8388608contrib/julia-config.jl--ldlibs库列表Unix 下包含-llibname与两个-Wl,-rpathWindows 下为-llibname -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 和 WindowsMSYS2 环境上完成编译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 中。打开 Visual Studio新建控制台应用程序Console Application项目打开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 位并在继续之前移除与安装位数不符的平台配置在项目属性对话框中C/C→General→ 将$(JULIA_DIR)\include\julia\加入 Additional Include DirectoriesLinker→General→ 将$(JULIA_DIR)\lib加入 Additional Library DirectoriesLinker→Input→ 在库列表中追加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_isa、jl_typeis或jl_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_call0、jl_call2、jl_call3分别处理不同数量的参数原型见 src/julia.h。参数更多时使用通用形式jl_value_t *jl_call(jl_value_t *f, jl_value_t **args, int32_t nargs)其中args是jl_value_t*参数数组nargs是参数个数实际声明中nargs为uint32_t见 src/julia.h。注意在 test/embedding/embedding.c 中展示了与此完全一致的jl_get_functionjl_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 RootingJulia 对象在 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_PUSH2到JL_GC_PUSH9宏源码中 1–9 参数版本均声明于 src/julia.hJL_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 BarrierGC 还假定它了解每个老一代对象指向新一代对象的引用关系。任何打破该假设的指针更新都必须通过写屏障通知收集器。首选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是字段指向的类型原子变体的最后一个参数是内存序relaxed或release。宏的实现可参见 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)强制对所有对象执行一次 GCjl_gc_collect(JL_GC_INCREMENTAL)仅对新生代对象强制执行 GCjl_gc_collect(JL_GC_AUTO)强制执行 GC自动在全量与增量之间选择jl_gc_enable(0)禁用 GC返回先前状态intjl_gc_enable(1)启用 GC返回先前状态intjl_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_type与jl_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.hdouble *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 i0; isize1; i) for(size_t j0; jsize0; j) p[j size0*i] i j;注意虽然 Julia 数组使用 1 基索引但 C API 为了读起来更像地道 C 代码而使用0 基索引例如jl_array_dim的参数。相关宏/函数定义可查 src/julia.hjl_array_dim、jl_array_nrows、jl_array_ndims与 src/julia.hjl_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的用法类似printfjl_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_THREADS2 ./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_CATCHtest/embedding/Makefile展示如何通过julia-config.jl自动化构建嵌入程序并提供了make check-embedding测试目标配合 test/embedding/embedding-test.jl 验证test/embedding/asan-embedding.c在 AddressSanitizer 环境下的嵌入测试用于验证嵌入宿主程序的内存安全cli/loader_exe.cJulia 官方 CLI 加载器演示如何设置jl_options后链接libjuliacontrib/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),仅供参考