Object lifetime problem with CxxWrap and make_const_array

I’m experimenting with using CxxWrap to connect an existing C++ library to a Julia project. There’s a particular case I’m looking at right now - there’s a C++ object that has a std::vector as a data member which I want to pass to Julia. Here’s the declaration:

namespace brv {
    struct big_random_vector {
        std::vector<double> m_vals;
        big_random_vector(const double min, const double max);
    };
}

Here’s my C++ wrapper code:

#include <vector>
#include <brv.hpp>
#include <jlcxx/jlcxx.hpp>
#include <jlcxx/const_array.hpp>

JLCXX_MODULE define_julia_module(jlcxx::Module& mod) {
    mod.add_type<brv::big_random_vector>("big_random_vector")
        .constructor<const double, const double>()
        .method("get_vals", [](brv::big_random_vector& brv) {
                                return jlcxx::make_const_array(brv.m_vals.data(), brv.m_vals.size()); });
}

And here’s some Julia code that calls the wrapper:

module BrvWrapper
using CxxWrap

@wrapmodule("libbrv_wrap.so")
function __init__()
    @initcxx
end

brv = nothing
function do_everything()
    global brv
    brv = big_random_vector(-10.0, 10.0)
    vals = get_vals(brv)
    sqvals = map(x -> x^2, vals)
    []
end

So this works fine. However, if I take out the ‘global brv’ declaration - making brv local - and call do_everything(), julia dumps core:

in expression starting at .../sandbox/juliacpp/cxxwrap_user.jl:34
iterate at ./abstractarray.jl:0 [inlined]
iterate at ./generator.jl:44 [inlined]
collect_to! at ./array.jl:667 [inlined]
collect_to_with_first! at ./array.jl:646 [inlined]
_collect at ./array.jl:640
map at ./array.jl:564 [inlined]
do_everything at .../sandbox/juliacpp/cxxwrap_user.jl:23
##core#424 at .../.julia/packages/BenchmarkTools/7aqwe/src/execution.jl:297
##sample#425 at .../.julia/packages/BenchmarkTools/7aqwe/src/execution.jl:305
sample at .../.julia/packages/BenchmarkTools/7aqwe/src/execution.jl:320 [inlined]
#_lineartrial#41 at .../.julia/packages/BenchmarkTools/7aqwe/src/execution.jl:71
_lineartrial at .../.julia/packages/BenchmarkTools/7aqwe/src/execution.jl:63
_jl_invoke at /buildworker/worker/package_linux64/build/src/gf.c:2141 [inlined]
jl_apply_generic at /buildworker/worker/package_linux64/build/src/gf.c:2305
jl_apply at /buildworker/worker/package_linux64/build/src/julia.h:1631 [inlined]
jl_f__apply at /buildworker/worker/package_linux64/build/src/builtins.c:627
jl_f__apply_latest at /buildworker/worker/package_linux64/build/src/builtins.c:665
#invokelatest#1 at ./essentials.jl:709 [inlined]
invokelatest at ./essentials.jl:708 [inlined]
#lineartrial#38 at .../.julia/packages/BenchmarkTools/7aqwe/src/execution.jl:33 [inlined]
lineartrial at .../.julia/packages/BenchmarkTools/7aqwe/src/execution.jl:33 [inlined]
#tune!#44 at .../.julia/packages/BenchmarkTools/7aqwe/src/execution.jl:135
tune! at .../.julia/packages/BenchmarkTools/7aqwe/src/execution.jl:134 [inlined]
tune! at .../.julia/packages/BenchmarkTools/7aqwe/src/execution.jl:134
_jl_invoke at /buildworker/worker/package_linux64/build/src/gf.c:2141 [inlined]
jl_apply_generic at /buildworker/worker/package_linux64/build/src/gf.c:2305
jl_apply at /buildworker/worker/package_linux64/build/src/julia.h:1631 [inlined]
do_call at /buildworker/worker/package_linux64/build/src/interpreter.c:328
eval_value at /buildworker/worker/package_linux64/build/src/interpreter.c:417
eval_stmt_value at /buildworker/worker/package_linux64/build/src/interpreter.c:368 [inlined]
eval_body at /buildworker/worker/package_linux64/build/src/interpreter.c:778
jl_interpret_toplevel_thunk_callback at /buildworker/worker/package_linux64/build/src/interpreter.c:888
unknown function (ip: 0xfffffffffffffffe)
unknown function (ip: 0x7ff36c954d8f)
unknown function (ip: 0x8)
jl_interpret_toplevel_thunk at /buildworker/worker/package_linux64/build/src/interpreter.c:897
jl_toplevel_eval_flex at /buildworker/worker/package_linux64/build/src/toplevel.c:814
jl_eval_module_expr at /buildworker/worker/package_linux64/build/src/toplevel.c:181
jl_toplevel_eval_flex at /buildworker/worker/package_linux64/build/src/toplevel.c:640
jl_parse_eval_all at /buildworker/worker/package_linux64/build/src/ast.c:873
jl_load at /buildworker/worker/package_linux64/build/src/toplevel.c:878
include at ./boot.jl:328 [inlined]
include_relative at ./loading.jl:1105
include at ./Base.jl:31
_jl_invoke at /buildworker/worker/package_linux64/build/src/gf.c:2135 [inlined]
jl_apply_generic at /buildworker/worker/package_linux64/build/src/gf.c:2305
include at ./client.jl:424
_jl_invoke at /buildworker/worker/package_linux64/build/src/gf.c:2135 [inlined]
jl_apply_generic at /buildworker/worker/package_linux64/build/src/gf.c:2305
jl_apply at /buildworker/worker/package_linux64/build/src/julia.h:1631 [inlined]
do_call at /buildworker/worker/package_linux64/build/src/interpreter.c:328
eval_value at /buildworker/worker/package_linux64/build/src/interpreter.c:417
eval_stmt_value at /buildworker/worker/package_linux64/build/src/interpreter.c:368 [inlined]
eval_body at /buildworker/worker/package_linux64/build/src/interpreter.c:778
jl_interpret_toplevel_thunk_callback at /buildworker/worker/package_linux64/build/src/interpreter.c:888
unknown function (ip: 0xfffffffffffffffe)
unknown function (ip: 0x7ff36bc5e50f)
unknown function (ip: 0xffffffffffffffff)
jl_interpret_toplevel_thunk at /buildworker/worker/package_linux64/build/src/interpreter.c:897
jl_toplevel_eval_flex at /buildworker/worker/package_linux64/build/src/toplevel.c:814
jl_toplevel_eval_flex at /buildworker/worker/package_linux64/build/src/toplevel.c:764
jl_toplevel_eval_in at /buildworker/worker/package_linux64/build/src/toplevel.c:843
eval at ./boot.jl:330
_jl_invoke at /buildworker/worker/package_linux64/build/src/gf.c:2135 [inlined]
jl_apply_generic at /buildworker/worker/package_linux64/build/src/gf.c:2305
eval_user_input at /buildworker/worker/package_linux64/build/usr/share/julia/stdlib/v1.3/REPL/src/REPL.jl:86
macro expansion at /buildworker/worker/package_linux64/build/usr/share/julia/stdlib/v1.3/REPL/src/REPL.jl:118 [inlined]
#26 at ./task.jl:333
_jl_invoke at /buildworker/worker/package_linux64/build/src/gf.c:2135 [inlined]
jl_apply_generic at /buildworker/worker/package_linux64/build/src/gf.c:2305
jl_apply at /buildworker/worker/package_linux64/build/src/julia.h:1631 [inlined]
start_task at /buildworker/worker/package_linux64/build/src/task.c:659
unknown function (ip: 0xffffffffffffffff)
Allocations: 5599103 (Pool: 5598119; Big: 984); GC: 15
[2]    5329 segmentation fault (core dumped)  julia

What am I doing wrong?

I think I have a similar issue regarding object lifetime. In my case it’s on FastJet.jl.
For me, it looks like the object is going out of scope too early, and my solution is similar to the OP: put the julia object into the external scope.
Pinging @barche for help here.

Sorry, just saw this now. To make sure this is related to object lifetime, you could try running it without the garbage collector (GC.enable(false)).

Reading this thread I think you might need to use GC.@preserve in the line that defines sqvals, it seems the compiler might think here that brv is no longer needed.

Thanks for the response - I’ll try your suggestions. We ended up going down a different path for the project that prompted my original post, but I’m still curious about the behavior.

hmm. GC.@preserve does not fix it for me. It’s definitely related to object lifetime, though. A simple print() of the object later gets rid of the crash.
To be clear, in my case it’s a bit subtle. In FastJet a ClusteringSequence is handed a bunch of vectors that it clusters into “jets”. The method that returns the jets only returns references, so if the ClusteringSequence goes out of scope in the meantime, that list of references is pretty useless. It would be nice if there were a way to tell the GC that those to objects are linked, and not to kill the ClusteringSequence while I’m still looking at the jets…

Do you have a link to a concrete example of this use case? Another possible workaround is to use the gcprotect function from CxxWrap, then the object stays protected until explicitly unprotected.

The tests of the FastJet package expose that problem. If you look at https://github.com/jstrube/FastJet.jl/blob/main/test/runtests.jl#L39 and line 54, I’m printing a property of that object to keep it around.
Removing that print (sometimes) leads to the problem I’m seeing. GC.enable(false) “fixes” it, but that’s obviously no solution for long-running processes. GC.@preserve did not work for me.
The gcprotect function sounds interesting, thanks for the tip. I’ll look into it, but figuring out when to unprotect might be challenging, not sure.

I think that in the second test you need to protect some of the other objects as well, see my PR.

1 Like

Many thanks! Do you have an idea for how this could be moved up from the user-facing part of the code? Maybe one would have to figure out a better way to signal relationships between objects to be able to track this behind the scenes.
Maybe it’s not big enough of an issue to spend time on. Other APIs I’m using don’t have this issue.
The problem is that the PseudoJet objects produced by the ClusterSequence have a link to it.
This is not a criticism, but I find this pretty ugly. However, if this is the only package that faces this, I can live with it.

Anyway, thanks for spending the time to help me understand this better and making the PR. Much appreciated.

Yes, it is worth opening a CxxWrap issue on this, before this thread I assumed it was enough to keep a variable around to protect the object from GC, but at least for local variables that clearly isn’t true. I’m thinking of introducing an additional reference type that can keep a “Julia reference” to the parent object. We have to think about how to opt in to that however, because it creates extra overhead.

https://github.com/JuliaInterop/CxxWrap.jl/issues/256

1 Like