强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 类型大小说明
voidCvoid0返回类型用
charCchar1有符号字符
unsigned charCuchar1无符号字符
shortCshort2
unsigned shortCushort2
intCint4
unsigned intCuint4
longClong4/8平台相关
unsigned longCulong4/8平台相关
long longClonglong8
floatCfloat4
doubleCdouble8
char* / const char*Cstring / Cstring8自动转换
void*Ptr{Cvoid}8通用指针
size_tCsize_t864位系统
int8_tInt81
int16_tInt162
int32_tInt324
int64_tInt648
bool (_Bool)Cuchar1C99 布尔

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参数全部通过引用传递,注意名称修饰

扩展阅读