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,在某种程度上使其比当前建议的更灵活。
根据此 commit,unsafe_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 显式转换。
考虑使用此本机函数的动态库 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 theccall
will be converted to the correspondingargtype
, by automatic insertion of calls tounsafe_convert(argtype, cconvert(argtype, argvalue))
. (See also the documentation forunsafe_convert
andcconvert
for further details.) In most cases, this simply results in a call toconvert(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 matchesT
, 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 astrunc(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,在某种程度上使其比当前建议的更灵活。
根据此 commit,unsafe_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 显式转换。