Why does this ccall segfault?

The following code segfaults for me:

C = """
typedef struct CParticle {
    double charge;
    double E;
    double weight;
} CParticle;


void simulate(void (*f)(CParticle x)) {
    CParticle cp;
    cp.charge = 0;
    cp.E = 1;
    cp.weight = 2;
    (*f)(cp);
    return;
}

"""

struct CParticle
    charge::Cdouble
    E::Cdouble
    weight::Cdouble
end

write("lib.c", C)
run(`gcc -c -fPIC lib.c -o lib.o`)
run(`gcc -shared lib.o -o lib.so`)
lib = "./lib.so"

function main()
    ret = CParticle[]
    f = x::CParticle -> (push!(ret, x); nothing)
    f_c = @cfunction $f Cvoid (CParticle,)
    GC.@preserve ret f f_c begin # AFAICT I don't need to GC.@preserver here
        @ccall lib.simulate(f_c::Ptr{Cvoid})::Cvoid
    end
    @show ret
end

main()

I read the manual and in particular the section about closures, but I am still not sure where the error lies.
It works, if I do one of the following:

  • make the CParticle struct smaller e.g. only charge + weight fields
  • execute the main code globally
  • don’t call push! in the callback
2 Likes

Am I missing something or you’re trying to push to an array that doesn’t exist in the C-side of your code? I assume your is an oversimplified excerpt of your real-world application, but this less contrived example works

C = """
typedef struct CParticle {
    double charge;
    double E;
    double weight;
} CParticle;

CParticle simulate2() {
    CParticle cp;
    cp.charge = 0;
    cp.E = 1;
    cp.weight = 2;
    return cp;
}

"""

struct CParticle
    charge::Cdouble
    E::Cdouble
    weight::Cdouble
end

run(pipeline(`gcc -x c -g -shared -fPIC - -o lib.so`; stdin = IOBuffer(C)))
lib = "./lib.so"

function main()
    ret = CParticle[]
    push!(ret, @ccall lib.simulate2()::CParticle)
    @show ret
end

main()
julia> main()
ret = CParticle[CParticle(0.0, 1.0, 2.0)]
1-element Vector{CParticle}:
 CParticle(0.0, 1.0, 2.0)
1 Like

It works if the object is pass-by-pointer instead of pass-by-value.

C = """
       typedef struct CParticle {
           double charge;
           double E;
           double weight;
       } CParticle;


       void simulate(void (*f)(CParticle* x)) {
           CParticle cp;
           cp.charge = 0;
           cp.E = 1;
           cp.weight = 2;
           (*f)(&cp);
           return;
       }

       """

function main()
           ret = CParticle[]
           f = x::Ptr{CParticle} -> (push!(ret, unsafe_load(x)); nothing)
           f_c = @cfunction $f Cvoid (Ptr{CParticle},)
           GC.@preserve f_c begin
               @ccall lib.simulate(f_c::Ptr{Cvoid})::Cvoid
           end
           @show ret
       end
1 Like

@giordano thanks!

Am I missing something or you’re trying to push to an array that doesn’t exist in the C-side of your code?

What do you mean by does not exist in the C-side? My naive expectation was that I could execute arbitrary Julia code in a function pointer that I pass to C? Ok arbitrary is an exaggeration, there are various caveats e.g. multi-threading, GC. But other then GC issues, which should not apply here, whats the problem with manipulating some heap-allocated julia object like a vector in this callback?

I assume your is an oversimplified excerpt of your real-world application, but this less contrived example works

In my real example, a simulation is running and it accepts callbacks, which are executed at various events, so one can extract information. I cannot handle my real case by your return value trick.

Thanks @Gnimuc this is a good workaround. Can you comment on, why it is better to pass by pointer, then by value here?

I’m not sure. As this is indeed an unexpected behavior and there is no related docs or tests, maybe it’s worth it to file an issue?

A “canonical” callback function should accept a void * userdata argument for extracting information, in this way, there is no need to use closures.

void simulate(void (*f)(CParticle x, void* userdata), void* userdata) {
           CParticle cp;
           cp.charge = 0;
           cp.E = 1;
           cp.weight = 2;
           (*f)(cp, userdata);
           return;
       }
function f(x::CParticle, userdata::Vector{CParticle})::Cvoid
    push!(userdata, x);
    return nothing
end

function main()
     ret = CParticle[]
     f_c = @cfunction f Cvoid (CParticle, Vector{CParticle})
     @ccall lib.simulate(f_c::Ptr{Cvoid}, ret::Any)::Cvoid
     @show ret
end
1 Like

https://github.com/JuliaLang/julia/issues/40164