【发布时间】: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 函数原型兼容(强调我的):
每个
argvalue到ccall都将通过自动插入对unsafe_convert(argtype, cconvert(argtype, argvalue))的调用转换为相应的argtype。 (有关更多详细信息,另请参阅unsafe_convert和cconvert的文档。)在大多数情况下,这只会导致调用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 事实上,
Ptr和Ref恰好使转化行为不同。我已编辑问题以包含该详细信息。 -
除了类型检查部分之外,未定义的错误可能是由于您没有创建变量并将其传递给
ccall,您直接传递了一个可能由于GC而产生错误的表达式问题(这只是一个猜测)。我认为当数组类型和T不匹配时,曾经有一个warning。unsafe_convert(::Type{Ptr{S}}, a::AbstractArray{T}) where {S,T} = convert(Ptr{S}, unsafe_convert(Ptr{T}, a))inpointer.jl处理类型不同时我认为反过来重新解释调用bitcast的指针。 -
我认为应该将此行为报告为问题。
-
@hckr 这很有见地,但也很有趣。在这种情况下,参数函数
a不应该表现得像一个变量吗?由于本机函数甚至不保留指向数组的指针,我会说数组仍然存在,只要它必须存在。执行ccall(sumofeven_sym, UInt32, (Ptr{UInt32}, Csize_t), UInt32[1,2,3], 3)在实践中似乎也可以正常工作,尽管这并不能证明太多:UB 确实以神秘的方式工作。不过,我会在上游报告这个!
标签: arrays pointers julia implicit-conversion ffi