How to avoid GC of Julia structs within C/C++/Rust data structures?

Here’s an example.

First the julia code to create a foreign DataType:

function maketype(marker, mod::Module=@__MODULE__)

    dtp = ccall(:jl_new_foreign_type,
               Any,
               (Symbol,Module,Any,Ptr{Cvoid}, Ptr{Cvoid}, Cint, Cint),
               :QueueRef, mod, Any, marker, C_NULL, 1, 0)
    mod.eval(
        quote
            const QueueRef = $dtp
            # a constructor:
            function QueueRef(h::Ptr)
                # make a new QueueRef object 
                ptls = Core.getptls()
                newobj = ccall(:jl_gc_alloc_typed, Any, 
                               (Ptr{Cvoid}, Csize_t, Any,),
                               ptls, sizeof(Ptr{Cvoid}), QueueRef)::QueueRef
                # store the pointer in it
                # this would be your queue handle
                unsafe_store!(Ptr{UInt}(pointer_from_objref(newobj)), UInt(h), 1)
                # return the object
                newobj
            end
            Base.getindex(qr::QueueRef) = unsafe_load(Ptr{Any}(pointer_from_objref(qr)))
            # here an enqueue, dequeue and offer may be defined for the QueueRef.
        end
    )
end

Then some C-code which I compile to a shared library in the file libmarker.so. This is where you loop through the C-queue and mark each entry. I only mark the content of the QueueRef object, in my case a Vector{Int}. Nothing can happen to julia objects during marking, all normal julia threads are waiting during the entire GC, so no need to think about concurrency.

#include <stdio.h>
#include "julia.h"
#include "julia_gcext.h"

int marker(jl_ptls_t ptls, jl_value_t *obj[]) {
  jl_value_t *p = obj[0];
  int marks = jl_gc_mark_queue_obj(ptls, p);
  if(marks > 0) printf("marked %p %d\n",p,marks);
  return marks;
}

Then a julia script using this stuff, which shows that I can lose all refs to the vector v. A pointer is hidden inside the QueueRef object, and the marker C-function takes care of marking it as still in use:

using Libdl
flib = dlopen("./libmarker.so")
cfun = dlsym(flib,:marker)
maketype(cfun)

v = [42, 42, 42]
finalizer(v) do o
    @ccall printf("finalize %p\n"::Cstring; pointer_from_objref(o)::Ptr{Nothing})::Cint
end

a = QueueRef(pointer_from_objref(v));
julia> a
QueueRef()

julia> v = nothing

julia> GC.gc(true)
marked 0x77e04e73f030 1
marked 0x77e04e73f030 1

julia> GC.gc(true)
marked 0x77e04e73f030 1

julia> a = nothing

julia> GC.gc(true)
finalize 0x77e04e73f030
1 Like

It’s O(n), yes. But you don’t want much gc in a performant parallel program anyway. As long as your wrapper doesn’t allocate a lot, that would be up to the user.

In such a marker you don’t have to worry about concurrency. All julia threads are waiting while gc runs, so nothing can happen to the C-structures while marking.

This is very helpful @sgaure — thank you!

Just for the reference, I made a more streamlined interface, where the foreign ref can hold an arbitrary struct with more than a pointer. The struct layout must be synchronized between C and julia, but I think there are packages which can do this (CBinding.jl ?) I had the idea to make a package ForeignRefs for these things, but I don’t have the time to maintain it. Unfortunately, one can’t write the marker function in julia and pass it on as a @cfunction, since calling a @cfunction accesses some GC structures with a lock, resulting in a deadlock.

Definitions
abstract type ForeignRef{T} end

function makeforeigntype(name, ::Type{T},
                  marker::Ptr{Nothing}, sweeper::Ptr{Nothing},
                  mod::Module) where {T}

    isconcretetype(T) || throw(ArgumentError("Must be a concrete type: $T"))
    sname = Symbol(name)
    dt = ccall(:jl_new_foreign_type,
               Any,
               (Symbol,Module,Any,Ptr{Cvoid}, Ptr{Cvoid}, Cint,Cint),
               sname, mod, ForeignRef{T}, marker, sweeper, 1, 0)
    mod.eval(:(const $sname = $dt))
end

# constructor for a foreign type
function (::Type{FT})(obj::T) where {T, FT<:ForeignRef{T}}
    isconcretetype(FT) || (() -> throw(MethodError(FT,Tuple{T})))()
    newobj = ccall(:jl_gc_alloc_typed, Any,
                   (Ptr{Cvoid}, Csize_t, Any,),
                   Core.getptls(), sizeof(T), FT)::FT
    unsafe_store!(Ptr{T}(pointer_from_objref(newobj)), obj)
    newobj
end

contenttype(fr::ForeignRef{T}) where T = T
Base.getindex(fr::ForeignRef{T}) where T = unsafe_load(Ptr{T}(pointer_from_objref(fr)))

And a marker, not marking anything, since the example has no reference to julia objects:

Marker, just printing the content
#include <stdio.h>
#include "julia.h"
#include "julia_gcext.h"

int marker(jl_ptls_t ptls, jl_value_t *obj[]) {

  int64_t a = (int64_t) obj[0];
  int64_t b = (int64_t) obj[1];
  uint64_t c = (uint64_t) obj[2];
  printf("marking a=%ld, b=%ld, c=0x%lx\n", a, b, c);
  return 0;
}

And the more general interface, where any struct can be held by the foreign type:

struct MyStruct
    a::Int
    b::Int
    c::Ptr{Nothing}
end

mystruct = MyStruct(17, 14, Ptr{Nothing}(0x42bad00bad42))

using Libdl

flib = dlopen("./libmarker.so")
marker = dlsym(flib,:marker)

makeforeigntype(:FRef, MyStruct, marker, C_NULL, Main)

fref = FRef(mystruct)
mystruct = nothing

julia> dump(fref[])   # mystruct is in there!
MyStruct
  a: Int64 17
  b: Int64 14
  c: Ptr{Nothing}(0x000042bad00bad42)
1 Like

Thanks @sgaure — looks like that could really come in handy. For now using the Julia port of the optimized C queue is way faster than using the moodycamel C++ queue, but I’m sure I’ll return to this once I lean on some other native data structures in the near future.

1 Like