Ccall with a C struct containing a pointer

I’m writing a Julia interface for a C library. I have a C struct containing C-style array. I’ve wrapped said struct in Julia like so:

mutable struct Example
   data::Ptr{Int64} # in the C, this is an array.
end
x = Int64[1,2,3]
s = Example(pointer(x))
@ccall lib.someFunc(s::Example)::Cvoid #this works as desired

How do I create a Julia class to encapsulate this functionality? Writing functions that take the type Example as an argument seems ripe for memory issues: what if the array that data points to goes out of scope?

In C++, I’d have data point to an newed array allocated on the heap, which I then would delete at some point. From the docs on wrapping C and Fortran code in Julia, if I were calling a C function that took a C-style array, I would construct a myArray of type Vector{T} then pass myArray::Ref{T} into the @ccall. But the interface of the C function requires the struct: I cannot pass a bare array into the C function.

One option is to manually root the array during the @ccall, to make sure that it stays allocated until the call is complete, e.g.

GC.@preserve x @ccall lib.someFunc(s::Example)::Cvoid

Another option is to pad your struct on the Julia side with additional fields that store references to the Julia arrays, e.g.

mutable struct Example
    data::Ptr{Int64} # in the C, this is an array.
    _data::Vector{Int64} # keep a ref to the Julia array
    Example(d::Vector{Int64}) = new(pointer(d), d)
end

I think there’s a pattern involving cconvert and unsafe_convert here, with two structs. One for using on the Julia side, one for the actual FFI interop, which would only exist for the duration of the FFI call. I’m not 100% sure on the exact definitions necessary, so the following may not necessarily be correct:

// The C example just returns the first element of the array

typedef struct {
  int* data;
} CExample;

int printarr(CExample* ex) {
  return ex->data[0];
}
mutable struct JLExample
    data::Vector{Int64}
end

mutable struct CExample
    data::Ptr{Int64}
end

Base.cconvert(::Type{Ptr{CExample}}, jle::JLExample) = CExample(pointer(jle.data))

function Base.unsafe_convert(::Type{Ptr{CExample}}, ce::CExample)
    return convert(Ptr{CExample}, pointer_from_objref(ce))
end

The reason I’m not 100% sure on this is because of that pointer(jle.data) in cconvert. This is likely illegal - cconvert isn’t supposed to take pointers, that’s the job of unsafe_convert here. The manual seems self contradictory here, since it says this:

Base.cconvert normally just calls convert, but can be defined to return an arbitrary new object more appropriate for passing to C. This should be used to perform all allocations of memory that will be accessed by the C code. For example, this is used to convert an Array of objects (e.g. strings) to an array of pointers.

but also this:

This can be used to allocate memory that will be accessed by the ccall. If multiple objects need to be allocated, a tuple of the objects can be used as return value.

Neither convert nor cconvert should take a Julia object and turn it into a Ptr.

which seems to imply that the conversion of an Array of objects to an array of pointers would be illegal…

Either way, it would be used like so:

julia> function bar()
           ex = JLExample([15,2,3])
           GC.@preserve ex begin
               @ccall "./example.so".printarr(ex::Ref{CExample})::Cint
           end
       end
bar (generic function with 1 method)

julia> bar()
15

which works, but certainly feels a little ugly due to the need for that GC.@preserve. The GC.@preserve should make the definitions above safe, since ex (and by extension, the wrapped array) is not allowed to be freed until the end of that begin block. This shouldn’t be necessary though, and I’m pretty sure I’m missing the correct way of doing this :thinking: Would be really great to get such an example involving a more complicated struct into the docs…

Here’s one other variation that might be more appropriate, and could be safer by default, if I’m using the interface correctly:

mutable struct JLExample
    data::Vector{Int64}
end

struct CExample # not mutable!
    data::Ptr{Int64}
end

Base.cconvert(::Type{Ref{CExample}}, jle::JLExample) = (jle, Ref{CExample}())

function Base.unsafe_convert(::Type{Ref{CExample}}, ce::Tuple{JLExample, Base.RefValue{CExample}})
    jle, ref = ce
    ref[] = CExample(pointer(jle.data))
    return Base.unsafe_convert(Ptr{CExample}, ref)
end

Basically, this ensures the C-FFI-struct has a safe place to go, by making sure both the source object jle and the inline data is preserved. This should be the case, since the manual says that the result of cconvert will “behave as if passed to GC.@preserve”. Then, in unsafe_convert the actual construction of the immutable data being passed to C is performed. Since that object is stored in the already allocated Ref (and is itself isbits, so will be stored inline in the Ref), all that’s left to do is convert that Ref to a Ptr, which will be passed to the ccall. (I’m not exactly sure why this last unsafe_convert is needed, but without it Julia complained about expecting a Ptr…)

This also seems to be zero-overhead on the calling side:

julia> ex = JLExample([15,2,3])
JLExample([15, 2, 3])

julia> function bar(ex)
           @ccall "./example.so".printarr(ex::Ref{CExample})::Cint
       end
bar (generic function with 1 method)

julia> bar(ex)
15

# no allocations!!
julia> @time bar(ex)
  0.000002 seconds
15

I should also mention that both of my posts assume that the original memory is allocated & managed by Julia. If the data is instead allocated & managed by C, the dance is going to be a bit different, I think.

Yet a third variation that seems to work:

mutable struct JLExample
    data::Vector{Int64}
end

mutable struct CExample
    data::Ptr{Int64}
    CExample() = new()
end

Base.cconvert(::Type{Ref{CExample}}, jle::JLExample) = (jle, CExample())

function Base.unsafe_convert(::Type{Ref{CExample}}, ce::Tuple{JLExample, CExample})
    jle, ref = ce
    ref.data = pointer(jle.data)
    return convert(Ptr{CExample}, Base.pointer_from_objref(ref))
end

This is basically the same as the one above, except that CExample itself is mutable, instead of storing an immutable in a Ref. This way, we can take the pointer to our data in unsafe_convert, and just write that pointer into the existing struct, which is then passed to C. I think this one, as well as the previous one are equally good, but there’s still a little lingering uneasiness about the whole thing.

In GTPSA.jl, we solved this problem by allocating the array directly in C. E.g. for a member array coef, we allocate in Julia using:

coef = @ccall jl_malloc(sz::Csize_t)::Ptr{T}

We then attach the finalizer to it:

f(t) = @ccall jl_free(t.coef::Ptr{Cvoid})::Cvoid
finalizer(f, t)

This solution has the benefits of keeping the C-style array owned by the C, safe from Julia’s garbage collector. I then pass to C functions like:

s = TPS() # mutable type like your example
@ccall lib.someFunc(s::Ref{TPS})::Cvoid

Of course, you must make sure that flexible array members are not used in the C code for this to work.

Thanks for bringing cconvert and unsafe_convert to my attention: using those seems like the cleanest way. Storing both the pointer and the pointed-to data could also work…but it feels kind of clunky. However, when I try to imitate the above examples, I’m getting an Illegal instruction error.

One thing I should’ve mentioned: the C functions I’m interested in typically take the C struct itself, not a pointer to the C struct. In that case, do things look different than in the above examples?

More details about my goals: I’m trying to build a Julia interface for the Apple Accelerate routines related to sparse matrix factorization. (Related links: my work-in-progress/proof-of-concept GitHub repo and this forum post.) I want to avoid re-inventing the wheel: ideally, cconvert should accept a SparseMatrixCSC (the Julia struct) and create a matching SparseMatrix struct (the C struct), for passing into the various @ccalls. The structs mostly line up, but there’s small differences: 0- vs 1-indexing, Cints vs Clongs for row indices.

For a more minimal, less involved example: take a look at DenseMatrix. This only has a single pointer, and no re-indexing is required. How would I write cconvert so as to take a Julia A::Matrix{T} and create a B::DenseMatrix{T} such that B.data = pointer(A.ref), so I could have SparseMultiply take in normal Julia matrices for arg2 and arg3?

EDIT: okay I have a bigger problem. I’m nesting structs (SparseMatrixStructure inside SparseMatrix), where the inner struct is not an isbits struct type. Thus the Illegal instruction error: fields weren’t lining up. But I’d still be interested in knowing the answer to the above question.

Not really - the difference is just that you wouldn’t pass a Ptr/Ref in the ccall but just the struct directly, without the need for pointer_from_objref and the like at all.

If the size/type of the individual items in the struct is different, you’ll want/need to make sure to convert the data to that type. So from julian Int64 to a Cint, by storing the data in a new bit of memory.

That should be something like

function Base.cconvert(::Type{DenseMatrix{T}}, m::Matrix{T}) where T
    rowCount, colCount = size(m)
    stride = 1 # not 100% sure on this - check that row/column majorness aligns! Julia is column major!
    dm = DenseMatrix{T}(rowCount, colCount, strig, #= the attribute type object =#, C_NULL)
    return (m, dm)
end

function Base.unsafe_convert(::Type{DenseMatrix{T}}, tup::Tuple{Matrix{T}, DenseMatrix{T}}) where T
    m, dm = tup
    dm.data = pointer(m) # m is GC-preserved in this function, so this is safe
    dm
end

Yes, the types you end up passing to C must match exactly in terms of layout. You can check Base.allocated_inline, which should help with that. The important thing is that the outermost struct you pass is mutable (either because the struct itself is mutable, or it’s a Ref, which is just a mutable struct with a single field), so that any modification C makes into that memory is visible again on the julia side. To Julia this would be “as if” the entire content was replaced, even though C just performed a partial write.

Great! Thank you: answer accepted. I’ve expanded that snippet into a self-contained example, for posterity:

mutable struct DenseMatrix # layout matches the C.
    rowCount::Cint
    columnCount::Cint
    data::Ptr{Cdouble}
end
function Base.cconvert(::Type{DenseMatrix}, m::Matrix{Float64})
    return (m, DenseMatrix(size(m)[1], size(m)[2], C_NULL))
end
function Base.unsafe_convert(::Type{DenseMatrix}, tup::Tuple{Matrix{Float64}, DenseMatrix})
    m, dm = tup
    dm.data = pointer(m) # m is GC-preserved in this function, so this is safe
    return dm
end
function CWrapper(arg1::Matrix{Float64}) # Julia base type here...
    @ccall lib.fname(arg1::DenseMatrix) # ...and C struct type here.
end

It’d be good to get a more elaborate example like this in the docs.

1 Like