I’ve been chasing down a segfault in SCS.jl on 0.7 (0.7.0-DEV.2725), and I believe I’ve narrowed it down to the following issue:
# Not standalone code
struct A
val::Float64
... # lots of plain data fields (isbits(A) == true)
end
struct B
a::Ptr{A}
end
function foo_()
a = Ref(A(10.0, #= extra fields... =#))
b = Ref(B(Base.unsafe_convert(Ptr{A}, a)))
# foo modifies b and a
ccall(:foo, Void, (Ptr{B},), b)
# the value of unsafe_load(b[].a).val is corrupted when foo first reads it ...
# but not if the following line in uncommented
# @show a
end
foo_()
I’m inferring from this that the compiler is seeing that a
is not needed after the construction of b
, and hence it aggressively “frees” the memory that backs a
even within the scope of foo_
. Is there a proper way to extend the lifetime of a
past the ccall
?
Edit: related discussion at Help with ccall to a clib function
I am kinda surprised that this compiles at all. I always thought that taking addresses of stack-objects (local variables) was forbidden/undefined in julia.
A very simple workaround is to create a boxed A and B, e.g. by putting them into an array. This costs an allocation, so you should store them someplace for reuse if you plan to call foo_() often.
A more explicit workaround would be to @gc_preserve a until the ccall is finished. But until someone more knowledgeable chimes in to tell us that this is allowed, I would not trust taking addresses of any stack objects…
@gc_preserve
seems to be used in similar ccall
situations within Base
, and it fixes the segfaults, thanks! I’m surprised it’s not mentioned in the “Calling C and Fortran code” documentation (unless there’s a more official workaround).
My understanding was that using Ref
should be equivalent to but more efficient than creating a 1-element array.
IIUC the problem is the Base.unsafe_convert(Ptr{A}, a)
without the @gc_preserve
since the GC “loses” track of the Ptr. Still the definition of B
is problematic since I don’t think you can use @gc_preserve
in situations like
function f()
a = Ref(A(10.0, #= extra fields... =#))
b = Ref(B(Base.unsafe_convert(Ptr{A}, a)))
return b
end
The GC is now free to get rid of a
…
Yes, but that’s not the situation here. The fix is now in action at https://github.com/JuliaOpt/SCS.jl/blob/f1e41834ef8488c5d26dee16e71b39626f0b84f5/src/c_wrapper.jl#L66 for anyone who wants to see a more complex example.