Calling C and Fortran Code

Calling C and Fortran Code

尽管大多数代码都可以用Julia编写,但是已经有许多高质量的,成熟的数字计算库已经用C和Fortran编写. 为了轻松使用此现有代码,Julia使调用C和Fortran函数变得简单高效. Julia具有"没有样板"的理念:可以直接从Julia调用函数,而无需任何"胶合"代码,代码生成或编译-甚至从交互式提示中也可以. 只需使用ccall语法进行适当的调用ccall ,这看起来像普通的函数调用.

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

共享库和函数由(:function, "library")("function", "library")形式的元组引用,其中function是C导出的函数名,而library指的是共享库名. (特定于平台的)加载路径中可用的共享库将按名称解析. 也可以指定库的完整路径.

函数名称可以代替元组单独使用(只是:function"function" ). 在这种情况下,名称将在当前进程中解析. 该格式可用于调用C库函数,Julia运行时中的函数或链接到Julia的应用程序中的函数.

By default, Fortran compilers generate mangled names (for example, converting function names to lowercase or uppercase, often appending an underscore), and so to call a Fortran function via ccall you must pass the mangled identifier corresponding to the rule followed by your Fortran compiler. Also, when calling a Fortran function, all inputs must be passed as pointers to allocated values on the heap or stack. This applies not only to arrays and other mutable objects which are normally heap-allocated, but also to scalar values such as integers and floats which are normally stack-allocated and commonly passed in registers when using C or Julia calling conventions.

最后,您可以使用ccall实际生成对库函数的调用. ccall的参数为:

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

    OR

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

    OR

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

  2. 函数的返回类型

  3. 输入类型的元组,对应于功能签名

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

Note

(:function, "library")对,返回类型和输入类型必须为文字常量(即,它们不能为变量,但请参见下面的非常量函数规范 ).

定义包含方法时,将在编译时评估其余参数.

Note

有关如何将C类型映射到Julia类型的信息,请参见下文.

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

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

julia> t
2292761

julia> typeof(ans)
Int32

clock不带参数,并返回Int32 . 一个常见的陷阱是必须用尾随逗号编写一个1元组的参数类型. 例如,要调用getenv函数以获取指向环境变量值的指针,可以进行如下调用:

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

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

请注意,参数类型元组必须写为(Cstring,) ,而不是(Cstring) . 这是因为(Cstring)只是用括号括起来的表达式Cstring ,而不是包含Cstring的1元组:

julia> (Cstring)
Cstring

julia> (Cstring,)
(Cstring,)

在实践中,特别是在提供可重用功能时,通常将ccall用途包装在Julia函数中,以建立参数,然后以C或Fortran函数指示它们的任何方式检查错误,并传播给Julia调用方作为异常. 这一点特别重要,因为众所周知,C和Fortran API在指示错误情况的方式上是不一致的. 例如, getenv C库函数包装在以下Julia函数中,该函数是env.jl实际定义的简化版本:

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

C getenv函数通过返回NULL指示错误,但是其他标准C函数以各种不同的方式来指示错误,包括通过返回-1、0、1和其他特殊值. 如果调用方尝试获取不存在的环境变量,则此包装器将引发异常,以明确指示问题:

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

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

这是一个稍微复杂的示例,它发现本地计算机的主机名. 在此示例中,假定网络库代码位于名为" libc"的共享库中. 实际上,此函数通常是C标准库的一部分,因此应省略" libc"部分,但我们希望在此处显示此语法的用法.

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

此示例首先分配一个字节数组,然后调用C库函数gethostname以用主机名填充该数组,获取指向主机名缓冲区的指针,然后将指针转换为Julia字符串(假定它是NUL终止) C字符串. C库通常使用这种模式,要求调用者分配要传递给被调用者并填充的内存.像这样从Julia分配内存通常是通过创建一个未初始化的数组并将指向其数据的指针传递给它来完成的. C函数. 这就是为什么我们在这里不使用Cstring类型的原因:由于数组未初始化,因此它可能包含NUL字节. 作为Cstring一部分转换为ccall检查包含的NUL字节,因此可能引发转换错误.

Creating C-Compatible Julia Function Pointers

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

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

@cfunction生成与C兼容的函数指针,用于对Julia函数的调用. @cfunction的参数为:

  1. 朱莉娅函数
  2. 函数的返回类型
  3. 输入类型的元组,对应于功能签名
Note

As with ccall, the return type and tuple of input types must be literal constants.

Note

当前,仅支持平台默认的C调用约定. 这意味着@cfunction生成的指针不能在WINAPI需要32位Windows上的stdcall函数的调用中使用,而可以在WIN64( stdcall与C调用约定统一在一起)上使用.

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

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

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

现在,假设我们要使用qsort函数(而不是Julia内置的sort函数) sort Julia中的一维值数组A进行sort . 在担心调用qsort和传递参数之前,我们需要编写一个比较函数:

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

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

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

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

@cfunction需要三个参数:Julia函数( mycompare ),返回类型( Cint )和输入参数类型的文字元组,在这种情况下, Cdouble Float64数组( Float64 )进行排序.

qsort的最终调用如下所示:

julia> A = [1.3, -2.7, 4.4, 3.1]
4-element Array{Float64,1}:
  1.3
 -2.7
  4.4
  3.1

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

julia> A
4-element Array{Float64,1}:
 -2.7
  1.3
  3.1
  4.4

可以看出, A更改为排序后的数组[-2.7, 1.3, 3.1, 4.4] . 请注意,Julia 负责将数组转换为Ptr{Cdouble} ,以字节为单位计算元素类型的大小,依此类推.

为了好玩,尝试插入println("mycompare($a, $b)")行成mycompare ,这将让你看到比较qsort的执行(并验证它是否真的调用朱莉娅功能,你传递对此).

Mapping C Types to Julia

It is critical to exactly match the declared C type with its declaration in Julia. Inconsistencies can cause code that works correctly on one system to fail or produce indeterminate results on a different system.

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

Automatic Type Conversion

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

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

将表现为以下内容:

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

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

Base.unsafe_convert处理到Ptr类型的转换. 认为这是不安全的,因为将对象转换为本机指针可能会将对象隐藏到垃圾回收器中,从而导致其过早释放.

Type Correspondences

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

语法/关键字ExampleDescription
mutable structBitSet "叶类型" ::一组包含类型标签的相关数据,由Julia GC管理,并由对象标识定义. 叶子类型的类型参数必须完全定义(不允许使用TypeVars ),以便构造实例.
abstract typeAny, AbstractArray{T, N}, Complex{T}"超级类型" ::不能实例化但可以用来描述一组类型的超级类型(不是叶子类型).
T{A}Vector{Int}"类型参数" ::类型的特殊化(通常用于调度或存储优化).
" TypeVar" ::类型参数声明中的T称为TypeVar(类型变量的缩写).
primitive typeInt, Float64 "原始类型" ::没有字段,但是有大小的类型. 它是按值存储和定义的.
structPair{Int, Int} " Struct" ::一种类型,所有字段都定义为常量. 它是按值定义的,可以与类型标签一起存储.
ComplexF64 (isbits) " Is-Bits" ::一个primitive type ,或者是一个struct类型,其中所有字段都是其他isbits类型. 它是按值定义的,存储时没有类型标签.
struct ...; endnothing" Singleton" ::没有字段的叶子类型或结构.
(...) or tuple(...)(1, 2, 3) "元组" ::与匿名结构类型相似的不变数据结构,或常量数组. 表示为数组或结构.

Bits Types

有几种特殊类型需要注意,因为无法定义其他类型来表现相同:

在我们当前支持的所有系统上,基本C / C ++值类型可以按以下方式转换为Julia类型. 每个C型也有相应的朱莉娅类型,具有相同的名称,由C.该前缀可帮助编写移植代码(和记住的int在C是不一样的Int在茱莉亚).

系统独立类型

C名Fortran名称标准茱莉亚·阿里亚斯(Julia Alias)朱莉娅基本类型
unsigned charCHARACTERCucharUInt8
bool (仅在C ++中)CucharUInt8
shortINTEGER*2, LOGICAL*2CshortInt16
unsigned shortCushortUInt16
intBOOL (C,典型)INTEGER*4, LOGICAL*4CintInt32
unsigned intCuintUInt32
long longINTEGER*8, LOGICAL*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 and [[noreturn]] or _NoreturnUnion{}
void*Ptr{Cvoid}
T* (其中T代表适当定义的类型)Ref{T}
char* (或char[] ,例如字符串)CHARACTER*NCstring如果为NUL终止),否则为Ptr{UInt8}
char** (or *char[])Ptr{Ptr{UInt8}}
jl_value_t* (任何Julia类型)Any
jl_value_t** (对Julia类型的引用)Ref{Any}
va_arg不支持
... (可变函数规范)T... (其中T是上述类型之一,不支持不同参数类型的变量函数)

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

系统相关类型

C名标准茱莉亚·阿里亚斯(Julia Alias)朱莉娅基本类型
charCcharInt8 (x86,x86_64), UInt8 (powerpc,arm)
longClongInt (UNIX), Int32 (Windows)
unsigned longCulongUInt (UNIX), UInt32 (Windows)
wchar_tCwchar_tInt32 (UNIX), UInt16 (Windows)
Note

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

Warning

对于字符串参数( char* ),Julia类型应该为Cstring (如果期望使用NUL终止的数据),或者为Ptr{Cchar}Ptr{UInt8}否则(这两种指针类型具有相同的作用),如上所述,而不是String . 类似地,对于数组参数( T[]T* ),Julia类型应该再次为Ptr{T} ,而不是Vector{T} .

Warning

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

Warning

Union{}返回类型表示该函数将不返回,即C ++ 11 [[noreturn]]或C11 _Noreturn (例如jl_throwlongjmp ). 不要将此函数用于不返回任何值( void )但确实返回的函数,而应使用Cvoid .

Note

对于wchar_t*参数,Julia类型应该为Cwstring (如果C例程期望以NUL终止的字符串)或否则为Ptr{Cwchar_t} . 还请注意,Julia中的UTF-8字符串数据在内部是NUL终止的,因此可以将其传递给期望NUL终止的数据的C函数,而无需进行复制(但是,如果字符串本身使用Cwstring类型,则将引发错误)包含NUL个字符).

Note

可以通过在Julia中使用Ptr{Ptr{UInt8}}类型来调用采用char**类型参数的C函数. 例如,C函数的形式为:

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

可以通过下面的Julia代码调用:

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

对于采用字符类型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))
Warning

Fortran编译器也可以增加其他隐藏参数为指针,假定形状( : )和假定大小( * )阵列. 可以通过使用ISO_C_BINDING并在子例程的定义中包含bind(c)来避免这种行为,强烈建议对可互操作的代码进行这种操作. 在这种情况下,将没有隐藏的参数,这是以某些语言功能为代价的(例如,仅允许character(len=1)传递字符串).

Note

声明为返回Cvoid AC函数将在Julia中nothing返回任何值.

Struct Type Correspondences

可以通过使用具有相同字段布局的struct定义在Julia中镜像复合类型,也就是C中的struct或Fortran90中的TYPE (或F77的某些变体中的STRUCTURE / RECORD ).

递归使用时, isbits类型将内联存储. 所有其他类型都存储为指向数据的指针. 当在C中的另一个结构中镜像按值使用的结构时,必须不要尝试手动复制字段,因为这将无法保持正确的字段对齐. 相反,声明一个isbits结构类型并使用它. 未命名的结构无法翻译成Julia.

Packed structs and union declarations are not supported by Julia.

如果您先验地知道将具有最大大小(可能包括填充)的字段,则可以得到一个union的近似值. 将字段转换为Julia时,请声明Julia字段仅属于该类型.

参数数组可以用NTuple表示. 例如,用C表示法的结构写为

struct B {
    int A[3];
};

b_a_2 = B.A[2];

可以用朱莉娅写成

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)

Type Parameters

当定义包含用法的方法时,将静态评估ccall@cfunction的类型参数. 因此,它们必须采用文字元组而不是变量的形式,并且不能引用局部变量.

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

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

SIMD Values

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

如果C / C ++例程的参数或返回值是本机SIMD类型,则对应的Julia类型是VecElement的同类元组,该元组自然地映射到SIMD类型. 特别:

例如,考虑使用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((:dist, "libdist"), m256, (m256, m256), a, b)
end

println(call_dist(a,b))

主机必须具有必需的SIMD寄存器. 例如,上面的代码在没有AVX支持的主机上将不起作用.

Memory Ownership

malloc/free

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

When to use T, Ptr{T} and Ref{T}

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

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

Mapping C Functions to Julia

ccall / @cfunction argument translation guide

要将C参数列表转换为Julia:

ccall / @cfunction return type translation guide

要将C返回类型转换为Julia:

Passing Pointers for Modifying Inputs

由于C不支持多个返回值,因此C函数通常会使用指向该函数将修改的数据的指针. 要在ccall完成此ccall ,您首先需要将该值封装在适当类型的Ref{T}中. 当您将此Ref对象作为参数传递时,Julia将自动将C指针传递给封装的数据:

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

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

C Wrapper Examples

让我们从返回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(
        (:gsl_permutation_alloc, :libgsl), # name of C function and library
        Ptr{gsl_permutation},              # output type
        (Csize_t,),                        # tuple of input types
        n                                  # name of Julia variable to pass in
    )
    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查看gsl_permutation结构内部,因此相应的Julia包装器仅需要一个新的类型声明gsl_permutation ,该声明没有内部字段,其唯一目的是放在Ptr类型的type参数中. ccall的返回类型声明为Ptr{gsl_permutation} ,因为output_ptr分配和指向的内存由C控制.

输入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::Ref{gsl_permutation})
    ccall(
        (:gsl_permutation_free, :libgsl), # name of C function and library
        Cvoid,                             # output type
        (Ref{gsl_permutation},),          # tuple of input types
        p                                 # name of Julia variable to pass in
    )
end

在这里,输入p声明为Ref{gsl_permutation}类型,这意味着p指向的内存可以由Julia或C管理.指向C分配的内存的指针应为Ptr{gsl_permutation}类型,但是它可以使用Base.cconvert转换,因此

现在,如果您仔细看一下这个示例,您可能会发现它是不正确的,考虑到我们上面对首选声明类型的解释. 你看到了吗? 我们正在调用的函数将释放内存. 不能为此类操作指定Julia对象(它会崩溃或导致内存损坏). 因此,可能最好将p类型声明为Ptr{gsl_permutation } ,以使用户错误地将另一种对象传递给那里,而不是通过gsl_permutation_alloc获得的对象.

如果C包装程序从不希望用户将指针传递给Julia所管理的内存,则可以使用p::Ptr{gsl_permutation}作为包装程序的方法签名,并且类似地在ccall中也可以接受.

这是传递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(
        (:gsl_sf_bessel_Jn_array, :libgsl), # name of C function and library
        Cint,                               # output type
        (Cint, Cint, Cdouble, Ref{Cdouble}),# tuple of input types
        nmin, nmax, x, result_array         # names of Julia variables to pass in
    )
    if errorcode != 0
        error("GSL error code $errorcode")
    end
    return result_array
end

包装的C函数返回整数错误代码; Bessel J函数的实际求值结果将填充Julia数组result_array . 此变量被声明为Ref{Cdouble} ,因为它的内存是由Julia分配和管理的. 对Base.cconvert(Ref{Cdouble}, result_array)的隐式调用将指向Julia数组数据结构的Julia指针解压缩为C可以理解的形式.

Fortran Wrapper Example

下面的示例利用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((:ddot_, "libLAPACK"),
                    Float64,
                    (Ref{Int32}, Ptr{Float64}, Ref{Int32}, Ptr{Float64}, Ref{Int32}),
                    n, DX, incx, DY, incy)
    return product
end

Garbage Collection Safety

将数据传递给ccall ,最好避免使用pointer函数. 而是定义一个convert方法并将变量直接传递给ccall . ccall自动安排所有参数从垃圾回收中保留,直到调用返回. 如果C API将存储对Julia分配的内存的引用,则在ccall返回之后,必须安排该对象对垃圾收集器仍然可见. 解决此问题的建议方法是,使类型为Array{Ref,1}的全局变量保存这些值,直到C库通知您已完成它们.

Whenever you have created a pointer to Julia data, you must ensure the original data exists until you are done with using the pointer. Many methods in Julia such as unsafe_load and String make copies of data instead of taking ownership of the buffer, so that it is safe to free (or alter) the original data without affecting Julia. A notable exception is unsafe_wrap which, for performance reasons, shares (or can be told to take ownership of) the underlying buffer.

垃圾收集器不保证任何终结命令. 也就是说,如果a包含对b的引用,并且ab都应进行垃圾回收,则不能保证b将在a之后完成. 如果正确确定a取决于b是否有效,则必须以其他方式进行处理.

Non-constant Function Specifications

(name, library)函数规范必须是一个常量表达式. 但是,可以通过在eval进行分段来按以下方式将计算值用作函数名:

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

该表达式使用string构造一个名称,然后将该名称替换为新的ccall表达式,然后对其求值. 请记住, eval仅在最高级别上运行,因此在此表达式中本地变量将不可用(除非它们的值替换为$ ). 因此, eval通常仅用于形成顶级定义,例如在包装包含许多相似功能的库时. 可以为@cfunction构造类似的示例.

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

Indirect Calls

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

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

macro dlsym(func, lib)
    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("myfunc", mylibvar), Cvoid, ())

Closure cfunctions

@cfunction的第一个参数可以用$标记,在这种情况下,返回值将是struct CFunction ,该函数在参数上封闭. 您必须确保该返回对象保持活动状态,直到完成所有使用. 当删除并退出该引用时,将通过finalizer擦除cfunction指针处的内容和代码. 由于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, Cvoid, (Ptr{T}, Csize_t, Csize_t, Ptr{Cvoid}),
        a, length(a), Base.elsize(a), callback)
    # 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

Closing a Library

有时关闭(卸载)库以重新加载它很有用. 例如,在开发供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 (symbol, library) tuple (remaining arguments are the
same).  Libdl.dlclose(lib) # Close the library explicitly.

请注意,在将ccall与元组输入一起使用时(例如ccall((:my_fcn, "./my_lib.so"), ...) )),该库将隐式打开,并且可能未明确关闭.

Calling Convention

ccall的第二个参数可以选择是调用约定说明符(紧邻返回类型). 没有任何说明符,将使用平台默认的C调用约定. 其他受支持的约定是: stdcallcdeclfastcallthiscall (在64位Windows上为no-op). 例如(从base/libc.jl ),我们看到与上面相同的gethostname ccall ,但具有适用于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函数不同,没有兼容性层可以确保内部Core.Intrinsics有意义并可以在当前目标上Core.Intrinsics .

Accessing Global Variables

可以使用cglobal函数按名称访问由本机库导出的全局变量. 到参数cglobal是通过使用一个符号规范完全相同ccall ,并且描述存储在该变量中的值的类型:

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

结果是一个给出值地址的指针. 可以使用unsafe_loadunsafe_store!通过此指针操纵该值unsafe_store! .

Note

在名为" libc"的库中可能找不到此errno符号,因为这是系统编译器的实现细节. 通常,仅应按名称访问标准库符号,从而允许编译器填写正确的符号. 另外,此示例中显示的errno符号在大多数编译器中都是特殊的,因此此处看到的值可能不是您期望或想要的值. 在任何具有多线程功能的系统上用C编译等效代码通常实际上会调用一个不同的函数(通过宏预处理器重载),并且给出的结果可能不同于此处打印的旧值.

Accessing Data through a Pointer

下列方法被描述为"不安全",因为错误的指针或类型声明会导致Julia突然终止.

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

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

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

The reverse operation (writing data to a Ptr{T}), can be performed using unsafe_store!(ptr, value, [index]). Currently, this is only supported for primitive types or other pointer-free (isbits) immutable struct types.

引发错误的任何操作当前可能尚未实现,应将其发布为错误,以便可以解决.

如果感兴趣的指针是纯数据数组(原始类型或不可变结构),则函数unsafe_wrap(Array, ptr,dims, own = false)可能更有用. 如果Julia应该"获得"底层缓冲区的所有权并在返回的Array对象完成时调用free(ptr) ,则final参数应该为true. 如果own参数被省略或为false,则调用者必须确保在所有访问完成之前缓冲区仍然存在.

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

Thread-safety

一些C库从其他线程执行其回调,并且由于Julia不是线程安全的,因此您需要采取一些额外的预防措施. 特别是,您将需要建立一个两层系统:C回调仅应计划 (通过Julia的事件循环)"真实"回调的执行. 为此,创建一个AsyncCondition对象并wait它:

cond = Base.AsyncCondition()
wait(cond)

传递到C回调应该只执行一个ccall:uv_async_send ,路过cond.handle作为参数,同时注意避免与朱莉娅运行的任何拨款或其他交互.

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

More About Callbacks

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

C++

有关直接C ++接口的信息,请参见Cxx软件包. 有关创建C ++绑定的工具,请参见CxxWrap程序包.

[1]

可以内联C和Julia中的非库函数调用,因此与共享库函数的调用相比,其开销甚至更低. 上面的要点是,实际执行外部函数调用的成本与使用任何一种本地语言进行调用的成本大约相同.

[2]

Clang包可用于从C头文件自动生成Julia代码.

by  ICOPY.SITE