I need to pass a struct that may contain an array of its own type to a C library. To achieve that, I use the pointer function to obtain an address and encode that in an UInt64 field (as expected by the C library).
I’ve seen random crashes or corrupted data, so I’m guessing that I must be doing something wrong here. I wonder if it’s because the array ar got GC’ed even though the object is still around. If that’s the case, what would be the correct way to maintain the reference or avoid GC?
Here’s the MWE to illustrate the situation:
julia> struct Bar
val::UInt64
ty::UInt8
Bar(v::Float64) = new(reinterpret(UInt64, v), 0x00)
Bar(vv::Vector{Float64}) = begin
ar = Bar.(vv)
new(reinterpret(UInt64, pointer(ar)), 0x01)
end
end
So the value of val could either be a Float64 value or the address of an object of type Vector{Bar}. Here’s how to construct such objects:
You will need to keep a reference to ar somewhere, e.g. in a global Dict, maybe using the pointer as a key. That way, you can remove the arrays from the dict when you free the C struct by removing the element corresponding to the pointer.
Create the object and pass it into the C function. After the call is finished, the object is usually discarded.
The C function may return the struct. In which case, an unpack function is created to extract the data into Julia objects. It would do the same thing as listed above - a one dimensional array of native Julia values is returned ([2.0, 3.0])
So the risk lies with #1 - GC may kick in and overwrite what ar was pointing to with some garbage and the C call could fail.
#2 isn’t really a problem because the memory is allocated by the C library not by Julia, hence it’s not going to be picked up by GC.
I’m not sure how to maintain the pointer though. I went through the PyCall code but don’t really understand what it does. I understand a Dict isn’t performant but if that’s just a data structure thing then I will adjust according to my usage pattern and use a different data structure as necessary. Also, if maintain this pointer then I would have to register a finalizer function when the parent object is freed. Is that right?
Another idea is to create another wrapper struct that contains the julia data the underlying C struct. More concretely:
struct Foo
ar::Vector{Bar}
obj::Bar
end
and then the binding function would look like:
function callme(x)
ccall((:func, LIB), Ptr{Bar}, (Ptr{Bar},), x)
end
and I would have to deference to the object before calling the binding function:
Yes, this is the pycall approach. If this is an struct that you need to operate on directly, you can make a struct with an extra field that holds the reference to the julia object (i.e. give it three field with the third one being an Any).
No this is wrong. This will not keep the julia array live. Julia is free to disgard the ar field.
It seems that you only need to use this struct when passing stuff to C so in that case, you can simply create that struct at ccall callsite within cconvert/unsafe_convert. You can put all allocations into cconvert(::Type{Ptr{Bar}}, ::Vector{Float64}), and cconvert(::Type{Ptr{Bar}}, ::Float64) including allocation of a Ref{Bar} and return the Ref{Bar} in unsafe_convert. The ccall lowering will guarantee that the unsafe operatioins in unsafe_convert are safe assuming all the objects that needs to be alive are in the return value of cconvert.
Base.cconvert(::Type{Ptr{Bar}}, v::Float64) = Ref(Bar(v)) # This use the `::Float64` constructor
function Base.cconvert(::Type{Ptr{Bar}}, vv::Vector{Float64}) # This replaces the `::Vector{Float64}` constructor
ar = Bar.(vv)
bar = Bar(reinterpret(UInt64, pointer(ar)), 0x01)
return (Ref(bar), ar)
end
function Base.unsafe_convert(::Type{Ptr{Bar}}, t::Tuple{Ref{Bar},Vector{Bar}})
return pointer_from_objref(t[1])
end
If you are not using any julia object from their pointer values, you should never need to worry about object lifetime.
If the invisible object lifetime is bound to something visible in julia, which can be a function local scope or a julia object (the latter being the usecase in PyCall), you should not use any external state to keep the object live and should use the related julia object/scope to do so. This can mean using @gc_preserve or writing the correct unsafe_convert/cconvert since these are the only two local mechanism to keep object alive.
Only if the object is kept alive in a way that’s totally invisible to any local julia state (like if you store the julia address to a C global object) it is then needed to use an external/global state to manage/extend the lifetime of the julia object.
Mmm… I don’t understand how ar could be freed. The Foo object is holding a reference to ar until the ccall is finished. What am I missing?
Just so we’re on the same page, let’s say the struct now looks like this:
julia> struct Bar
val::UInt64
ty::UInt8
Bar(v::Float64) = new(reinterpret(UInt64, v), 0x00)
Bar(v::Vector{Bar}) = new(reinterpret(UInt64, pointer(v)), 0x01) # v could be gc'ed unless it's referenced elsewhere
end
julia> struct Foo
ar::Vector{Bar}
obj::Bar
end
Then, the ccall would like:
# binding function created by Clang.jl
function callme(x)
ccall((:func, LIB), Ptr{Bar}, (Ptr{Bar},), x)
end
# a useful business function...
function reachthesky()
ar = Bar.([2.0, 3.0])
obj = Bar(ar)
foo = Foo(ar, obj)
unpack(callme(Ref(foo.obj)))
end
The compiler could reorder stuff and replace your aggregate with a bunch of scalars such that Foo doesn’t even get constructed anymore etc etc. Perhaps it doesn’t do that now but in general, using the official way ( @gc_preserve) is a good idea (yes 0.7).
No. As mentioned above, the only two ways you can guarantee something is live locally are @gc_preserve and ccall.
If you only have one or two of these calls yes that’s fine. If you are calling this from different places using the unsafe_convert and cconvert would be better.
I could not make sense of this discussion nor the sample notebook to adapt to my specific use case.
Any help would be appreciated:
I have a Struct with a fixed size array field. I want to use Julia to build that struct and then call the C code which will do the heavy computation accessing the elements in the struct. The Julia struct does not need to stay in memory after the ccall.
Here is an example:
//C code
typedef struct {
double array[3];
} Mystruct;
int access_struct_array(Mystruct* mystruct)
{
for (int i = 0; i < 3; i++)
{
printf("%f\n", mystruct->array[i]);
}
return 0;
}
Then I use Julia to build the struct and call the C code
type Mystruct
array::Array{Float64, 1}
end
mystruct = Mystruct([1.1, 2.1, 3.1]);
ccall(("access_struct", "./shared.so"), Int, (Ref{Mystruct},), mystruct); #undefined behavior
You do not need to use this thread at all if all what you have is a C array instead of a C pointer. C arrays members are just, well, arrays (i.e. consecutive objects) so anything with a similar memory layout should work. You just need to construct such an object and pass it as reference. struct ::Float64; ::Float64; ::Float64 end, NTuple{3,Float64} or even just the array direction should work.