Julia 教程 / C/Fortran 互操作(ccall)
C/Fortran 互操作(ccall)
Julia 的 ccall 机制允许直接调用 C 和 Fortran 编写的共享库函数,无需编写胶水代码。这是 Julia 高性能生态的重要基础——大量底层科学计算库(BLAS、LAPACK、FFTW)都通过此机制调用。
1. ccall 语法详解
1.1 基本语法
# ccall 的标准形式
return_value = ccall(
(:function_name, "library"), # 函数名 + 库名
ReturnType, # 返回类型
(ArgType1, ArgType2, ...), # 参数类型元组
arg1, arg2, ... # 实际参数
)
1.2 最小示例
# 调用 C 标准库的 clock() 函数
ticks = ccall(:clock, Int32, ())
println("时钟周期: $ticks")
# 调用 strlen
len = ccall(:strlen, Csize_t, (Cstring,), "hello")
println("字符串长度: $len")
1.3 库名的多种指定方式
# 方式一:符号(已加载的库)
ccall(:func_name, ...)
# 方式二:元组(函数名, 库名)
ccall((:func_name, "libname"), ...)
# 方式三:完整路径
ccall((:func_name, "/path/to/lib.so"), ...)
# 方式四:使用 Libdl 动态加载
using Libdl
lib = Libdl.dlopen("libexample")
func = Libdl.dlsym(lib, :my_function)
ccall(func, Cint, (Cint,), 42)
Libdl.dlclose(lib)
2. C 类型映射表
2.1 基本类型映射
| C 类型 | Julia 类型 | 大小 | 说明 |
|---|---|---|---|
void | Cvoid | 0 | 返回类型用 |
char | Cchar | 1 | 有符号字符 |
unsigned char | Cuchar | 1 | 无符号字符 |
short | Cshort | 2 | |
unsigned short | Cushort | 2 | |
int | Cint | 4 | |
unsigned int | Cuint | 4 | |
long | Clong | 4/8 | 平台相关 |
unsigned long | Culong | 4/8 | 平台相关 |
long long | Clonglong | 8 | |
float | Cfloat | 4 | |
double | Cdouble | 8 | |
char* / const char* | Cstring / Cstring | 8 | 自动转换 |
void* | Ptr{Cvoid} | 8 | 通用指针 |
size_t | Csize_t | 8 | 64位系统 |
int8_t | Int8 | 1 | |
int16_t | Int16 | 2 | |
int32_t | Int32 | 4 | |
int64_t | Int64 | 8 | |
bool (_Bool) | Cuchar | 1 | C99 布尔 |
2.2 指针类型映射
| C 类型 | Julia 类型 | 说明 |
|---|---|---|
int* | Ptr{Cint} | 整数指针 |
double* | Ptr{Cdouble} | 浮点指针 |
char** | Ptr{Ptr{Cchar}} | 字符串数组 |
void* | Ptr{Cvoid} | 通用指针 |
const int* | Ptr{Cint} | 常量指针(Julia 无 const 区分) |
struct Foo* | Ptr{Foo} | 结构体指针 |
⚠️ 注意:Julia 中没有
const指针语义。传递Ptr{Cdouble}时,C 函数可能修改内存内容。确保不写入不该修改的区域。
3. 调用共享库
3.1 调用 libc 函数
# 数学函数
result = ccall(:sin, Cdouble, (Cdouble,), 3.14159)
println("sin(π) ≈ $result")
# 内存操作
buf = ccall(:malloc, Ptr{Cvoid}, (Csize_t,), 1024)
ccall(:memset, Ptr{Cvoid}, (Ptr{Cvoid}, Cint, Csize_t), buf, 0, 1024)
ccall(:free, Cvoid, (Ptr{Cvoid,), buf)
# 获取环境变量
val = ccall(:getenv, Cstring, (Cstring,), "HOME")
if val != C_NULL
println("HOME = ", unsafe_string(val))
end
3.2 调用 libm 函数
# Julia 已经包装了大多数 libm 函数,这里演示自定义调用
hypot_result = ccall(:hypot, Cdouble, (Cdouble, Cdouble), 3.0, 4.0)
println("hypot(3, 4) = $hypot_result") # 5.0
3.3 调用自定义共享库
假设我们有一个 C 库 libmathutils:
// mathutils.c
#include <math.h>
typedef struct {
double x;
double y;
} Point;
double distance(Point* p1, Point* p2) {
double dx = p1->x - p2->x;
double dy = p1->y - p2->y;
return sqrt(dx*dx + dy*dy);
}
void scale_array(double* arr, int n, double factor) {
for (int i = 0; i < n; i++) {
arr[i] *= factor;
}
}
编译为共享库:
gcc -shared -fPIC -o libmathutils.so mathutils.c -lm
在 Julia 中调用:
# 定义对应的 Julia 结构体
struct Point
x::Cdouble
y::Cdouble
end
# 调用 distance 函数
p1 = Point(1.0, 2.0)
p2 = Point(4.0, 6.0)
d = ccall((:distance, "libmathutils"), Cdouble, (Ref{Point}, Ref{Point}), p1, p2)
println("距离: $d") # 5.0
4. 传递数组
4.1 传递 Julia 数组给 C
# Julia 数组的内存布局与 C 兼容
arr = [1.0, 2.0, 3.0, 4.0, 5.0]
# 方式一:使用 pointer()
ccall((:scale_array, "libmathutils"), Cvoid,
(Ptr{Cdouble}, Cint, Cdouble),
pointer(arr), length(arr), 2.0)
println(arr) # [2.0, 4.0, 6.0, 8.0, 10.0]
# 方式二:使用 Ref(更安全,防止数组被 GC 移动)
arr2 = [1.0, 2.0, 3.0]
ccall((:scale_array, "libmathutils"), Cvoid,
(Ref{Cdouble}, Cint, Cdouble),
arr2, length(arr2), 3.0)
4.2 从 C 返回数组
# 方式一:预先分配 Julia 数组,传指针给 C 填充
function fill_array_from_c(n::Int)
result = Vector{Cdouble}(undef, n)
ccall((:fill_array, "libexample"), Cvoid,
(Ptr{Cdouble}, Cint), result, n)
return result
end
# 方式二:C 分配内存,Julia 复制后释放
function create_array_from_c(n::Int)
ptr = ccall((:create_array, "libexample"), Ptr{Cdouble}, (Cint,), n)
arr = unsafe_wrap(Array, ptr, n; own=false)
result = copy(arr) # 复制到 Julia 管理的内存
ccall(:free, Cvoid, (Ptr{Cvoid},), ptr) # 释放 C 分配的内存
return result
end
💡 提示:尽量在 Julia 端分配数组,让 C 函数填充数据。这样 Julia 的 GC 负责内存管理,避免内存泄漏。
5. 传递字符串
5.1 字符串传递
# Julia String → C const char*
# 使用 Cstring 类型,Julia 自动处理编码和终止符
len = ccall(:strlen, Csize_t, (Cstring,), "Hello, World!")
# 从 C 获取字符串
function get_c_string()
ptr = ccall(:getenv, Cstring, (Cstring,), "PATH")
if ptr == C_NULL
return nothing
end
return unsafe_string(ptr) # 复制到 Julia String
end
5.2 字符串数组
# 传递字符串数组给 C (char**)
function list_to_c_strings(strings::Vector{String})
# 创建 C 风格字符串数组
c_strings = [Base.unsafe_convert(Cstring, s) for s in strings]
result = ccall((:process_strings, "libexample"), Cint,
(Ptr{Cstring}, Cint), c_strings, length(strings))
# 注意:不需要释放 c_strings,Julia 管理内存
return result
end
5.3 修改 C 传来的字符串
# 当 C 返回 malloc 分配的字符串时
function get_allocated_string()
ptr = ccall((:create_string, "libexample"), Cstring, ())
if ptr == C_NULL
error("C 函数返回 NULL")
end
result = unsafe_string(ptr) # 复制内容
ccall(:free, Cvoid, (Ptr{Cvoid},), ptr) # 释放原始内存
return result
end
6. 传递结构体
6.1 基本结构体映射
// C 结构体
typedef struct {
double latitude;
double longitude;
int altitude;
char name[64];
} GeoPoint;
# Julia 对应结构体(字段顺序必须完全一致!)
struct GeoPoint
latitude::Cdouble
longitude::Cdouble
altitude::Cint
name::NTuple{64, Cchar} # 定长字符数组
end
# 创建实例
function make_geopoint(lat, lon, alt, name)
name_arr = ntuple(i -> i <= length(name) ? Cchar(name[i]) : Cchar(0), 64)
return GeoPoint(Cdouble(lat), Cdouble(lon), Cint(alt), name_arr)
end
6.2 使用 @kwdef 简化
# 使用关键字参数构造
Base.@kwdef struct GeoPoint
latitude::Cdouble = 0.0
longitude::Cdouble = 0.0
altitude::Cint = 0
name::NTuple{64, Cchar} = ntuple(_ -> Cchar(0), 64)
end
point = GeoPoint(latitude=39.9, longitude=116.4, altitude=50)
6.3 填充(Packed)结构体
# ⚠️ 如果 C 使用 #pragma pack(1),Julia 需要对应调整
# 默认情况下 Julia 不填充,与大多数 C 编译器一致
# 如果需要 packed 布局,手动计算偏移或使用 @static if
struct PackedStruct
a::UInt8
b::UInt32 # 在 packed 模式下紧跟 a
end
# 验证大小
@assert sizeof(PackedStruct) == 5 # 如果是 packed 的话
7. 回调函数(C 调用 Julia)
7.1 基本回调
// C 端定义回调接口
typedef int (*Callback)(int);
int apply_callback(int* arr, int n, Callback cb) {
int sum = 0;
for (int i = 0; i < n; i++) {
sum += cb(arr[i]);
}
return sum;
}
# Julia 端定义回调函数
function my_callback(x::Cint)::Cint
return x * x
end
# 将 Julia 函数转为 C 回调指针
const CCallback = Cvoid # 或者定义为函数指针类型
function apply_with_callback(arr::Vector{Cint})
# 使用 @cfunction 创建 C 兼容的函数指针
cb = @cfunction(my_callback, Cint, (Cint,))
result = ccall((:apply_callback, "libexample"), Cint,
(Ptr{Cint}, Cint, Ptr{Cvoid}),
arr, length(arr), cb)
return result
end
7.2 闭包回调
# C 函数指针不能直接对应 Julia 闭包
# 需要通过全局状态或用户数据指针传递上下文
# 方案一:使用全局变量
const _callback_state = Ref{Any}(nothing)
function _c_callback(x::Cint)::Cint
f = _callback_state[]
return Cint(f(x))
end
function apply_closure(f, arr::Vector{Cint})
_callback_state[] = f
cb = @cfunction(_c_callback, Cint, (Cint,))
return ccall((:apply_callback, "libexample"), Cint,
(Ptr{Cint}, Cint, Ptr{Cvoid}),
arr, length(arr), cb)
end
# 使用
result = apply_closure(x -> x + 1, Cint[1, 2, 3, 4, 5])
⚠️ 注意:回调函数中不能抛出异常到 C 代码,否则会导致未定义行为。始终在回调中捕获所有异常。
8. 内存管理
8.1 GC 与 C 交互
# Julia 的 GC 可能在任何时候运行
# 传递给 C 的数据可能被移动(如果 GC 触发压缩)
# 安全的方式:使用 GC.@preserve
arr = [1.0, 2.0, 3.0]
GC.@preserve arr begin
# 在这个代码块中,arr 不会被 GC 移动或回收
ptr = pointer(arr)
ccall((:process, "libexample"), Cvoid, (Ptr{Cdouble}, Cint), ptr, length(arr))
end
8.2 unsafe_wrap 与内存归属
# 让 Julia 数组包装已有的 C 内存
function get_c_array()
ptr = ccall((:create_array, "libexample"), Ptr{Cdouble}, (Cint,), 100)
# own=false: Julia 不负责释放内存
arr = unsafe_wrap(Array, ptr, 100; own=false)
# 使用完毕后手动释放
result = copy(arr)
ccall(:free, Cvoid, (Ptr{Cvoid},), ptr)
return result
end
# own=true: Julia 的 GC 最终会调用 free
# ⚠️ 只在 C 使用 malloc 分配的内存时使用
8.3 防止内存泄漏
# 使用 try-finally 确保释放
function safe_ccall_example()
buf = ccall(:malloc, Ptr{Cvoid}, (Csize_t,), 4096)
try
# 使用 buf
ccall((:process_buffer, "libexample"), Cvoid, (Ptr{Cvoid}, Csize_t), buf, 4096)
finally
ccall(:free, Cvoid, (Ptr{Cvoid},), buf)
end
end
9. 自动生成绑定 (Bindgen)
9.1 Clang.jl 自动绑定
using Clang
# 从 C 头文件自动生成 Julia 绑定
# 首先创建配置
ctx = create_context(
["include/mylib.h"], # 头文件列表
["-I/usr/include", "-I./include"], # 编译选项
)
# 生成绑定代码
run(ctx)
9.2 Clang.jl 配置文件
# generator.toml 配置示例
[general]
library_name = "libexample"
output_file_path = "./src/LibExample.jl"
module_name = "LibExample"
jll_pkg_name = "Example_jll"
[general.clang_args]
extra_args = ["-I./include"]
[api.function]
argument_filter = ["ctx", "data"]
9.3 使用 JLL 包
# Julia 二进制构建器 (BinaryBuilder.jl) 创建的 JLL 包
using Example_jll
# JLL 包自动管理共享库的下载和路径
result = ccall((:my_function, libexample), Cint, (Cint,), 42)
# libexample 变量由 JLL 包自动提供
10. Fortran 互操作
10.1 调用 Fortran 子程序
! fortran_math.f90
subroutine scale_vector(x, n, factor)
implicit none
integer, intent(in) :: n
double precision, intent(inout) :: x(n)
double precision, intent(in) :: factor
integer :: i
do i = 1, n
x(i) = x(i) * factor
end do
end subroutine
# 编译: gfortran -shared -fPIC -o libfmath.so fortran_math.f90
# Fortran 传递所有参数通过引用
arr = [1.0, 2.0, 3.0, 4.0]
n = Cint(length(arr))
factor = 2.0
ccall((:scale_vector_, "libfmath"), Cvoid,
(Ref{Cdouble}, Ref{Cint}, Ref{Cdouble}),
arr, n, factor)
println(arr) # [2.0, 4.0, 6.0, 8.0]
⚠️ 注意:Fortran 编译器通常会在函数名后追加下划线
_。检查实际符号名:nm -D libfmath.so
10.2 BLAS/LAPACK 调用示例
# Julia 已内置 BLAS 调用,但有时需要直接调用
using LinearAlgebra
# 直接调用 BLAS dgemm
m, n, k = 100, 100, 100
A = randn(m, k)
B = randn(k, n)
C = zeros(m, n)
ccall((:dgemm_, LinearAlgebra.BLAS.libblastrampoline), Cvoid,
(Ref{UInt8}, Ref{UInt8}, Ref{Cint}, Ref{Cint}, Ref{Cint},
Ref{Cdouble}, Ptr{Cdouble}, Ref{Cint},
Ptr{Cdouble}, Ref{Cint},
Ref{Cdouble}, Ptr{Cdouble}, Ref{Cint}),
'N', 'N', m, n, k, 1.0, A, m, B, k, 0.0, C, m)
常见陷阱与解决方案
| 陷阱 | 表现 | 解决方案 |
|---|---|---|
| 类型不匹配 | 崩溃或错误结果 | 严格对照 C 类型映射表 |
| 字符串编码 | 非 ASCII 内容损坏 | 确保 UTF-8 编码一致 |
| 内存泄漏 | 内存持续增长 | 使用 try-finally 释放 |
| GC 移动对象 | 段错误 | 使用 GC.@preserve |
| 结构体对齐 | 字段偏移错误 | 使用 fieldoffset 验证 |
| Fortran 下划线 | 符号未找到 | 检查 nm 输出 |
| NULL 指针 | 段错误 | 检查返回值是否为 C_NULL |
| 回调异常 | 程序终止 | 回调中 try-catch |
业务场景
场景一:调用遗留 C 库
公司有一个核心算法的 C 库,需要用 Julia 包装。通过 Clang.jl 自动从头文件生成绑定代码,配合 BinaryBuilder.jl 打包为 JLL 包,实现了跨平台分发。
场景二:高性能图像处理
调用 OpenCV 的 C++ 接口进行图像处理。通过 C 包装层(extern “C”)暴露函数,Julia 端使用 ccall 调用,比纯 Julia 实现快 5 倍(针对特定 SIMD 优化的算法)。
场景三:调用 Fortran 数值库
调用一个经典的 Fortran 数值积分库。通过 ccall 直接调用,注意了 Fortran 的引用传递和名称修饰规则,成功集成到 Julia 的数值计算流程中。
总结
| 主题 | 关键要点 |
|---|---|
| 基本语法 | ccall((:func, "lib"), RetType, (ArgTypes...), args...) |
| 类型映射 | 使用 Cint/Cdouble/Cstring 等 C 兼容类型 |
| 数组传递 | 使用 pointer() 或 Ref{},配合 GC.@preserve |
| 回调函数 | 使用 @cfunction 创建 C 兼容函数指针 |
| 内存管理 | 明确所有权,使用 try-finally 防泄漏 |
| 自动绑定 | Clang.jl + BinaryBuilder.jl 自动生成并打包 |
| Fortran | 参数全部通过引用传递,注意名称修饰 |