Engee 文档

在C和Fortran中调用代码

虽然您几乎可以在Julia上编写任何代码,但有许多现成的,开发的库用于c和Fortran中的数值计算。 为了使现有代码易于使用,Julia支持c和Fortran中的高效函数调用。 Julia使用"无定型"原则:函数可以直接从Julia调用,而无需链接代码,代码生成或编译-甚至可以从交互式命令行调用。 为此,使用宏进行适当的调用就足够了。 '@ccall'(或不太方便的语法 `ccall';请参阅语法部分 'ccall')。

被调用的代码必须作为共享库可用。 大多数C和Fortran库都是作为已经编译的共享库提供的,但是如果您使用GCC(或Clang)自己编译代码,则必须使用`-shared`和`-fPIC`选项。 Julia JIT编译器生成的机器指令类似于他们自己的C调用,因此总成本最终将与从c中的代码调用库函数时相同。脚注:1[对C和Julia中的非库函数的调用可以 原因是调用第三方函数的成本几乎与用母语调用函数相同。]

Fortran编译器默认为https://en.wikipedia.org/wiki/Name_mangling#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

时钟函数不接受参数并返回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报告错误状态不同的原因尤其重要。 例如,来自C库的’getenv’函数包括以下Julia函数,这是来自定义的简化版本https://github.com/JuliaLang/julia/blob/master/base/env.jl ['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和其他特殊值。 如果调用者试图获取一个不存在的环境变量,则此shell会抛出异常,报告问题。:

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 # обеспечиваем завершающий нулевой символ
    return GC.@preserve hostname unsafe_string(pointer(hostname))
end

在此示例中,首先将字节数组放在内存中。 然后调用C库中的`gethostname’函数以用主机名填充数组。 最后,取一个指向带有主机名的缓冲区的指针(假设这是一个带有尾随空字符的C字符串)并转换为Julia字符串。

C库通常要求调用者分配一个内存区域,该内存区域将传递给被调用的对象以填充。 在Julia中,通常会为此创建一个未初始化的数组,并将指向数据的指针传递给C函数。 这就是为什么这里没有使用’Cstring’类型。:由于数组未初始化,因此它可以包含零个字节。 转换为’Cstring`时,作为`@ccall`调用的一部分检查零字节的存在,这可能会导致转换错误。

使用`unsafe_string`取消引用指针`pointer(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 米利布。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'创建一个c兼容的函数指针来调用Julia函数。 方法 '@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’是一个回调函数,它接受指向两个元素(a`和`b)的指针,如果元素`a`应该在元素`b`之前或之后(或者如果允许任何顺序为零),则返回大于或小于零的整数。

现在让我们假设Julia有一个值`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]. 请注意,朱莉娅 执行数组转换为'Ptr{Cdouble}',以字节为单位计算元素类型的大小等。

为了感兴趣,请尝试将字符串`println("mycompare(a a,b b)")`插入`mycompare'。 这将允许您查看’qsort’函数进行的比较(并检查它是否实际调用传递的Julia函数)。

C型和Julia的比较

C和Julia中的类型声明完全匹配是非常重要的。 由于不一致,在一个系统上正常工作的代码可能会失败或在另一个系统上产生不确定的结果。

请注意,在C中调用函数的过程中不使用C头文件:您负责将Julia调用的类型和签名匹配到C头文件[using https://github.com/ihnorton/Clang.jl [Clang包],可以根据C头文件自动创建Julia代码]。

自动类型转换

Julia自动添加函数调用 '基地。cconvert'将每个参数转换为指定的类型。 例如,挑战:

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

它将被执行,就好像它具有以下格式:

c_x = Base.cconvert(Int32, x)
c_y = Base.cconvert(Float64, y)
GC.@preserve c_x c_y begin
    @ccall "libfoo".foo(
        Base.unsafe_convert(Int32, c_x)::Int32,
        Base.unsafe_convert(Float64, c_y)::Float64
    )::Cvoid
end

功能 '基地。cconvert'通常只是调用 'convert',但它可以被重新定义,以便它返回另一个更适合传输到C的任意对象。这样,C代码将访问的所有内存区域都应该被分配。 例如,通过这种方式,对象数组(比如字符串)被转换为指针数组。

'基地。unsafe_convert'执行到类型的转换 'Ptr'。 此操作被认为是不安全的,因为将对象转换为非托管指针可能会使垃圾回收器无法访问它,这将导致它提前被销毁。

匹配类型

首先,让我们来看看Julia中与类型相关的一些重要术语。

语法或关键字 例子: 资料描述

'可变结构`

"比特集`

叶类型:一组相关数据,其中包括类型标签,由Julia垃圾回收器管理,并由对象标识符确定。 要创建最终类型的实例,必须完全定义类型参数(`TypeVars’变量无效)。

"抽象类型`

'Any','AbstractArray{T, N}','复杂{T}`

超类型:一种非有限类型,不能实例化,但可用于描述一组类型。

'T{A}`

'向量{Int}`

类型参数:类型特化(通常用于调度或优化存储)。

类型变量(TypeVar):类型参数声明中的`T’元素称为TypeVar(type variable—​类型变量的简称)。

"原始类型`

'Int','Float64`

原始类型:没有字段,但具有一定大小的类型。 值来存储和确定。

'结构`

'对{Int, Int}`

Struct:将所有字段定义为常量的类型。 它由值决定,可以用类型标签存储。

'ComplexF64'('isbits`)

位类型(Is-Bits):基元类型(primitive type)或结构体(struct),其所有字段也属于位类型(isbits)。 它是由值决定的,并且没有类型标签存储.

'结构。..;结束`

"没什么`

单一类型(Singleton):没有字段的有限类型或结构。

(...)+`或'+元组(。..).

(1, 2, 3)

元组:类似于匿名结构类型或常量数组的不可变数据结构。 它由数组或结构表示。

位类型

应特别注意一些特殊类型,其特征不能在其他类型中实现。:

  • '漂浮32`

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

  • '漂浮64`

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

  • 'ComplexF32`

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

  • 'ComplexF64`

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

  • '签名`

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

  • 'Ref{T}`

    它作为一个指针'Ptr{T}`,它可以通过Julia垃圾回收器管理它的内存区域。

  • '阵列{T,N}`

    当数组作为参数`Ptr传递给C时{T}`,它不是使用reinterpret cast进行强制转换:Julia需要匹配数组元素的类型以键入’T’并传递第一个元素的地址。

    因此,如果’数组’包含格式不正确的数据,则必须使用诸如`trunc的调用显式转换它。(Int32,A)'。

    要将数组`A`作为另一种类型的指针传递,而无需首先转换数据(例如,将`Float64’类型的数组传递给使用未解释字节操作的函数),您可以将参数声明为'Ptr{Cvoid}`.

    如果数组类型为eltype'Ptr{T}'作为参数传递'Ptr{Ptr{T}}',函数 '基地。cconvert'将首先尝试创建它的空终止副本,用其版本替换每个元素 '基地。cconvert'。 例如,这允许传递类型为"Vector"的"argv"指针数组。{String}'在类型参数中'Ptr{Ptr{Cchar}}`.

在所有当前支持的系统中,基本值类型为C/C++ 它们可以通过以下方式转换为Julia类型。 对于每种类型的C,也有一个具有相同名称的相应Julia类型,但带有前缀C.这在编写可移植代码时可以提供帮助(并且不要忘记C中的类型`int`与Julia中的类型`

与系统无关的类型

名称为C Fortran中的名称 Julia中的标准别名 基本类型是Julia

"无符号字符`

"性格`

'Cuchar`

'UInt8`

'bool'(C99+中的_Bool)

'Cuchar`

'UInt8`

`短'

'整数*2`,'逻辑*2`

'Cshort`

`Int16'

无符号短文

'库索特`

'UInt16`

'int','BOOL'(C中的标准名称)

'整数*4`,'逻辑*4`

'Cint`

`Int32'

'unsigned int`

'Cuint`

'UInt32`

隆陇

'整数*8`,逻辑*8

'克隆龙`

`Int64'

'unsigned陇隆`

`库龙'

'UInt64`

'intmax_t`

Cintmax_t

`Int64'

uintmax_t

'Cuintmax_t`

'UInt64`

`浮动'

'真实*4i`

Cfloat

'漂浮32`

"双倍"

'真实*8`

'Cdouble`

'漂浮64`

"复杂浮动"

'复杂*8`

'ComplexF32`

'复杂{Float32}`

'复杂的双`

'复杂*16`

'ComplexF64`

'复杂{Float64}`

ptrdiff_t

'Cptrdiff_t`

`Int'

'ssize_t`

'Cssize_t`

`Int'

`size_t'

'Csize_t`

'UInt`

`无效'

'Cvoid`

'void’和''或'_Noreturn`

'联合{}`

'无效*`

'Ptr{Cvoid}'(或者,等价地,'Ref{Cvoid}`)

`T*'(其中T表示适当定义的类型)

'Ref{T}'(t的安全修改是可能的,只有当它是位类型)

'char*'(或’char[]',例如字符串)

'字符*N`

'Cstring',如果以零结尾,或'Ptr{UInt8}'否则

'char**'(或'*char[]`)

'Ptr{Ptr{UInt8}}`

'jl_value_t’(任何类型的Julia)

"任何`

'jl_value_t*const’(链接到Julia值)

'Ref{Any}'(一个常量,因为写入需要一个不能正确添加的写入障碍)

'va_arg`

不支持

`...'(参数数量可变的函数的规范)

'T...'(其中`T`是使用`ccall’函数时上面列出的类型之一)

`...'(参数数量可变的函数的规范)

';va_arg1::T,va_arg2::S等。`(仅支持宏'@ccall')

类型 'Cstring'实际上是'Ptr'的同义词{UInt8},但如果Julia字符串包含嵌入的空字符,则转换为`Cstring’会导致错误(如果C例程将空字符视为终止,则会自动截断此类字符串)。 如果您将`char*`传递给不期望尾随空字符的C例程(例如,因为您显式传递了字符串的长度),或者如果您确定Julia字符串不包含空字符并希望跳过检查,则可以使{UInt8}+. `Cstring’也可以用作返回类型。 'ccall',但在这种情况下不执行额外的检查。:这样做只是为了增加调用的可读性。

依赖于系统的类型

名称为C Julia中的标准别名 基本类型是Julia

'char`

'Cchar`

'Int8'(x86,x86_64),'UInt8'(powerpc,arm)

`长'

'克隆`

'Int'(UNIX),'Int32'(Windows)

`未签名的长"

'库龙`

'UINT'(UNIX)’UInt32'(Windows)

wchar_t

'Cwchar_t`

'Int32'(UNIX),'UInt16'(Windows)

当调用Fortran代码时,所有输入数据必须作为指向放置在"堆"或堆栈上的值的指针传递,因此上面列出的所有相关类型的规范必须包含在附加的’Ptr’包装器中。{..}'或’Ref{..}`.

对于字符串参数('char*'`,如果预期具有尾随空字符的数据,则应使用Julia’Cstring’类型或'+Ptr’类型。{Cchar}+'或'+Ptr{UInt8}+'否则(这两种类型的指针的工作方式相同)如上所述。 不要使用’String’类型。 同样,对于数组参数(T[]`或`T*),应该使用Julia'Ptr类型。{T},不是'向量{T}.

Julia中的类型’Char’的大小为32位,这与其他一些平台上扩展字符(wchar_t`或`wint_t)的大小不同。

返回的类型是’Union{}` означает, что функция не возвращает управление (например, jl_throw или longjmp). В C++11对应于属性`',并且在C11-_Noreturn’中。 对于不返回值('void)但返回控件的函数,请勿使用此类型。 使用’Cvoid’代替。

对于Julia中的参数’wchar_t’,应该使用类型 'Cwstring',如果C例程期望以空字符结尾的字符串,或'Ptr{Cwchar_t}'否则。 另请注意,Julia中UTF-8编码的字符串数据以空字符结尾,因此可以将其传递给c函数,等待结尾为空字符的数据,而无需创建副本(但是,如果字符串本身包含空字符,则使用`Cwstring`类型将导致错误)。

使用类型`char**的参数的c函数可以使用类型Ptr调用{Ptr{UInt8}}'在朱莉娅。 例如,以下形式的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函数,字符串长度指定为_expressed arguments_。 这些参数在列表中的类型和位置取决于编译器。 通常,`Csize_t’用作类型,隐藏参数被添加到参数列表的末尾。 在某些编译器(例如,GNU)中,这是一个严格的规则,但在其他编译器(Intel,PGI)中,如果需要,可以在字符参数之后立即指定隐藏参数。 例如,以下形式的Fortran子例程:

"'fortran 子程序测试(str1,str2) 字符(len=*)::str1,str2

您可以使用以下Julia代码调用它,其中字符串长度添加到列表的末尾:

"'朱莉娅
str1="foo"
str2="酒吧"
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`(或Fortran77的某些版本中的`STRUCT`和`RECORD`)可以通过创建具有相同字段结构的`struct’定义在Julia中表示。

递归使用时,`isbits’的类型直接存储。 所有其他类型都存储为指向数据的指针。 当在C中表示由另一个结构内部的value使用的结构时,永远不要尝试手动复制字段:字段的对齐将不会保留。 相反,声明结构类型’isbits’并使用它。 没有名称的结构不能转换为Julia。

Julia不支持打包结构和join声明。

如果最初知道最大的字段(您可能需要添加填充字节),您可以粗略地重新创建`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]  # обратите внимание на различия в индексировании (в Julia от 1, в C от 0)

不直接支持未知大小的数组(符合C99标准并指定为`[][0]`的可变长度结构)。 通常,使用它们的最佳方法是直接使用字节偏移进行操作。 例如,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`宏的类型参数是在确定使用它们的方法时静态计算的。 因此,这样的参数必须是字面元组,而不是变量,并且不能引用局部变量。

这个限制可能看起来很奇怪,但不要忘记,与Julia不同,C不是动态语言,它的函数只能接受具有静态,固定签名的参数类型。

但是,尽管必须静态指定类型结构以定义所需的ABI c接口,但静态函数参数被认为是此静态环境的一部分。 静态函数参数如果不影响类型结构,可以在调用签名中用作类型参数。 例如,调用'f(x::T)where {T} =@ccall有效(x::Ptr{T})::Ptr{T}'是可以接受的,因为`Ptr`总是一个机器字大小的原始类型。 反过来,调用'g(x::T)其中 {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))

函数call_dist(a::m256,b::m256)
    @ccall"libdist"。区(a::m256,b::m256)::m256
结束

println(call_dist(a,b))

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

内存所有权

'malloc'/'免费`

要为对象分配内存并释放它,您需要从您使用的库中调用适当的清理例程,就像在任何C程序中一样。 不要尝试释放使用该函数从C库中获取的对象所占用的内存 'Libc.自由'在朱莉娅。 这可能会导致从错误的库中调用`free’函数并导致进程崩溃。 反向操作(试图从内存中删除外部库中Julia代码中创建的对象)也是不可接受的。

使用`T`,Ptr'的区别{T}+'和'+Ref{T}

在调用外部C例程的Julia代码中,必须在`@ccall`调用中使用类型`T`声明普通数据(而不是指针),因为它们是按值传递的。 对于接受指针的C代码,通常应将以下内容用作输入参数类型: 'Ref{T}. 这允许通过隐式调用来使用Julia或C环境管理的指针 '基地。cconvert'。 相反,被调用的C函数返回的指针必须声明为具有输出类型。 'Ptr{T}. 这意味着它们指向的内存仅由C管理。C结构中包含的指针必须由类型为`Ptr'的字段表示。{T}Julia结构类型中的`,它重新创建相应C结构的内部结构。

在调用外部Fortran例程的Julia代码中,所有输入参数都必须使用"Ref"类型声明{T}',因为在Fortran中,所有变量都通过指向内存位置的指针传递。 对于Fortran例程,返回类型必须为’Cvoid`,对于返回类型为`T`的Fortran函数必须为`T'。

比较C和Julia函数

将参数转换为'@ccall’和'@cfunction’的指南

C中的参数列表根据下面列出的规则转换为Julia。

  • T,其中’T’是原始类型之一:'char`,intlongshortfloatdoublecomplexenum’或它们的任何等价物’typedef'。 'T,其中`T’是等效的Julia位类型(根据上表)。 如果’T’是`enum’的枚举,则参数的类型必须等效于’Cint’或’Cuint'。 **参数的值被复制(按值传递)。

  • 'struct T'(包括结构的typedef)。 'T`,其中`T’是Julia的最终类型。 参数的值被复制(按值传递)。

  • '无效*` 取决于参数的使用方式;首先将其转换为所需的指针类型,然后根据列表中的进一步规则确定等效的Julia类型。 此参数可以声明为'Ptr{Cvoid}',如果它真的只是一个未知的指针。

  • 'jl_value_t’ '任何` 参数的值必须是有效的Julia对象。

  • 'jl_value_t*const*` 'Ref{Any}` 参数的值必须是有效的Julia对象(或’C_NULL`)。 **不能用于输出参数,除非用户可以安排垃圾回收器保存对象。

  • 'T*` 'Ref{T}`,其中’T’是`T’对应的Julia类型。 如果参数的值是`inlinealloc`类型(这包括`isbits`类型),则会复制该参数的值,否则该值必须是有效的Julia对象。

  • T(*)(。..)(例如,指向函数的指针)。 **'Ptr{Cvoid}'(您可能需要使用宏来创建此指针 `@cfunction'显式)

  • ...(例如,vararg)。

    • **目前不支持宏"@cfunction"。

  • 'va_arg` **不受`ccall`函数或`@cfunction`宏的支持。

转换返回类型`@ccall`和`@cfunction`指南

C中返回的函数类型根据下面列出的规则转换为Julia。

  • 无效' **'Cvoid(返回’nothing::Cvoid’的单独实例)

  • T,其中’T’是原始类型之一:'char`,intlongshortfloatdoublecomplexenum’或它们的任何等价物’typedef'。 'T,其中`T’是等效的Julia位类型(根据上表)。 如果’T’是`enum’的枚举,则参数的类型必须等效于’Cint’或’Cuint'。 **参数的值被复制(按值返回)。

  • 'struct T'(包括结构的typedef)。 'T`,其中`T’是Julia的最终类型。 参数的值被复制(按值返回)。

  • '无效*` 取决于参数的使用方式;首先将其转换为所需的指针类型,然后根据列表中的进一步规则确定等效的Julia类型。 此参数可以声明为'Ptr{Cvoid}',如果它真的只是一个未知的指针。

  • 'jl_value_t’ '任何` 参数的值必须是有效的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**')。 C代码*不得*更改'Ref返回的内存区域的内容{T}'如果’T’是’isbits’的类型。 如果内存由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

返回control后,可以使用`width[]`和`range[]`获取`width`和`range`变量的内容(如果它被`foo`函数更改),即它们充当零维数组。

C壳的例子

让我们从一个返回`Ptr`类型的C shell的简单示例开始:

mutable struct gsl_permutation
end

# Соответствующая сигнатура в C имеет вид
#     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 # Не удалось выделить память
        throw(OutOfMemoryError())
    end
    return output_ptr
end

在https://www.gnu.org/software/gsl /[GNU科学库](这里假设它可以通过名称`:libgsl`获得)不透明指针`gsl_permutation*被定义为C函数`gsl_permutation_alloc’的返回类型。 由于用户代码不需要访问’gsl_permutation’结构,因此要实现相应的Julia shell,只需声明一个新类型`gsl_permutation`就足够了,它没有内部字段,其唯一目的是在参数中指定类型`Ptr。 对于功能 ccall'返回类型'+Ptr’已被声明{gsl_permutation}+,因为使用`output_ptr’指针分配并指向的内存区域由C环境控制。

输入参数’n`是按值传递的,因此函数输入数据的签名有一个非常简单的形式:::Csize_t'。 不需要’Ref’或’Ptr'。 (如果shell调用Fortran函数,则相应的输入数据签名将是'+::Ref{Csize_t}+,因为Fortran中的变量是通过指针传递的。)而且’n’可以具有可以转换为整数值`Csize_t’的任何类型;函数 ccall'隐式调用 '基地。cconvert(Csize_t,n)

这里是另一个例子,这个时候对应的析构函数的shell:

# Соответствующая сигнатура в C имеет вид
#     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数组的第三个例子:

# Соответствующая сигнатура в C имеет вид
#    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返回一个整数错误代码,计算Bessel函数J的结果输入Julia’result_array’数组。 此变量声明为'Ref{Cdouble}',因为为它分配了一个内存块并由Julia代码管理。 隐性挑战 '基。cconvert(Ref{Cdouble},result_array)`将Julia指向Julia数组数据结构的指针解压缩为C可以理解的形式。

Fortran的示例shell

在下面的示例中,使用’ccall`,调用Fortran标准库(libBLAS)中的函数来计算标量积。 请注意,这种情况下的参数比较与上面略有不同,因为比较是从Julia到Fortran进行的。 对于每种类型的参数,都指定了’Ref’或’Ptr'。 这种转换约定可能取决于特定的Fortran编译器和操作系统,并且很可能没有反映在文档中。 但是,许多Fortran编译器实现要求将每个参数类型包含在`Ref`(或`Ptr`,如果适用)中:

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`时,最好不要使用该函数 '指针'。 而是定义一个方法 '基地。cconvert'并将变量直接传递给'@ccall'。 @ccall’自动保护其所有参数免受垃圾回收,直到调用返回控制权。 如果C API将存储对Julia代码分配的内存的引用,则在将控制权返回到'@ccall’调用后,必须确保垃圾收集器可以访问该对象。 推荐的方法是将这些值存储在`Array’类型的全局变量中。{Ref,1},直到C库通知与他们的工作完成。

每当创建指向Julia数据的指针时,只要该指针正在使用,该数据就必须存在。 Julia中的许多方法,例如 "不安全负荷"`String',创建数据的副本,而不是控制缓冲区。 这允许您安全地从内存中删除(或修改)源数据,以便它不会影响Julia代码的执行。 一个重要的例外是方法 'unsafe_wrap',出于性能原因,它在共享模式下使用基本缓冲区(换句话说,控制它)。

垃圾收集器不保证对象的任何特定处理顺序。 换句话说,如果对象’a’包含对对象`b`的引用,并且两者都受到垃圾回收,则不能保证对象`b`将在`a`之后被消除。 如果对象’b’必须存在以消除对象’a',则需要不同的方法。

功能规格不一致

在某些情况下,所需库的名称或路径事先未知,必须在运行时确定。 在这种情况下,库组件规范可以是函数调用,例如`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

CFUNCTION闭包函数

第一个论点 '@cfunction'可以用符号`$`标记。 在这种情况下,返回值将是一个带有闭包参数的"结构CFunction"对象。 这个返回的对象必须存在,直到它的使用完成。 Cfunction指针的内容和代码将被函数销毁 `finalizer'删除此链接并退出后。 这通常不是必需的,因为在C中没有这样的功能,但是当使用设计不佳的Api时,它可能很有用,这些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})
    # Здесь `callback` — это функция Base.CFunction, которая будет преобразована в Ptr{Cvoid}
    # (и защищена от ликвидации) посредством ccall
    @ccall qsort(a::Ptr{T}, length(a)::Csize_t, Base.elsize(a)::Csize_t, callback::Ptr{Cvoid})
    # Вместо этого можно использовать:
    #    GC.@preserve callback begin
    #        use(Base.unsafe_convert(Ptr{Cvoid}, callback))
    #    end
    # если функцию необходимо использовать вне вызова `ccall`
    return a
end

闭包函数 '@cfunction'使用LLVM跳板,并非在所有平台上都可用(例如,在ARM和PowerPC上不可用)。

关闭图书馆

有时关闭(卸载)库以便可以再次下载它是有用的。 例如,在编写与Julia一起使用的C代码时,您可能需要编译,从Julia调用C代码,然后关闭库,进行更改,再次编译并上传新的更改。 为此,您可以重新启动Julia,或者使用`Libdl’函数显式管理库,如下所示。

lib = Libdl.dlopen("./my_lib.so") # Открываем библиотеку явным образом.
sym = Libdl.dlsym(lib, :my_fcn)   # Получаем символ для вызываемой функции.
@ccall $sym(...) # Используем указатель `sym` вместо кортежа library.symbol.
Libdl.dlclose(lib) # Закрываем библиотеку явным образом.

请注意,当将`@ccall’与输入数据一起使用时(例如,@ccall'。/my_lib.so ".my_fcn(...)::Cvoid)库隐式打开,可能不会显式关闭。

具有可变数量参数的函数调用

要使用可变数量的参数调用C函数,可以用分号(semicolon)将所需的参数与参数列表中可变数量的参数分开。 下面是调用’printf’函数的示例。

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

界面 link:@id ccall-interface['ccall']

这是另一个接口,可以作为`@ccall’的替代品。 它有点不太方便,但它可以让你设置 挑战协议

方法 'ccall'具有以下参数:

  1. Couple`(:function,"library")`(最常),

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

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

  2. 函数的返回类型

  3. 函数签名对应的输入类型的元组。 在参数类型的单例元组中,不要忘记在末尾放一个逗号。

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

在`(:function,"library")'中,返回类型和输入类型必须是字面常量(也就是说,它们不能是变量;但是,请注意该部分 功能规格不一致)。

如果定义了containing方法,则在编译时计算其余参数。

宏和函数接口之间的转换表如下所示。

'@ccall` 'ccall`

'@ccall时钟()::Int32`

'ccall(:时钟,Int32,())`

'@ccall f(a::Cint)::Cint`

'ccall(:a,Cint,(Cint,),a)`

'@ccall"mylib"。f(a::Cint,b::Cdouble)::Cvoid`

'ccall((:f,"mylib"),Cvoid,(Cint,Cdouble),(a,b))`

'@ccall$fptr。f()::Cvoid`

'ccall(fptr,f,Cvoid,())`

'@ccall printf("%s=%d\n"::Cstring;"foo"::Cstring,foo::Cint)::Cint`

<不可用>

'@ccall printf("%s=%s\n"::Cstring;"2+2"::Cstring,"5"::Cstring)::Cint`

'ccall(:printf,Cint,(Cstring,Cstring...),"%s=%s\n", "2 + 2", "5")`

<不可用>

'ccall(:gethostname,stdcall,Int32,(Ptr{UInt8},UInt32),hn,长度(hn))`

挑战协议

'Ccall’调用的第二个参数(紧接在返回类型之前)可以是调用协议说明符(目前,@ccall`宏不允许您指定调用协议)。 如果未指定说明符,则使用平台默认采用的c调用约定。 此外,还支持以下约定’stdcall','cdecl,`fastcall’和’thiscall'(在64位Windows系统上无效)。 例如,这就是对`gethostname’ccall`函数的调用的样子,类似于上面的函数,但具有正确的windows签名(代码取自`base/libc。jl'):

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

有关详细信息,请参阅https://llvm.org/docs/LangRef.html#calling-conventions [LLVM语言参考]。

还有一个特殊的呼叫协议 'llvmcall',它允许您直接将调用插入内部LLVM指令。 这在为gpgpu等不寻常的平台编写代码时特别有用。 例如,对于https://llvm.org/docs/NVPTXUsage.html [CUDA]您可能需要获取流索引。:

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

与任何ccall调用一样,获取参数的正确签名非常重要。 另外,请注意,与Core提供的Julia函数不同,没有兼容性包装器可以确保内部指令对于当前目的是正确和有效的。本征'。

访问全局变量

本机库提供的全局变量可以使用函数按名称访问 'cglobal'。 函数参数 'cglobal'是一个符号规范(与调用相同 'ccall')和变量中存储的值的类型:

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

结果是指向值的地址的指针。 使用此指针,您可以使用函数对值执行操作 "不安全负荷"'unsafe_store!`.

'Errno’符号可能不存在于libc库中:它取决于您的系统的编译器实现的功能。 通常,标准库的符号应仅通过名称引用,以便编译器替代所需的符号。 此外,由于此示例中显示的`errno`符号在大多数编译器中是特殊的,因此在您的情况下,含义可能会有所不同。 当在任何支持多线程的系统上用C编译类似的代码时,可能会调用另一个函数(通过重载宏处理器),这将给出与此处呈现的值不同的结果。

通过指针访问数据

下面介绍的方法是不安全的,因为不正确的指针或类型声明可能导致Julia突然停止工作。

如果指针是'Ptr{T}',类型’T’的内容,作为一项规则,可以通过调用`unsafe load(ptr,[index])从它引用Julia对象的内存区域复制。 索引参数是可选的(默认情况下它是1),并根据Julia中采用的约定从1索引。 此函数的行为故意使其类似于函数的行为 'getindex''setindex!'(例如,语法匹配[]'`。

返回一个新对象,其中包含指针引用的内存区域内容的副本。 之后,可以安全地释放此存储器区域。

如果`T’的类型是’Any',则假定内存区域包含对Julia对象的引用(jl_value_t*')。 结果将是对此对象的引用,并且对象本身不会被复制。 在这种情况下,有必要确保垃圾回收器可以访问该对象(不考虑指针,不像新引用),以便内存不会提前清除。 请注意:如果对象最初不是由Julia代码放置在内存中,则新对象不会被Julia垃圾回收器消除。 如果对象’Ptr’本身是’jl_value_t*',则可以使用函数将其转换回对Julia对象的引用 'unsafe_pointer_to_objref(ptr)。 (Julia`v’值可以转换为’jl_value_t’指针*'how'Ptr{Cvoid}'通过调用 'pointer_from_objref(v’。)

反向操作(将数据写入'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`添加整数时,指针会移位一定数量的_bytes,而不是元素。 因此,作为具有指针的算术运算的结果而获得的地址值不依赖于指针元素的类型。

螺纹安全

一些C库从另一个线程进行回调,并且由于Julia不是线程安全的语言,因此需要采取额外的预防措施。 特别是,您应该设置一个双层系统:C回调应该只安排执行"真正的"回调(通过Julia事件循环)。 为此,请创建一个对象 'AsyncCondition'并将函数应用于它 '等待':

cond = Base.AsyncCondition()
wait(cond)

传递给C的回调应该只进行调用 ccall'应用于:uv_async_send’并传递’cond。处理`作为参数。 不应该有内存分配或与Julia运行时的其他交互。

请注意,事件可以组合,以便对`uv_async_send’的多次调用可以导致激活条件的单个通知。

有关回调的其他信息

有关将回调传递给C库的更多信息,请参阅https://julialang.org/blog/2013/05/callback [此博客条目]。

C++

创建c绑定的工具++ 可以在包中找到https://github.com/JuliaInterop/CxxWrap.jl [CxxWrap]。