【问题标题】:Does ccall really convert arguments passed by pointer?ccall 真的会转换指针传递的参数吗?
【发布时间】:2023-04-09 15:43:01
【问题描述】:

考虑一个带有这个返回数组中所有偶数(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 函数原型兼容(强调我的):

每个argvalueccall 都将通过自动插入对unsafe_convert(argtype, cconvert(argtype, argvalue)) 的调用转换为相应的argtype。 (有关更多详细信息,另请参阅 unsafe_convertcconvert 的文档。)在大多数情况下,这只会导致调用 convert(argtype, argvalue)

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

当一个数组作为Ptr{T} 参数传递给C 时,它不是reinterpret-cast:Julia 要求数组的元素类型匹配T,并且第一个元素被传递。

因此,如果Array 包含格式错误的数据,则必须使用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 让我在没有任何显式转换的情况下传递不同元素类型的数组?

【问题讨论】:

  • 我对 FFI 一无所知,但文档中的 this part 似乎建议您应该使用 Ref 而不是 Ptr,因为输入数组应该是由 Julia 管理。
  • @phg 事实上,PtrRef 恰好使转化行为不同。我已编辑问题以包含该详细信息。
  • 除了类型检查部分之外,未定义的错误可能是由于您没有创建变量并将其传递给ccall,您直接传递了一个可能由于GC而产生错误的表达式问题(这只是一个猜测)。我认为当数组类型和T 不匹配时,曾经有一个warningunsafe_convert(::Type{Ptr{S}}, a::AbstractArray{T}) where {S,T} = convert(Ptr{S}, unsafe_convert(Ptr{T}, a)) in pointer.jl 处理类型不同时我认为反过来重新解释调用bitcast的指针。
  • 我认为应该将此行为报告为问题。
  • @hckr 这很有见地,但也很有趣。在这种情况下,参数函数a 不应该表现得像一个变量吗?由于本机函数甚至不保留指向数组的指针,我会说数组仍然存在,只要它必须存在。执行ccall(sumofeven_sym, UInt32, (Ptr{UInt32}, Csize_t), UInt32[1,2,3], 3) 在实践中似乎也可以正常工作,尽管这并不能证明太多:UB 确实以神秘的方式工作。不过,我会在上游报告这个!

标签: arrays pointers julia implicit-conversion ffi


【解决方案1】:

事实证明,这要么是核心库中的错误,要么是一个非常误导的文档,具体取决于视角 (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 调用函数参数中数组的元素类型不是静态强制的。在适用的情况下使用类型签名和/或显式转换。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-10-13
    • 2013-12-24
    • 2012-05-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多