ccall 真的会转换指针传递的参数吗?

Does ccall really convert arguments passed by pointer?

考虑使用此本机函数的动态库 returns 数组中所有偶数(32 位无符号)数的总和:

uint32_t sum_of_even(const uint32_t *numbers, size_t length);

上面的函数实现是用Rust写成如下,并打包成C动态库。

use libc::size_t;
use std::slice;

#[no_mangle]
pub extern "C" fn sum_of_even(n: *const u32, len: size_t) -> u32 {
    let numbers = unsafe {
        assert!(!n.is_null());
        slice::from_raw_parts(n, len as usize)
    };

    numbers
        .iter()
        .filter(|&v| v % 2 == 0)
        .sum()
}

我编写了以下 Julia (v1.0.1) 包装函数:

lib = Libdl.dlopen(libname)
sumofeven_sym = Libdl.dlsym(lib, :sum_of_even)

sumofeven(a) = ccall(
    sumofeven_sym,
    UInt32,
    (Ptr{UInt32}, Csize_t),
    a, length(a)
)

文档多次指出 ccall 中的参数被转换为与 C 函数原型兼容(强调我的):

Each argvalue to the ccall will be converted to the corresponding argtype, by automatic insertion of calls to unsafe_convert(argtype, cconvert(argtype, argvalue)). (See also the documentation for unsafe_convert and cconvert for further details.) In most cases, this simply results in a call to convert(argtype, argvalue).

此外,当通过 Ptr{U}Array{T} 传递给 C 函数时,如果 T 和 U 两种类型不同,则调用无效,因为没有添加重新解释转换(部分Bits Types):

When an array is passed to C as a Ptr{T} argument, it is not reinterpret-cast: Julia requires that the element type of the array matches T, and the address of the first element is passed.

Therefore, if an Array contains data in the wrong format, it will have to be explicitly converted using a call such as trunc(Int32, a).

然而,这似乎并非如此。如果我故意传递一个带有另一种类型元素的数组:

println(sumofeven(Float32[1, 2, 3, 4, 5, 6]))

程序调用直接传递数组的 C 函数,没有转换值,也没有抱怨不同的元素类型,导致无意义的输出或分段错误。

如果我重新定义函数以接受 Ref{UInt32} 而不是 Ptr{UInt32},我将无法使用浮点数组调用它:

ERROR: LoadError: MethodError: Cannot `convert` an object of type Array{Float32,1} to an object of type UInt32
Closest candidates are:
  convert(::Type{T<:Number}, !Matched::T<:Number) where T<:Number at number.jl:6
  convert(::Type{T<:Number}, !Matched::Number) where T<:Number at number.jl:7
  convert(::Type{T<:Integer}, !Matched::Ptr) where T<:Integer at pointer.jl:23
  ...

但是,Ref 不是为数组设计的。

使示例与 Ptr{UInt32} 一起工作需要我指定 Array{UInt32} 作为输入类型 a(静态强制),或者首先 convert 数组更灵活的功能。

sumofeven(a:: Array{UInt32}) = ccall( # ← either this
    sumofeven_sym,
    UInt32,
    (Ptr{UInt32}, Csize_t),
    convert(Array{UInt32}, a), # ← or this
    length(a))

至此,我还是觉得自己的推理有差距。当文档说作为 Ptr{T} 传递给 C 的数组不是重新解释转换时,文档真正暗示的是什么?为什么 Julia 允许我在不进行任何显式转换的情况下传递不同元素类型的数组?

根据观点 (issue #29850),结果证明这要么是核心库中的错误,要么是非常误导的文档。函数 unsafe_convert 的行为从版本 0.4 更改为 0.5,在某种程度上使其比当前建议的更灵活。

根据此 commitunsafe_convert 从此更改为:

unsafe_convert(::Type{Ptr{Void}}, a::Array) = ccall(:jl_array_ptr, Ptr{Void}, (Any,), a)

为此:

unsafe_convert{S,T}(::Type{Ptr{S}}, a::AbstractArray{T}) = convert(Ptr{S}, unsafe_convert(Ptr{T}, a))

对于数组,这种宽松的实现将实现从 T 数组到另一种类型 S 的指针的转换。实际上,unsafe_convert(cconvert(array)) 将重新解释-强制转换数组的基指针,就像在 C++ 命名法中一样。我们留下了一个跨越 FFI 边界的危险的重新解释数组。

要点在于,将数组传递给 C 函数时需要格外小心,因为 C 调用函数参数中的数组元素类型不是静态强制执行的。在适用的情况下使用类型签名 and/or 显式转换。