はじめに
Juliaはccall
あるいは@ccall
でCの関数を呼び出すことが手軽にできます。
しかし、公式ドキュメントの内容が誤解を招くっぽいので、適切な戻り値の型と引数の型の選択に迷ったユーザーがかなりあります。
この記事には、ポインタ型の選択ルールを簡単に解説します。
要点
ccall
のポインタ引数および戻り値のデータ型を指定する時、いつもPtr{T}
を使っても構いません。
例えば、Julia側の引数の型にかかわらず、下記のように書けます。
# C関数`foo`のシグネチャ:
# const unsigned char * foo(int x, float * y);
ccall(
(:foo, "libfoo"), # 関数名, ライブラリ名
Ptr{Cuchar}, # `foo`の戻り値の型
(Cint, Ptr{Cfloat}), # `foo`の引数1の型, 引数2の型
x, # Julia側の引数x
y, # Julia側の引数y
)
実際は、ccall
は普通の関数じゃなくて1、C関数にJulia側の引数を直接に渡すわけじゃないです。
引数yと引数2の型が一致しない場合には、Base.cconvert
とBase.unsafe_convert
による, 引数yの型が引数2の型と暗黙に変換されます。Base.cconvert
とBase.unsafe_convert
呼び出しの順番は下記のようになります2。
Base.unsafe_convert(Ptr{Cfloat}, Base.cconvert(Ptr{Cfloat}, 引数))
JuliaのBase
モジュールの中には、ここでのunsafe_convert
関数とcconvert
関数を事前に定義してありますので、引数の型を自動的に変換することができます。しかも、適切なメソッドが見つからない場合、オーバーロードする必要があります。
julia> methods(Base.cconvert)
# 21 methods for generic function "cconvert":
[1] cconvert(::Type{T}, x::Enum{T2}) where {T<:Integer, T2<:Integer} in Base.Enums at Enums.jl:20
[2] cconvert(::Type{Ref{T}}, t::Tuple{Vararg{T, N}}) where {N, T} in Base at refpointer.jl:170
...
julia> methods(Base.unsafe_convert)
# 69 methods for generic function "unsafe_convert":
[1] unsafe_convert(T::Union{Type{Ptr{Nothing}}, Type{Ptr{Base.Libc.FILE}}}, f::Base.Libc.FILE) in Base.Libc at libc.jl:94
[2] unsafe_convert(::Type{Ref{BigFloat}}, x::Ptr{BigFloat}) in Base.MPFR at mpfr.jl:130
[3] unsafe_convert(::Type{Ref{BigFloat}}, x::Ref{BigFloat}) in Base.MPFR at mpfr.jl:131
...
Ref{T}
を使うべき主な場面
Ref{T}
3は、実際にRefValue{T}
4として使う場面が多いです。
例えば、前文のJulia側の引数yは、下記のように定義します。
julia> y = Ref{Cfloat}(0.0f0)
Base.RefValue{Float32}(0.0f0)
なぜこれが必要なのかというと、JuliaのInt
, Float32
, Ptr{Float32}
といった__isbitstype
型は、固定のアドレスがないからです。__
C関数がこのJuliaから受け取ったアドレスに何を記入するかもしれないので、固定のアドレスを使わなければならないでしょう。
julia> pointer_from_objref(0.0f0)
ERROR: pointer_from_objref cannot be used on immutable objects
Stacktrace:
[1] error(s::String)
@ Base ./error.jl:33
[2] pointer_from_objref(x::Any)
@ Base ./pointer.jl:146
[3] top-level scope
@ REPL[13]:1
julia> pointer_from_objref(y)
Ptr{Nothing} @0x000000010f65f8b0
まとめ
-
ccall
/@ccall
に関わるポイント型を選択する必要がない。常にPtr{T}
が使えます。 -
デフォルトメソッドで、
Ptr{T}
に変換エラーが発生する時、Base.unsafe_convert
をオーバーロードする必要があります。 -
Cの関数を呼び出すために、Julia側のポインタ型変数を
Ref{T}
で定義するのがほとんどです。
クイズ
下記のコードの中に、間違えたところがありますか。なぜですか。
y = Ref{Cfloat}(0.0f0)
y1 = Base.cconvert(Ptr{Cfloat}, y)
y2 = Base.unsafe_convert(Ptr{Cfloat}, y1)
ccall((:foo, "libfoo"), Ptr{Cuchar}, (Cint, Ptr{Cfloat}), 0, y2)
y[]