14  Julia 与 C 的互操作

14.1 在 Julia 中调用 C

在 Julia 中调用 C 函数非常简单,我们只需要使用 ccall 函数即可。

ccall(:printf, Cvoid, (Cstring,), "Hello World!\n")
Hello World!

ccall 的参数可以分为四部分:

  • :printf 部分对应要调用的 C 函数的名称或函数指针。
  • Cvoid 部分对应 C 函数的返回值类型。
  • (Cstring,) 部分对应 C 函数的参数类型。
  • 剩余部分对应于传入 C 函数的参数。这些参数应当与 C 函数的参数类型一一对应。

此外,Julia 还提供了 @ccall 宏,可以简化这一调用。

@ccall printf("Hello World using @ccall!"::Cstring)::Cvoid
Hello World using @ccall!
说明

ccall 函数和 @ccall 宏更详细的使用方法可以参考 Julia 官方文档中的有关内容(中文版本在这里)。

14.1.1 从动态链接库中调用 C 函数

上面的例子中,可以直接调用 printf 函数,是因为 Julia 运行时所依赖的 libjulia 动态链接库本身包含了一些常用的 C 标准库。如果我们想要调用其他动态链接库中的函数,则必须告诉 Julia 运行时这个函数所对应的动态链接库的位置。

作为示例,我们在 interoperability/c 目录下提供了一个简单的动态链接库 libJuliaC,其中包含了几个简单的 C 函数。

interoperability/c/src/lib.c
#include "math.h"

int FLAG = 0;

int fibonacci(int n) {
    if (FLAG < n) {
        FLAG = n;
    }

    if (n < 2)
        return n;
    
    int a = 0, b = 1, c;
    for (int i = 2; i <= n; i++) {
        c = a + b;
        a = b;
        b = c;
    }

    return b;
}

void map(double *arr, int size, double (*f)(double)) {
    for (int i = 0; i < size; i++) {
        arr[i] = f(arr[i]);
    }
}

使用 CMake 构建这一动态链接库,并将构建得到的库文件复制到当前目录下,就可以运行下面的代码。

最简单的方式是直接声明对应动态链接库的路径(绝对路径或相对路径均可),就像下面这样:

ccall((:fibonacci, "libJuliaC"), Cint, (Cint,), 10)
55

我们也可以借助 Julia 标准库中的 Libdl 模块来动态加载动态链接库,然后再调用其中的函数。这样做的好处是可以在运行时动态地加载动态链接库。

using Libdl: dlopen, dlsym, dlclose

# 加载动态库
libJuliaC = dlopen("libJuliaC")

# 获取函数指针
fibonacci = dlsym(libJuliaC, :fibonacci)

@show ccall(fibonacci, Cint, (Cint,), 10)

# 卸载动态库
dlclose(libJuliaC);
ccall(fibonacci, Cint, (Cint,), 10) = 55

我们也可以使用 dlopen 的另一种调用方式:

dlopen("libJuliaC") do libJuliaC
    fibonacci = dlsym(libJuliaC, :fibonacci)
    @show ccall(fibonacci, Cint, (Cint,), 10)
    dlclose(libJuliaC)

    return nothing
end
ccall(fibonacci, Cint, (Cint,), 10) = 55

14.1.2 使用 C 中定义的全局变量

有时,我们还需要使用 C 中定义的全局变量。我们可以使用 cglobal 函数来获取对应的全局变量的指针。

dlopen("libJuliaC") do libJuliaC
    # 获取 C 语言中的全局变量 `FLAG`
    flag_sym = dlsym(libJuliaC, :FLAG)
    flag_ptr = cglobal(flag_sym, Cint)
    flag = unsafe_load(flag_ptr)
    @show flag

    # 调用 `fibonacci` 函数后,`FLAG` 的值会被修改
    fibonacci = dlsym(libJuliaC, :fibonacci)
    ccall(fibonacci, Cint, (Cint,), 10)
    flag = unsafe_load(flag_ptr)
    @show flag

    # 在 Julia 中修改 C 语言中的全局变量
    unsafe_store!(flag_ptr, 100)
    flag = unsafe_load(flag_ptr)
    @show flag

    dlclose(libJuliaC)

    return nothing
end
flag = 0
flag = 10
flag = 100

14.1.3 将 Julia 函数作为函数指针传入 C 函数

cfunc = @cfunction(x -> x^3, Cdouble, (Cdouble,))

dlopen("libJuliaC") do libJuliaC
    c_map = dlsym(libJuliaC, :map)
    arr = [1.0, 2.0, 3.0]

    # 调用 `libJuliaC` 中的 `map` 函数
    result = ccall(c_map, Cvoid,
        (Ptr{Cdouble}, Cint, Ptr{Cvoid}),
        arr, length(arr), cfunc
    )

    dlclose(libJuliaC)
    arr
end
3-element Vector{Float64}:
  1.0
  8.0
 27.0
注意

需要强调的是,如果不调用 dlclose 函数,动态链接库将保持加载状态。如果我们希望动态链接库中的全局变量在每次调用时都能够被重置,就需要及时调用 dlclose 函数。

14.2 在 C 中调用 Julia

下面是一个在 C 中调用 Julia 的例子。项目仓库中已经使用 CMake 配置好了编译和链接的有关选项,直接构建即可生成可执行程序 CJulia

interoperability/c/src/main.c
#include <julia.h>
// 如果希望提高代码运行速度,在一个可执行程序中定义这一选项且仅定义一次。
JULIA_DEFINE_FAST_TLS

int main(int argc, char *argv[])
{
    /* 初始化 Julia 环境(必须进行这一步骤) */
    jl_init();

    /* 从 Julia 的 `Main` 模块中获取 `sqrt` 函数 */
    jl_value_t *jl_sqrt = jl_get_function(jl_main_module, "sqrt");

    /* 调用 `sqrt` 函数。这里需要对 C 中的数值进行装箱。 */
    jl_value_t *result = jl_call1(jl_sqrt, jl_box_float64(2.0));

    /* 输出结果。这里需要对 Julia 函数的运行结果进行拆箱。 */
    printf("sqrt(2.0) = %f", jl_unbox_float64(result));

    /* 清理并退出 Julia 环境(强烈建议进行这一步骤) */
    jl_atexit_hook(0);
    return 0;
}
说明

在 C 中调用 Julia 函数更详细的方法可以参考 Julia 官方文档中的有关内容(中文版本在这里)。