调用 C 和 Fortran 代码

尽管大多数代码可以用 Julia 编写,但还有许多高质量、成熟的数值计算库是用 C 和 Fortran 编写的。为了方便使用这些现有的代码,Julia 使得调用 C 和 Fortran 函数变得简单高效。Julia 遵循“无样板”理念:函数可以直接从 Julia 调用,无需任何“粘合”代码、代码生成或编译——甚至可以在交互式提示符下调用。这是通过使用 @ccall 宏(或不太方便的 ccall 语法,请参阅 ccall 语法部分)进行适当的调用来实现的。

要调用的代码必须以共享库的形式提供。大多数 C 和 Fortran 库已经编译成共享库的形式,但如果您使用 GCC(或 Clang)自己编译代码,则需要使用 -shared-fPIC 选项。Julia 的 JIT 生成的机器指令与原生 C 调用相同,因此产生的开销与从 C 代码调用库函数相同。 [1]

默认情况下,Fortran 编译器 生成经过名称修饰的名称(例如,将函数名转换为小写或大写,通常附加下划线),因此要调用 Fortran 函数,您必须传递与您的 Fortran 编译器遵循的规则相对应的修饰标识符。此外,在调用 Fortran 函数时,所有输入都必须作为指向堆或栈上已分配值的指针传递。这不仅适用于数组和其他通常在堆上分配的可变对象,还适用于整数和浮点数等标量值,这些值通常在栈上分配,并且在使用 C 或 Julia 调用约定时通常通过寄存器传递。

用于生成对库函数调用的 @ccall 语法为

  @ccall library.function_name(argvalue1::argtype1, ...)::returntype
  @ccall function_name(argvalue1::argtype1, ...)::returntype
  @ccall $function_pointer(argvalue1::argtype1, ...)::returntype

其中 library 是一个字符串常量或字面量(但请参阅下面的 非常量函数规范)。可以省略库,在这种情况下,函数名将在当前进程中解析。此形式可用于调用 C 库函数、Julia 运行时中的函数或链接到 Julia 的应用程序中的函数。还可以指定库的完整路径。或者,@ccall 也可用于调用函数指针 $function_pointer,例如 Libdl.dlsym 返回的函数指针。argtype 对应于 C 函数签名,argvalue 是要传递给函数的实际参数值。

注意

有关如何 将 C 类型映射到 Julia 类型,请参见下文。

作为一个完整但简单的示例,以下代码调用大多数类 Unix 系统上的标准 C 库中的 clock 函数

julia> t = @ccall clock()::Int32
2292761

julia> typeof(t)
Int32

clock 不接受任何参数并返回一个 Int32。要调用 getenv 函数以获取指向环境变量值的指针,可以进行如下调用

julia> path = @ccall getenv("SHELL"::Cstring)::Cstring
Cstring(@0x00007fff5fbffc45)

julia> unsafe_string(path)
"/bin/bash"

在实践中,尤其是在提供可重用功能时,通常会将 @ccall 调用包装在 Julia 函数中,这些函数设置参数,然后以 C 或 Fortran 函数指定的任何方式检查错误。如果发生错误,则将其作为正常的 Julia 异常抛出。这尤其重要,因为 C 和 Fortran API 在如何指示错误条件方面臭名昭著地不一致。例如,getenv C 库函数在以下 Julia 函数中被包装,该函数是 env.jl 中实际定义的简化版本

function getenv(var::AbstractString)
    val = @ccall getenv(var::Cstring)::Cstring
    if val == C_NULL
        error("getenv: undefined variable: ", var)
    end
    return unsafe_string(val)
end

C getenv 函数通过返回 C_NULL 来指示错误,但其他标准 C 函数以不同的方式指示错误,包括返回 -1、0、1 和其他特殊值。如果调用者尝试获取不存在的环境变量,此包装器将抛出一个指示问题的异常

julia> getenv("SHELL")
"/bin/bash"

julia> getenv("FOOBAR")
ERROR: getenv: undefined variable: FOOBAR

这是一个稍微复杂一点的示例,它发现本地机器的主机名。

function gethostname()
    hostname = Vector{UInt8}(undef, 256) # MAXHOSTNAMELEN
    err = @ccall gethostname(hostname::Ptr{UInt8}, sizeof(hostname)::Csize_t)::Int32
    Base.systemerror("gethostname", err != 0)
    hostname[end] = 0 # ensure null-termination
    return GC.@preserve hostname unsafe_string(pointer(hostname))
end

此示例首先分配一个字节数组。然后,它调用 C 库函数 gethostname 以使用主机名填充数组。最后,它获取指向主机名缓冲区的指针,并将指针转换为 Julia 字符串,假设它是一个以 null 结尾的 C 字符串。

C 库通常使用这种模式,要求调用者分配内存传递给被调用者并进行填充。通常通过创建未初始化的数组并将指向其数据的指针传递给 C 函数来从 Julia 分配内存。这就是我们在这里不使用 Cstring 类型的原因:由于数组未初始化,因此可能包含空字节。在 @ccall 中转换为 Cstring 会检查是否包含空字节,因此可能会抛出转换错误。

使用 unsafe_stringpointer(hostname) 进行解引用是不安全的,因为它需要访问为 hostname 分配的内存,而这些内存可能在此期间已被垃圾回收。宏 GC.@preserve 可以防止这种情况发生,从而避免访问无效的内存位置。

最后,这是一个通过路径指定库的示例。我们创建一个具有以下内容的共享库

#include <stdio.h>

void say_y(int y)
{
    printf("Hello from C: got y = %d.\n", y);
}

并使用 gcc -fPIC -shared -o mylib.so mylib.c 编译它。然后可以通过将(绝对)路径指定为库名称来调用它

julia> @ccall "./mylib.so".say_y(5::Cint)::Cvoid
Hello from C: got y = 5.

创建与 C 兼容的 Julia 函数指针

可以将 Julia 函数传递给接受函数指针参数的原生 C 函数。例如,要匹配以下形式的 C 原型

typedef returntype (*functiontype)(argumenttype, ...)

@cfunction 为对 Julia 函数的调用生成与 C 兼容的函数指针。 @cfunction 的参数为

  1. 一个 Julia 函数
  2. 函数的返回类型
  3. 输入类型的元组,对应于函数签名
注意

@ccall 一样,返回类型和输入类型必须是字面常量。

注意

目前,仅支持平台默认的 C 调用约定。这意味着 @cfunction 生成的指针不能用于 WINAPI 在 32 位 Windows 上期望 stdcall 函数的调用,但可以在 WIN64 上使用(其中 stdcall 与 C 调用约定统一)。

注意

通过 @cfunction 公开的回调函数不应抛出错误,因为这会意外地将控制权返回到 Julia 运行时,并可能使程序处于未定义状态。

一个经典的例子是标准 C 库 qsort 函数,声明如下

void qsort(void *base, size_t nitems, size_t size,
           int (*compare)(const void*, const void*));

base 参数是指向长度为 nitems 的数组的指针,每个元素的大小为 size 字节。compare 是一个回调函数,它接收指向两个元素 ab 的指针,如果 a 应该出现在 b 之前则返回一个小于零的整数,如果 a 应该出现在 b 之后则返回一个大于零的整数(如果任何顺序都允许则返回零)。

现在,假设我们在 Julia 中有一个 1 维数组 A,我们希望使用 qsort 函数(而不是 Julia 内置的 sort 函数)对其进行排序。在考虑调用 qsort 并传递参数之前,我们需要编写一个比较函数。

julia> function mycompare(a, b)::Cint
           return (a < b) ? -1 : ((a > b) ? +1 : 0)
       end;

qsort 期望一个返回 C int 的比较函数,因此我们将返回类型注释为 Cint

为了将此函数传递给 C,我们使用宏 @cfunction 获取其地址。

julia> mycompare_c = @cfunction(mycompare, Cint, (Ref{Cdouble}, Ref{Cdouble}));

@cfunction 需要三个参数:Julia 函数 (mycompare)、返回类型 (Cint) 和输入参数类型的字面元组,在本例中,用于对 Cdouble (Float64) 元素数组进行排序。

qsort 的最终调用如下所示:

julia> A = [1.3, -2.7, 4.4, 3.1];

julia> @ccall qsort(A::Ptr{Cdouble}, length(A)::Csize_t, sizeof(eltype(A))::Csize_t, mycompare_c::Ptr{Cvoid})::Cvoid

julia> A
4-element Vector{Float64}:
 -2.7
  1.3
  3.1
  4.4

如示例所示,原始的 Julia 数组 A 现在已排序:[-2.7, 1.3, 3.1, 4.4]。请注意,Julia 会自动将数组转换为 Ptr{Cdouble},计算元素类型的字节大小等。

为了好玩,尝试在 mycompare 中插入 println("mycompare($a, $b)") 行,这将允许您查看 qsort 正在执行的比较(并验证它是否确实正在调用您传递给它的 Julia 函数)。

将 C 类型映射到 Julia

准确匹配声明的 C 类型及其在 Julia 中的声明至关重要。不一致会导致在某个系统上正确运行的代码在其他系统上失败或产生不确定的结果。

请注意,在调用 C 函数的过程中,任何地方都不会使用 C 头文件:您有责任确保您的 Julia 类型和调用签名准确反映 C 头文件中的类型和签名。[2]

自动类型转换

Julia 会自动插入对 Base.cconvert 函数的调用,以将每个参数转换为指定的类型。例如,以下调用:

@ccall "libfoo".foo(x::Int32, y::Float64)::Cvoid

的行为就像这样编写:

@ccall "libfoo".foo(
    Base.unsafe_convert(Int32, Base.cconvert(Int32, x))::Int32,
    Base.unsafe_convert(Float64, Base.cconvert(Float64, y))::Float64
    )::Cvoid

Base.cconvert 通常只是调用 convert,但可以定义为返回更适合传递给 C 的任意新对象。这应该用于执行 C 代码将访问的所有内存分配。例如,这用于将对象的 Array(例如字符串)转换为指针数组。

Base.unsafe_convert 处理转换为 Ptr 类型的转换。它被认为是不安全的,因为将对象转换为本机指针可能会隐藏该对象,使其无法被垃圾回收器识别,从而导致其过早释放。

类型对应关系

首先,让我们回顾一些相关的 Julia 类型术语。

语法/关键字示例描述
mutable structBitSet"叶类型" :: 一组相关数据,包括类型标签,由 Julia GC 管理,并由对象标识定义。为了构造实例,叶类型的类型参数必须完全定义(不允许使用 TypeVars)。
abstract typeAnyAbstractArray{T, N}Complex{T}"超类型" :: 一个超类型(不是叶类型),不能实例化,但可以用来描述一组类型。
T{A}Vector{Int}"类型参数" :: 类型的专门化(通常用于分派或存储优化)。
"TypeVar" :: 类型参数声明中的 T 被称为 TypeVar(类型变量的简称)。
primitive typeIntFloat64"基本类型" :: 没有字段但有大小的类型。它是按值存储和定义的。
structPair{Int, Int}"结构体" :: 所有字段都定义为常量的类型。它是按值定义的,并且可以与类型标签一起存储。
ComplexF64 (isbits)"Is-Bits" :: 基本类型 或所有字段都是其他 isbits 类型的 结构体 类型。它是按值定义的,并且在存储时不使用类型标签。
struct ...; endnothing"单例" :: 没有字段的叶类型或结构体。
(...)tuple(...)(1, 2, 3)"元组" :: 与匿名结构体类型或常量数组类似的不可变数据结构。表示为数组或结构体。

位类型

有一些特殊类型需要注意,因为没有其他类型可以定义为具有相同的行为。

  • Float32

    完全对应于 C 中的 float 类型(或 Fortran 中的 REAL*4)。

  • Float64

    完全对应于 C 中的 double 类型(或 Fortran 中的 REAL*8)。

  • ComplexF32

    完全对应于 C 中的 complex float 类型(或 Fortran 中的 COMPLEX*8)。

  • ComplexF64

    完全对应于 C 中的 complex double 类型(或 Fortran 中的 COMPLEX*16)。

  • Signed

    完全对应于 C 中的 signed 类型注释(或 Fortran 中的任何 INTEGER 类型)。任何不是 Signed 的子类型的 Julia 类型都被假定为无符号类型。

  • Ref{T}

    表现得像一个 Ptr{T},可以通过 Julia GC 管理其内存。

  • Array{T,N}

    当数组作为 Ptr{T} 参数传递给 C 时,不会对其进行重新解释转换:Julia 要求数组的元素类型与 T 匹配,并且会传递第一个元素的地址。

    因此,如果 Array 中的数据格式错误,则必须使用诸如 trunc.(Int32, A) 之类的调用显式转换。

    要将数组 A 作为不同类型的指针传递,而 *不* 预先转换数据(例如,将 Float64 数组传递给对未解释字节进行操作的函数),您可以将参数声明为 Ptr{Cvoid}

    如果 Ptr{T} 的 eltype 数组作为 Ptr{Ptr{T}} 参数传递,Base.cconvert 将首先尝试创建一个以 null 结尾的数组副本,其中每个元素都被其 Base.cconvert 版本替换。例如,这允许将 argv 指针数组(类型为 Vector{String})传递给 Ptr{Ptr{Cchar}} 类型的参数。

在我们目前支持的所有系统上,基本的 C/C++ 值类型可以转换为 Julia 类型,如下所示。每个 C 类型也都有一个对应的 Julia 类型,其名称相同,并在前面加上 C。这在编写可移植代码时(以及记住 C 中的 int 与 Julia 中的 Int 不相同)时很有帮助。

系统无关类型

C 名称Fortran 名称标准 Julia 别名Julia 基本类型
unsigned charCHARACTERCucharUInt8
bool (_Bool in C99+)CucharUInt8
shortINTEGER*2LOGICAL*2CshortInt16
unsigned shortCushortUInt16
intBOOL (C, typical)INTEGER*4LOGICAL*4CintInt32
unsigned intCuintUInt32
long longINTEGER*8LOGICAL*8ClonglongInt64
unsigned long longCulonglongUInt64
intmax_tCintmax_tInt64
uintmax_tCuintmax_tUInt64
floatREAL*4iCfloatFloat32
doubleREAL*8CdoubleFloat64
complex floatCOMPLEX*8ComplexF32Complex{Float32}
complex doubleCOMPLEX*16ComplexF64Complex{Float64}
ptrdiff_tCptrdiff_tInt
ssize_tCssize_tInt
size_tCsize_tUInt
voidCvoid
void[[noreturn]]_NoreturnUnion{}
void*Ptr{Cvoid}(或类似的 Ref{Cvoid}
T*(其中 T 表示适当定义的类型)Ref{T}(仅当 T 是 isbits 类型时,T 可以安全地进行修改)
char*(或 char[],例如字符串)CHARACTER*N如果以 null 结尾,则为 Cstring,否则为 Ptr{UInt8}
char**(或 *char[]Ptr{Ptr{UInt8}}
jl_value_t*(任何 Julia 类型)Any
jl_value_t* const*(对 Julia 值的引用)Ref{Any}(常量,因为修改需要写入屏障,而无法正确插入)
va_arg不支持
...(可变参数函数规范)T...(其中 T 是上述类型之一,当使用 ccall 函数时)
...(可变参数函数规范); va_arg1::T, va_arg2::S, etc.(仅支持 @ccall 宏)

Cstring 类型本质上是 Ptr{UInt8} 的同义词,除了转换为 Cstring 会在 Julia 字符串包含任何嵌入的 null 字符时引发错误(如果 C 例程将 null 视为终止符,则字符串将被静默截断)。如果您将 char* 传递给不假设 null 终止的 C 例程(例如,因为您传递了显式的字符串长度),或者您确定您的 Julia 字符串不包含 null 并希望跳过检查,则可以使用 Ptr{UInt8} 作为参数类型。Cstring 也可以用作 ccall 返回类型,但在这种情况下,它显然不会引入任何额外的检查,并且仅旨在提高调用的可读性。

系统相关类型

C 名称标准 Julia 别名Julia 基本类型
charCcharInt8 (x86, x86_64)、UInt8 (powerpc, arm)
longClongInt (UNIX)、Int32 (Windows)
unsigned longCulongUInt (UNIX)、UInt32 (Windows)
wchar_tCwchar_tInt32 (UNIX)、UInt16 (Windows)
注意

在调用 Fortran 时,所有输入都必须通过指向堆或栈分配值的指针传递,因此上述所有类型对应关系都应在其类型规范周围包含额外的 Ptr{..}Ref{..} 包装器。

警告

对于字符串参数 (char*),Julia 类型应为 Cstring(如果期望以 null 结尾的数据),否则为 Ptr{Cchar}Ptr{UInt8}(这两个指针类型具有相同的效果),如上所述,而不是 String。类似地,对于数组参数 (T[]T*),Julia 类型应再次为 Ptr{T},而不是 Vector{T}

警告

Julia 的 Char 类型为 32 位,这与所有平台上的宽字符类型 (wchar_twint_t) 不同。

警告

如果返回值类型为Union{},则表示函数不会返回,即 C++11 中的[[noreturn]]或 C11 中的_Noreturn(例如jl_throwlongjmp)。不要将此用于不返回值(void)但确实会返回的函数,对于这些函数,请改用Cvoid

注意

对于wchar_t*类型的参数,Julia类型应为Cwstring(如果C函数期望一个以空字符结尾的字符串),否则为Ptr{Cwchar_t}。另请注意,Julia中的UTF-8字符串数据在内部以空字符结尾,因此可以将其传递给期望以空字符结尾数据的C函数,而无需进行复制(但如果字符串本身包含空字符,使用Cwstring类型将导致抛出错误)。

注意

可以通过在Julia中使用Ptr{Ptr{UInt8}}类型来调用接受char**类型参数的C函数。例如,以下形式的C函数

int main(int argc, char **argv);

可以通过以下Julia代码调用

argv = [ "a.out", "arg1", "arg2" ]
@ccall main(length(argv)::Int32, argv::Ptr{Ptr{UInt8}})::Int32
注意

对于接受类型为character(len=*)的可变长度字符串的Fortran函数,字符串长度作为隐藏参数提供。这些参数在列表中的类型和位置是特定于编译器的,其中编译器供应商通常默认为使用Csize_t作为类型,并在参数列表的末尾附加隐藏参数。虽然此行为对于某些编译器(GNU)是固定的,但其他编译器可以选择允许将隐藏参数直接放在字符参数之后(Intel、PGI)。例如,以下形式的Fortran子程序

subroutine test(str1, str2)
character(len=*) :: str1,str2

可以通过以下Julia代码调用,其中长度已附加

str1 = "foo"
str2 = "bar"
ccall(:test, Cvoid, (Ptr{UInt8}, Ptr{UInt8}, Csize_t, Csize_t),
                    str1, str2, sizeof(str1), sizeof(str2))
警告

Fortran编译器也可能为指针、假定形状(:)和假定大小(*)数组添加其他隐藏参数。可以通过使用ISO_C_BINDING并在子程序的定义中包含bind(c)来避免此类行为,这对于可互操作代码强烈推荐。在这种情况下,将没有隐藏参数,但需要牺牲一些语言特性(例如,只有character(len=1)才能传递字符串)。

注意

声明为返回Cvoid的C函数将在Julia中返回nothing值。

结构体类型对应关系

诸如C中的struct或Fortran90中的TYPE(或某些F77变体中的STRUCTURE/RECORD)之类的复合类型,可以通过创建具有相同字段布局的struct定义在Julia中进行镜像。

当递归使用时,isbits类型会内联存储。所有其他类型都存储为指向数据的指针。当在C中的另一个结构体内部按值使用结构体时,绝对不要尝试手动复制字段,因为这将无法保留正确的字段对齐。相反,声明一个isbits结构体类型并改用它。在转换为Julia时,不支持未命名的结构体。

Julia不支持打包结构体和联合体声明。

如果您事先知道将具有最大大小的字段(可能包括填充),则可以获得union的近似值。将字段转换为Julia时,将Julia字段声明为仅为该类型。

可以使用NTuple表示参数数组。例如,C表示法中的结构体写为

struct B {
    int A[3];
};

b_a_2 = B.A[2];

可以在Julia中写为

struct B
    A::NTuple{3, Cint}
end

b_a_2 = B.A[3]  # note the difference in indexing (1-based in Julia, 0-based in C)

不支持未知大小的数组(由[][0]指定的符合C99标准的可变长度结构体)。通常,处理这些问题的最佳方法是直接处理字节偏移量。例如,如果一个C库声明了一个正确的字符串类型并返回一个指向它的指针

struct String {
    int strlen;
    char data[];
};

在Julia中,我们可以独立访问各个部分以创建该字符串的副本

str = from_c::Ptr{Cvoid}
len = unsafe_load(Ptr{Cint}(str))
unsafe_string(str + Core.sizeof(Cint), len)

类型参数

@ccall@cfunction的类型参数在包含用法的函数定义时进行静态计算。因此,它们必须采用文字元组的形式,而不是变量,并且不能引用局部变量。

这听起来可能像是一个奇怪的限制,但请记住,由于C不是像Julia这样的动态语言,因此其函数只能接受具有静态已知、固定签名的参数类型。

但是,虽然必须静态地知道类型布局才能计算预期的C ABI,但函数的静态参数被认为是此静态环境的一部分。函数的静态参数可以用作调用签名中的类型参数,只要它们不影响类型的布局即可。例如,f(x::T) where {T} = @ccall valid(x::Ptr{T})::Ptr{T}是有效的,因为Ptr始终是字大小的原始类型。但是,g(x::T) where {T} = @ccall notvalid(x::T)::T是无效的,因为T的类型布局不是静态已知的。

SIMD值

注意:此功能目前仅在64位x86和AArch64平台上实现。

如果C/C++例程的参数或返回值是本机SIMD类型,则相应的Julia类型是VecElement的同构元组,该元组自然映射到SIMD类型。具体来说

  • 元组的大小必须与SIMD类型相同。例如,表示x86上__m128的元组必须大小为16字节。
  • 元组的元素类型必须是VecElement{T}的实例,其中T是1、2、4或8字节的原始类型。

例如,考虑此使用AVX内联函数的C例程

#include <immintrin.h>

__m256 dist( __m256 a, __m256 b ) {
    return _mm256_sqrt_ps(_mm256_add_ps(_mm256_mul_ps(a, a),
                                        _mm256_mul_ps(b, b)));
}

以下Julia代码使用ccall调用dist

const m256 = NTuple{8, VecElement{Float32}}

a = m256(ntuple(i -> VecElement(sin(Float32(i))), 8))
b = m256(ntuple(i -> VecElement(cos(Float32(i))), 8))

function call_dist(a::m256, b::m256)
    @ccall "libdist".dist(a::m256, b::m256)::m256
end

println(call_dist(a,b))

主机必须具有必需的SIMD寄存器。例如,上面的代码在没有AVX支持的主机上将无法工作。

内存所有权

malloc/free

此类对象的内存分配和释放必须通过调用正在使用的库中的相应清理例程来处理,就像在任何C程序中一样。不要尝试使用Julia中的Libc.free释放从C库接收的对象,因为这可能导致通过错误的库调用free函数并导致进程中止。反过来(将Julia中分配的对象传递给外部库释放)同样无效。

何时使用TPtr{T}Ref{T}

在包装对外部C例程调用的Julia代码中,普通(非指针)数据应在@ccall内部声明为T类型,因为它们是按值传递的。对于接受指针的C代码,通常应为输入参数的类型使用Ref{T},允许通过隐式调用Base.cconvert使用由Julia或C管理的内存的指针。相反,被调用的C函数返回的指针应声明为输出类型Ptr{T},这反映了指向的内存仅由C管理。C结构体中包含的指针应表示为对应Julia结构体类型中的Ptr{T}类型的字段,这些类型旨在模仿相应C结构体的内部结构。

在包装对外部Fortran例程调用的Julia代码中,所有输入参数都应声明为Ref{T}类型,因为Fortran通过指向内存位置的指针传递所有变量。返回值类型对于Fortran子程序应为Cvoid,对于返回类型T的Fortran函数应为T

将C函数映射到Julia

@ccall / @cfunction参数转换指南

将C参数列表转换为Julia

  • T,其中T是以下原始类型之一:charintlongshortfloatdoublecomplexenum或任何其typedef等价物

    • T,其中T是等价的Julia位类型(根据上表)
    • 如果Tenum,则参数类型应等价于CintCuint
    • 参数值将被复制(按值传递)
  • struct T(包括对结构体的typedef)

    • T,其中T是Julia叶类型
    • 参数值将被复制(按值传递)
  • void*

    • 取决于此参数的使用方式,首先将其转换为目标指针类型,然后使用此列表中的其余规则确定Julia等价物
    • 如果此参数确实只是一个未知指针,则可以将其声明为Ptr{Cvoid}
  • jl_value_t*

    • Any
    • 参数值必须是有效的Julia对象
  • jl_value_t* const*

    • Ref{Any}
    • 参数列表必须是有效的Julia对象(或C_NULL
    • 不能用于输出参数,除非用户能够单独安排对象被GC保留
  • T*

    • Ref{T},其中T是对应于T的Julia类型
    • 如果参数值是inlinealloc类型(包括isbits),则参数值将被复制,否则,该值必须是有效的Julia对象
  • T (*)(...)(例如,指向函数的指针)

    • Ptr{Cvoid}(您可能需要显式使用@cfunction来创建此指针)
  • ...(例如,可变参数)

    • [对于ccall]:T...,其中T是所有剩余参数的单个Julia类型
    • [对于@ccall]:; va_arg1::T, va_arg2::S, etc,其中TS是Julia类型(即用;将常规参数与可变参数分开)
    • @cfunction目前不支持
  • va_arg

    • ccall@cfunction不支持

@ccall / @cfunction返回值转换指南

将C返回值转换为Julia

  • void

    • Cvoid(这将返回单例实例nothing::Cvoid
  • T,其中T是以下原始类型之一:charintlongshortfloatdoublecomplexenum或任何其typedef等价物

    • T,其中T是等价的Julia位类型(根据上表)
    • 如果Tenum,则参数类型应等价于CintCuint
    • 参数值将被复制(按值返回)
  • struct T(包括对结构体的typedef)

    • T,其中T是Julia叶类型
    • 参数值将被复制(按值返回)
  • void*

    • 取决于此参数的使用方式,首先将其转换为目标指针类型,然后使用此列表中的其余规则确定Julia等价物
    • 如果此参数确实只是一个未知指针,则可以将其声明为Ptr{Cvoid}
  • jl_value_t*

    • Any
    • 参数值必须是有效的Julia对象
  • jl_value_t**

    • Ptr{Any}Ref{Any}作为返回值无效)
  • T*

    • 如果内存已由Julia拥有,或者它是isbits类型,并且已知非空

      • Ref{T},其中T是对应于T的Julia类型
      • Ref{Any}作为返回值无效,它应该要么是Any(对应于jl_value_t*),要么是Ptr{Any}(对应于jl_value_t**
      • 如果Tisbits类型,则C**不得**修改通过Ref{T}返回的内存
    • 如果内存由C拥有

      • Ptr{T},其中T是对应于T的Julia类型
  • T (*)(...)(例如,指向函数的指针)

    • Ptr{Cvoid}要直接从Julia调用它,您需要将其作为第一个参数传递给@ccall。请参阅间接调用

传递指针以修改输入

由于 C 不支持多个返回值,因此 C 函数通常会接收指向数据的指针,函数将修改这些数据。要在 @ccall 中实现这一点,您需要首先将值封装在适当类型的 Ref{T} 中。当您将此 Ref 对象作为参数传递时,Julia 会自动将指向封装数据的 C 指针传递过去。

width = Ref{Cint}(0)
range = Ref{Cfloat}(0)
@ccall foo(width::Ref{Cint}, range::Ref{Cfloat})::Cvoid

返回后,可以通过 width[]range[] 检索 widthrange 的内容(如果它们被 foo 更改了);也就是说,它们的行为类似于零维数组。

C 包装器示例

让我们从一个返回 Ptr 类型的简单 C 包装器示例开始。

mutable struct gsl_permutation
end

# The corresponding C signature is
#     gsl_permutation * gsl_permutation_alloc (size_t n);
function permutation_alloc(n::Integer)
    output_ptr = @ccall "libgsl".gsl_permutation_alloc(n::Csize_t)::Ptr{gsl_permutation}
    if output_ptr == C_NULL # Could not allocate memory
        throw(OutOfMemoryError())
    end
    return output_ptr
end

GNU 科学库 (此处假设可以通过 :libgsl 访问) 将不透明指针 gsl_permutation * 定义为 C 函数 gsl_permutation_alloc 的返回类型。由于用户代码永远不必查看 gsl_permutation 结构体的内部,因此相应的 Julia 包装器只需要一个新的类型声明 gsl_permutation,它没有内部字段,其唯一目的是放置在 Ptr 类型的类型参数中。由于 output_ptr 指向并分配的内存由 C 控制,因此 ccall 的返回类型声明为 Ptr{gsl_permutation}

输入 n 按值传递,因此函数的输入签名只需声明为 ::Csize_t,无需任何 RefPtr。(如果包装器调用的是 Fortran 函数,则相应的函数输入签名将改为 ::Ref{Csize_t},因为 Fortran 变量是通过指针传递的。)此外,n 可以是任何可转换为 Csize_t 整数的类型;ccall 会隐式调用 Base.cconvert(Csize_t, n)

这是一个包装相应析构函数的第二个示例。

# The corresponding C signature is
#     void gsl_permutation_free (gsl_permutation * p);
function permutation_free(p::Ptr{gsl_permutation})
    @ccall "libgsl".gsl_permutation_free(p::Ptr{gsl_permutation})::Cvoid
end

这是一个传递 Julia 数组的第三个示例。

# The corresponding C signature is
#    int gsl_sf_bessel_Jn_array (int nmin, int nmax, double x,
#                                double result_array[])
function sf_bessel_Jn_array(nmin::Integer, nmax::Integer, x::Real)
    if nmax < nmin
        throw(DomainError())
    end
    result_array = Vector{Cdouble}(undef, nmax - nmin + 1)
    errorcode = @ccall "libgsl".gsl_sf_bessel_Jn_array(
                    nmin::Cint, nmax::Cint, x::Cdouble, result_array::Ref{Cdouble})::Cint
    if errorcode != 0
        error("GSL error code $errorcode")
    end
    return result_array
end

包装的 C 函数返回一个整数错误代码;贝塞尔 J 函数的实际计算结果填充了 Julia 数组 result_array。此变量声明为 Ref{Cdouble},因为其内存由 Julia 分配和管理。对 Base.cconvert(Ref{Cdouble}, result_array) 的隐式调用将 Julia 指针到 Julia 数组数据结构解包成 C 可理解的形式。

Fortran 包装器示例

以下示例利用 ccall 调用常用 Fortran 库 (libBLAS) 中的函数来计算点积。请注意,此处的参数映射与上面略有不同,因为我们需要从 Julia 映射到 Fortran。在每个参数类型上,我们都指定 RefPtr。此混淆约定可能特定于您的 Fortran 编译器和操作系统,并且可能没有记录。但是,将每个参数包装在 Ref(或等效的 Ptr)中是 Fortran 编译器实现的常见要求。

function compute_dot(DX::Vector{Float64}, DY::Vector{Float64})
    @assert length(DX) == length(DY)
    n = length(DX)
    incx = incy = 1
    product = @ccall "libLAPACK".ddot(
        n::Ref{Int32}, DX::Ptr{Float64}, incx::Ref{Int32}, DY::Ptr{Float64}, incy::Ref{Int32})::Float64
    return product
end

垃圾回收安全性

将数据传递给 @ccall 时,最好避免使用 pointer 函数。相反,定义一个 Base.cconvert 方法并将变量直接传递给 @ccall@ccall 会自动安排所有参数在调用返回之前都将保留不被垃圾回收。如果 C API 将存储对 Julia 分配的内存的引用,则在 @ccall 返回后,您必须确保该对象对垃圾回收器可见。建议的方法是创建一个类型为 Array{Ref,1} 的全局变量来保存这些值,直到 C 库通知您已完成使用它们为止。

每当您创建了指向 Julia 数据的指针时,您都必须确保原始数据存在,直到您完成使用该指针为止。Julia 中的许多方法(例如 unsafe_loadString)会复制数据而不是获取缓冲区的拥有权,因此可以安全地释放(或更改)原始数据而不会影响 Julia。一个值得注意的例外是 unsafe_wrap,出于性能原因,它共享(或可以被告知获取拥有权)底层缓冲区。

垃圾回收器不保证任何最终化顺序。也就是说,如果 a 包含对 b 的引用,并且 ab 都要进行垃圾回收,则不能保证 b 会在 a 之后被最终化。如果 a 的正确最终化依赖于 b 的有效性,则必须通过其他方式处理。

非常量函数规范

在某些情况下,所需的库的确切名称或路径事先未知,必须在运行时计算。为了处理此类情况,库组件规范可以是函数调用,例如 find_blas().dgemm。当 ccall 本身执行时,将执行调用表达式。但是,假设库位置一旦确定就不会更改,因此可以缓存并重用调用的结果。因此,表达式执行的次数是不确定的,并且对多次调用返回不同的值会导致未定义的行为。

如果需要更大的灵活性,可以通过以下方式通过 eval 进行分阶段使用计算值作为函数名。

@eval @ccall "lib".$(string("a", "b"))()::Cint

此表达式使用 string 构造名称,然后将此名称替换到新的 @ccall 表达式中,然后对其进行求值。请记住,eval 仅在顶层运行,因此在此表达式中,局部变量将不可用(除非它们的值用 $ 替换)。因此,eval 通常仅用于形成顶层定义,例如在包装包含许多类似函数的库时。可以为 @cfunction 构造类似的示例。

但是,这样做也会非常慢并导致内存泄漏,因此您通常应该避免这样做,而是继续阅读。下一节讨论如何使用间接调用来有效地实现类似的效果。

间接调用

@ccall 的第一个参数也可以是在运行时计算的表达式。在这种情况下,表达式必须计算为 Ptr,它将用作要调用的本机函数的地址。当第一个 @ccall 参数包含对非常量(例如局部变量、函数参数或非常量全局变量)的引用时,会发生此行为。

例如,您可以通过 dlsym 查找函数,然后将其缓存到该会话的共享引用中。例如

macro dlsym(lib, func)
    z = Ref{Ptr{Cvoid}}(C_NULL)
    quote
        let zlocal = $z[]
            if zlocal == C_NULL
                zlocal = dlsym($(esc(lib))::Ptr{Cvoid}, $(esc(func)))::Ptr{Cvoid}
                $z[] = zlocal
            end
            zlocal
        end
    end
end

mylibvar = Libdl.dlopen("mylib")
@ccall $(@dlsym(mylibvar, "myfunc"))()::Cvoid

闭包 cfunctions

@cfunction 的第一个参数可以用 $ 标记,在这种情况下,返回值将改为 struct CFunction,它会闭包该参数。您必须确保此返回值对象在所有使用它完成之前都保持存活。cfunction 指针处的内容和代码将在此引用被删除以及 atexit 时通过 finalizer 擦除。这通常不需要,因为此功能在 C 中不存在,但对于处理没有提供单独闭包环境参数的设计不良的 API 很有用。

function qsort(a::Vector{T}, cmp) where T
    isbits(T) || throw(ArgumentError("this method can only qsort isbits arrays"))
    callback = @cfunction $cmp Cint (Ref{T}, Ref{T})
    # Here, `callback` isa Base.CFunction, which will be converted to Ptr{Cvoid}
    # (and protected against finalization) by the ccall
    @ccall qsort(a::Ptr{T}, length(a)::Csize_t, Base.elsize(a)::Csize_t, callback::Ptr{Cvoid})
    # We could instead use:
    #    GC.@preserve callback begin
    #        use(Base.unsafe_convert(Ptr{Cvoid}, callback))
    #    end
    # if we needed to use it outside of a `ccall`
    return a
end
注意

闭包 @cfunction 依赖于 LLVM 蹦床,而并非所有平台都提供 LLVM 蹦床(例如 ARM 和 PowerPC)。

关闭库

有时关闭(卸载)库以便重新加载它会很有用。例如,在开发用于 Julia 的 C 代码时,可能需要编译、从 Julia 调用 C 代码,然后关闭库,进行编辑,重新编译并加载新更改。您可以重新启动 Julia 或使用 Libdl 函数显式管理库,例如

lib = Libdl.dlopen("./my_lib.so") # Open the library explicitly.
sym = Libdl.dlsym(lib, :my_fcn)   # Get a symbol for the function to call.
@ccall $sym(...) # Use the pointer `sym` instead of the library.symbol tuple.
Libdl.dlclose(lib) # Close the library explicitly.

请注意,当使用输入的 @ccall 时(例如,@ccall "./my_lib.so".my_fcn(...)::Cvoid),库会隐式打开,并且可能不会显式关闭。

可变参数函数调用

要调用可变参数 C 函数,可以在参数列表中使用 分号 将必需参数与可变参数分隔开。下面给出了一个使用 printf 函数的示例。

julia> @ccall printf("%s = %d\n"::Cstring ; "foo"::Cstring, foo::Cint)::Cint
foo = 3
8

ccall 接口

@ccall 还有另一种替代接口。此接口稍微不太方便,但它确实允许您指定 调用约定

ccall 的参数为

  1. (:function, "library") 对(最常见),

    :function 名称符号或 "function" 名称字符串(对于当前进程或 libc 中的符号),

    函数指针(例如,来自 dlsym)。

  2. 函数的返回类型

  3. 输入类型的元组,对应于函数签名。一个常见的错误是忘记包含一个参数类型的 1 元组必须使用尾随逗号编写。

  4. 要传递给函数的实际参数值(如果有);每个都是一个单独的参数。

注意

(:function, "library") 对、返回类型和输入类型必须是文字常量(即,它们不能是变量,但请参阅 非常量函数规范)。

其余参数在编译时求值,即包含方法定义时。

下面给出了宏和函数接口之间转换的表格。

@ccallccall
@ccall clock()::Int32ccall(:clock, Int32, ())
@ccall f(a::Cint)::Cintccall(:a, Cint, (Cint,), a)
@ccall "mylib".f(a::Cint, b::Cdouble)::Cvoidccall((:f, "mylib"), Cvoid, (Cint, Cdouble), (a, b))
@ccall $fptr.f()::Cvoidccall(fptr, f, Cvoid, ())
@ccall printf("%s = %d\n"::Cstring ; "foo"::Cstring, foo::Cint)::Cint<unavailable>
@ccall printf("%s = %d\n"::Cstring ; "2 + 2"::Cstring, "5"::Cstring)::Cintccall(:printf, Cint, (Cstring, Cstring...), "%s = %s\n", "2 + 2", "5")
<unavailable>ccall(:gethostname, stdcall, Int32, (Ptr{UInt8}, UInt32), hn, length(hn))

调用约定

ccall 的第二个参数(紧接在返回值类型之前)可以选择是一个调用约定说明符(@ccall 宏目前不支持提供调用约定)。如果没有指定说明符,则使用平台默认的 C 调用约定。其他支持的约定包括:stdcallcdeclfastcallthiscall(在 64 位 Windows 上为无操作)。例如(来自 base/libc.jl),我们看到了上面相同的 gethostnameccall,但具有 Windows 的正确签名

hn = Vector{UInt8}(undef, 256)
err = ccall(:gethostname, stdcall, Int32, (Ptr{UInt8}, UInt32), hn, length(hn))

更多信息,请参阅 LLVM 语言参考

还有一个额外的特殊调用约定 llvmcall,它允许直接插入对 LLVM 内在函数的调用。这在针对 GPGPU 等不寻常的平台时尤其有用。例如,对于 CUDA,我们需要能够读取线程索引

ccall("llvm.nvvm.read.ptx.sreg.tid.x", llvmcall, Int32, ())

与任何 ccall 一样,获取完全正确的参数签名至关重要。此外,请注意,没有兼容性层可以确保内在函数在当前目标上具有意义并起作用,这与 Core.Intrinsics 公开的等效 Julia 函数不同。

访问全局变量

可以使用 cglobal 函数按名称访问本机库导出的全局变量。 cglobal 的参数与 ccall 使用的符号规范相同,以及描述存储在变量中的值的类型

julia> cglobal((:errno, :libc), Int32)
Ptr{Int32} @0x00007f418d0816b8

结果是一个指向该值地址的指针。可以使用 unsafe_loadunsafe_store! 通过此指针操作该值。

注意

errno 符号可能无法在名为“libc”的库中找到,因为这是系统编译器的实现细节。通常,标准库符号应该只按名称访问,允许编译器填写正确的符号。但是,此示例中显示的 errno 符号在大多数编译器中是特殊的,因此此处看到的价值可能不是您期望或想要的。在任何支持多线程的系统上用 C 编译等效代码通常会实际调用不同的函数(通过宏预处理器重载),并且可能给出与此处打印的旧值不同的结果。

通过指针访问数据

以下方法被描述为“不安全”,因为错误的指针或类型声明可能导致 Julia 突然终止。

给定一个 Ptr{T},类型 T 的内容通常可以使用 unsafe_load(ptr, [index]) 从引用的内存复制到 Julia 对象中。索引参数是可选的(默认为 1),并遵循 Julia 的 1 为基索引约定。此函数在行为上故意类似于 getindexsetindex!(例如 [] 访问语法)。

返回值将是一个新对象,初始化为包含引用的内存内容的副本。引用的内存可以安全地释放或释放。

如果 TAny,则假定内存包含对 Julia 对象的引用(jl_value_t*),结果将是对该对象的引用,并且不会复制该对象。在这种情况下,您必须小心确保该对象始终对垃圾收集器可见(指针不算,但新引用算),以确保内存不会过早释放。请注意,如果该对象最初不是由 Julia 分配的,则 Julia 的垃圾收集器永远不会完成新对象。如果 Ptr 本身实际上是 jl_value_t*,则可以通过 unsafe_pointer_to_objref(ptr) 将其转换回 Julia 对象引用。(Julia 值 v 可以通过调用 pointer_from_objref(v) 转换为 jl_value_t* 指针,作为 Ptr{Cvoid}。)

反向操作(将数据写入 Ptr{T})可以使用 unsafe_store!(ptr, value, [index]) 执行。目前,这仅支持基本类型或其他无指针 (isbits) 的不可变结构体类型。

任何抛出错误的操作可能目前都未实现,应作为错误发布,以便可以解决。

如果感兴趣的指针是普通数据数组(基本类型或不可变结构体),则函数 unsafe_wrap(Array, ptr,dims, own = false) 可能更有用。如果 Julia 应该“获取”底层缓冲区的“所有权”并在返回的 Array 对象完成时调用 free(ptr),则最后一个参数应为 true。如果 own 参数被省略或为 false,则调用方必须确保缓冲区在所有访问完成后仍然存在。

Julia 中 Ptr 类型的算术运算(例如使用 +)与 C 的指针算术运算的行为不同。在 Julia 中向 Ptr 添加整数始终使指针移动一定数量的字节,而不是元素。这样,从指针算术获得的地址值不依赖于指针的元素类型。

线程安全

一些 C 库从不同的线程执行其回调,并且由于 Julia 不是线程安全的,因此您需要采取一些额外的预防措施。特别是,您需要设置一个两层系统:C 回调应该只安排(通过 Julia 的事件循环)执行您的“真实”回调。为此,创建一个 AsyncCondition 对象并在其上 wait

cond = Base.AsyncCondition()
wait(cond)

您传递给 C 的回调应该只执行对 :uv_async_sendccall,将 cond.handle 作为参数传递,注意避免任何分配或与 Julia 运行时的其他交互。

请注意,事件可能会合并,因此多次调用 uv_async_send 可能会导致对条件发出单个唤醒通知。

关于回调的更多信息

有关如何将回调传递给 C 库的更多详细信息,请参阅此 博客文章

C++

有关创建 C++ 绑定的工具,请参阅 CxxWrap 包。

  • 1C 和 Julia 中的非库函数调用都可以在内联,因此开销可能比对共享库函数的调用还要少。上面提到的要点是,实际执行外部函数调用的成本与在任一原生语言中进行调用的成本大致相同。
  • 2Clang 包 可用于根据 C 头文件自动生成 Julia 代码。