桥内部¶
8 个 C 文件、4 个职责组、1 份契约。用户侧页面从外面描述桥——fcj_*
ABI、两层 Fortran SDK、那些数字。本页打开盒子:每个文件拥有什么、GC 的账
怎么还、Julia 1.12 的 C API 在哪里逼出了绕行方案。它是写给改
src/bridge/ 的维护者的;要使用桥,不需要这里任何内容。
架构页用一屏讲同一个系统;如果你是初来,先读 它。
八个文件¶
| 组 | 文件 | 拥有 |
|---|---|---|
| 核心 | core.c、memory.c |
生命周期(fcj_init / fcj_init_ex / fcj_shutdown)、线程钉死、GC 根集 |
| 分发与装箱 | dispatch.c、types.c |
fcj_call* / fcj_call_kwargs、函数名缓存、标量装箱(fcj_box_*) |
| 数组通道 | arrays.c、extract.c |
N 维零拷贝包装、深拷贝、批量取数回 Fortran |
| 回调访问 | callbacks.c、accessors.c |
@cfunction 蹦床、get_field / get_property、原生 Float64 元素访问 |
两个头文件给它们定框。bridge.h 是公共契约:53 个 fcj_ 函数加
fcj_options_t——Fortran bind(c) 层能碰到的一切。bridge_internal.h
是私有的:原生元素访问器(fcj_get_real 及其同族)、共用助手、以及
fcj_shutdown 逐一走过的各模块清缓存钩子。全站沿用的”约 105 个导出
fcj_* 符号”这一口径数的是这些源码里整个 fcj_ 命名空间;绑定层
src/julia_binding.f90 声明其中 58 个(53 个公共函数中的 52 个——
fcj_ready 从不被绑定——加 6 个内部元素访问器),其余是从不过 ABI 的内
部助手。
核心:生命周期与根集¶
core.c 拥有状态机。fcj_state 是 _Atomic;每个入口先经 fcj_ready()
检查它才碰 Julia。已知限制里的线程规则在
check_thread() 里是被执行、不是被记录(逐字摘自 src/bridge/core.c):
int check_thread() {
if (pthread_self() != main_thread_id) {
char msg[256];
snprintf(msg, sizeof(msg), "Thread Error: Julia called from thread %p, runtime owned by thread %p",
(void*)pthread_self(), (void*)main_thread_id);
set_error(msg);
return 0; // Fail
}
return 1; // OK
}
失败语义按入口分家:fcj_init 失败即终止进程(原始契约),而
fcj_init_ex 返回错误码、原因留在 fcj_get_last_error()——Fortran 侧以
get_last_error_message() 读到。fcj_shutdown 是单向的:状态机永不回到
READY,shutdown 之后重新初始化会被响亮拒绝,而不是被尝试。
memory.c 是 GC 的故事,它的头注释比任何转述都说得清楚(摘自
src/bridge/memory.c):
// fcj_retain/fcj_release keep their bind(C) signatures but no longer round-trip
// through Julia (retain_obj/release_obj in GCManager.jl) on the hot path.
// Retained objects live in a Julia Vector{Any} reachable through the constant
// binding `Main.FJ_ROOTS` ... bookkeeping is a pure-C ptr -> slot hash map with
// per-pointer refcounts, mirroring GCManager's REF_COUNTS/PTR_CACHE logic.
维护者要记住的机制:
- C static 不是 GC 根。 任何缓存进
static的函数或值必须走fcj_retain(模块绑定算数;裸 static 不算)。唯一被允许的例外是被 Base 绑定保活的不朽单例,例如types.c缓存的ComplexF64类型对象。 fcj_map是开放寻址加墓碑与引用计数——被删条目打墓碑使探测链保 持完整;同一指针 retain 两次占一个槽、计数为二。- 这些 static 一处锁都没有。 单线程契约(
check_thread)就是锁。 破坏契约你得到的是数据竞争,不是一个 bug 报告。
分发与装箱¶
dispatch.c 把名字变成调用。函数名缓存是 256 槽定长表、FNV-1a 哈希;
缓存命中经 fcj_retain 路由,缓存住的 jl_function_t* 才有根。维护者
永远不能撤销的两条失效规则:
- 该缓存在
fcj_shutdown中最先被清——趁还没有任何东西能再解引用 它。 - 每次
julia_eval_string都使名字缓存失效。 一次 eval 可以把f从函数重定义成闭包,过期缓存项会永远调用捕获的旧对象。(Fast Path 注 册表刻意保留其捕获对象语义并写明了;见 两套 SDK,两种速度。)
fcj_call1_float64 一族到 N 参版本、再到关键字参数路径 fcj_call_kwargs,
全部汇入同一条 jl_call 加异常检查的尾巴。types.c 把 Fortran 标量装
箱成 Julia 对象——fcj_box_float64、fcj_box_string 等——外加上述缓存
的 ComplexF64 数据类型。
数组通道¶
arrays.c 经 jl_ptr_to_array 把 Fortran 内存包装成 1 到 FCJ_MAX_NDIM
任意秩的 Julia Array,零拷贝。包装路径逐字摘自 src/bridge/arrays.c
(三个守卫助手同形,为篇幅略去):
void* fcj_wrap_array(void* data, long ndim, const long* dims) {
if (!fcj_ready()) { set_error("Julia runtime not initialized or already shut down"); return NULL; }
if (!check_thread()) return NULL;
jl_value_t *array_type = NULL;
jl_value_t *dims_tuple = NULL;
JL_GC_PUSH2(&array_type, &dims_tuple);
array_type = array_type_for_ndim(ndim, "fcj_wrap_array");
...
dims_tuple = make_dims_tuple(ndim, dims, "fcj_wrap_array");
...
jl_array_t *arr = jl_ptr_to_array(array_type, data, dims_tuple, 0);
if (check_julia_exception()) { JL_GC_POP(); return NULL; }
JL_GC_POP();
return (void*)arr;
}
这段代码里看得见两条纪律,全桥处处负载:局部值在调用期间用
JL_GC_PUSH2 扎根;任何错误路径都先 set_error 原因再返回 NULL。深拷贝
兄弟 fcj_copy_array 分配全新 Julia 数组并 memcpy 进去,分配前做溢出检
查的规模乘积。
extract.c 是回程:fcj_to_fortran(一次批量拷进 Fortran 缓冲、长度校
验)、fcj_get_element、fcj_get_property。批量路径是从
jl_array_data_ 出发、带 n == len 检查的单次 memcpy——正因如此取数
通道是位级精确的,验收门才敢把 1e-8 的门摆在它面前
(验收门)。
回调访问¶
callbacks.c 跑另一个方向:Fortran bind(c) 子程序经 @cfunction 蹦床
包装成 Julia 可调用对象,调用上下文不透明地携带。这是
examples/06_sciml/1_callback.f90 那些 OrdinaryDiffEq 回调演示背后的机
器。用户页的硬规则在这一层同样成立:蹦床帧容不下 Julia 异常,会抛错的被
调方属未定义行为——在 Fortran 侧校验、返回哨兵值。
accessors.c 实现 %element 与 %property,而 Julia 1.12 的 C API 在
这里逼出了全树最有教益的绕行。extract.c 的头注释记录在案(摘自
src/bridge/extract.c):
// 1.12.7 API notes (verified against include/julia/julia.h):
// - jl_get_property does not exist; jl_get_field exists but only reads
// plain struct fields (no getproperty overloads, no NamedTuples), so
// the contract's fallback applies: Base.getproperty is cached once and
// jl_call-ed, matching helper_getfield semantics in Accessors.jl;
// getproperty dispatches on Symbol, so the name is interned with
// jl_symbol (immortal, globally rooted) instead of boxed as a String;
// - jl_arrayref does not exist either (removed with the Memory-backed
// array rewrite), so element boxing goes through a cached
// Base.getindex binding -- the same numeric boxing jl_arrayref used to
// perform.
把它读成通用教训:当某个 jl_* 便捷函数缺席时,把等价的 Base 函数缓存
一次(经 fcj_retain 扎根)再 jl_call 它——不要手搓结构体偏移。
bridge_internal.h 补上原生元素访问器(fcj_get_real / fcj_set_real
及 2 维、3 维形态):对包装内存的直接 Float64 读写,不装箱,供热路径
逐格使用。
信号与错误传输¶
桥永不写 jl_options.handle_signals:嵌入式 Julia 的 GC safepoint 轮询依
赖 Julia 自己的信号处理器,关掉它就会在 safepoint 崩溃——是必崩,不是偶
发(教训)。fcj_options_t 提供的 handle_signals = 0 含
义是不接管:init 成功后桥把 SIGINT / SIGTERM 重置为 SIG_DFL,
GC 关键的处理器原封不动。
错误走一条通道:每个运行时一个 last-error 缓冲,经 fcj_get_last_error()
读出、以 get_last_error_message() 露面。Julia 侧异常在调用边界被接住、
格式化进该缓冲,调用返回无效句柄或 NULL——进程继续跑。