Idiomatic Julia for common inner constructor across multiple types


#1

Hi all. I have a large number of types that I need to implement in Julia that correspond to some C/CPP created memory. I’m able to use ccall to allocate the memory and the way this library is set up is that ccall then returns a pointer to the memory address. This is all well and good but I don’t want users dealing with the pointer to the address and have decided having types in Julia that correspond to these various memory allocations is a good way to make a nice interface.

abstract type CWrapper end
Base.convert(T::Type{<:CWrapper}, p::Ptr{Nothing}) = T(p)
Base.unsafe_convert(T::Type{Ptr{Nothing}}, t::CWrapper) = t.ptr

struct CppClass1 <: CWrapper
    ptr::Ptr{Nothing}
end

struct CStruct1 <: CWrapper
    ptr::Ptr{Nothing}
end

struct CppClass2 <: CWrapper
    ptr::Ptr{Nothing}
end

struct CStruct2 <: CWrapper
    ptr::Ptr{Nothing}
end

This works really well because now I can write functions that prevent incorrect pointers being passed to the C library by strict typing

function createCppClass1(x::Int)
    ptr = ccall((:createCppClass1, LIBRARY), Ptr{CVoid}, (CInt), x) 
    return CppClass1(ptr)
end

function someOperationOnCppClass1(o::CppClass1)
    ccall((:someOperationOnCppClass1, LIBRARY), Cint, (Ptr{CVoid}, ), o)
end

This makes for a nice safer interface because now for example, users will not be able to call someOperationOnCppClass1 by passing in an argument of CppClass2.

The C library in question occasionally may fail to allocate memory and may return a null pointer. If this happens, I want to raise an error. I initially wanted to use an inner constructor for this. However, I have the exact same code being used and was wondering if there was a nicer way to do this.

abstract type CWrapper end
Base.convert(T::Type{<:CWrapper}, p::Ptr{Nothing}) = T(p)
Base.unsafe_convert(T::Type{Ptr{Nothing}}, t::CWrapper) = t.ptr

struct CppClass1 <: CWrapper
    ptr::Ptr{Nothing}
    function CppClass1(ptr::Ptr{Nothing})
        ptr == C_NULL && error("Received null pointer from C interface. Something went wrong")
        new(ptr)
    end
end

struct CStruct1 <: CWrapper
    ptr::Ptr{Nothing}
    function CStruct1(ptr::Ptr{Nothing})
        ptr == C_NULL && error("Received null pointer from C interface. Something went wrong")
        new(ptr)
    end
end

struct CppClass2 <: CWrapper
    ptr::Ptr{Nothing}
    function CppClass2(ptr::Ptr{Nothing})
        ptr == C_NULL && error("Received null pointer from C interface. Something went wrong")
        new(ptr)
    end
end

struct CStruct2 <: CWrapper
    ptr::Ptr{Nothing}
    function CStruct2(ptr::Ptr{Nothing})
        ptr == C_NULL && error("Received null pointer from C interface. Something went wrong")
        new(ptr)
    end
end

Is there a way to write a common constructor for all structs that are a subtype of a specific abstract type? If I have 50 or more types that I want to define I don’t want to write the same inner constructor 50 or so times. It seems like an awful lot of code repetition. Macros would be one way to do it but I’m wondering if there’s a way to do it without macros? I may have some additional unique constraints to pose for a few of the inner constructors for some of the types, and I don’t think I can generalize those easily using macros.


#2

Macros would be one way to do it but I’m wondering if there’s a way to do it without macros?

I actually do recommend macros, because that approach has another advantage: it allows you to do away with <: CWrapper. The macro can generate a block that contains your struct definition, the inner constructor, and also your convert and unsafe_convert methods.

By not using (/wasting?) type inheritance on an implementation detail (is it backed by C++ or not?) you free it up for structure that’s useful for your users: Maybe you want to reproduce part of the class hierarchy on the C++ side in Julia, or maybe some of these objects benefit from e.g. <: AbstractVector.

I realize that’s not a direct answer to your question. My answer to your question “is there a way to define a common inner constructor” would be “I don’t know of any”. Hope that you don’t mind me posting this reply regardless!


#3

Do all the wrappers just contain a pointer? Then perhaps you could get away with a parametric CWrapper. Quick proof of concept:

struct CWrapper{T}
    ptr::Ptr

    function init(T, ptr)
        ptr == C_NULL && error("Null pointer")
        new{T}(ptr)
    end
    function CWrapper{T}(ptr) where T
        init(T, ptr)
    end
    function CWrapper{:CppClass2}(ptr)
        println("unique constraints")
        init(:CppClass2, ptr)
    end
end

createCppClass1(s::String) = CWrapper{:CppClass1}(pointer(s * " (1)"))
createCppClass2(s::String) = CWrapper{:CppClass2}(pointer(s * " (2)"))

processCppClass1(o::CWrapper{:CppClass1}) = println(unsafe_string(o.ptr))
processCppClass2(o::CWrapper{:CppClass2}) = println(unsafe_string(o.ptr))

Usage:

julia> c1 = createCppClass1("foo")
CWrapper{:CppClass1}(Ptr{UInt8} @0x000000011531d918)

julia> c2 = createCppClass2("bar")
unique constraints
CWrapper{:CppClass2}(Ptr{UInt8} @0x00000001153344f8)

julia> processCppClass1(c1)
foo (1)

julia> processCppClass2(c2)
bar (2)

julia> processCppClass1(c2)
ERROR: MethodError: no method matching processCppClass1(::CWrapper{:CppClass2})

julia> processCppClass2(c1)
ERROR: MethodError: no method matching processCppClass2(::CWrapper{:CppClass1})

#4

Why not just loop over those 50 types and repeat the same definition with the type names interpolated using @eval code generation.

for (class,struc) ∈ [(Symbol("CppClass$n"),Symbol("CStruct$n")) for n ∈ 1:2]
    @eval begin
        struct $class <: CWrapper
            ptr::Ptr{Nothing}
            function $class(ptr::Ptr{Nothing})
                ptr == C_NULL && error("Received null pointer from C interface. Something went wrong")
                new(ptr)
            end
        end
        struct $struc <: CWrapper
            ptr::Ptr{Nothing}
            function $struc(ptr::Ptr{Nothing})
                ptr == C_NULL && error("Received null pointer from C interface. Something went wrong")
                new(ptr)
            end
        end
    end
end

just another idea…


#5

Thanks for all the replies! The parametric wrapper is a really neat idea! I ended up going with the macro implementation.

Is there a way to add the docstring in the macro implementation as well? Everything I’ve tried (using @doc) as well doesn’t seem to work and I’m not able to find other examples of people using a docstring on a struct definition inside a macro.


#6

Is there a way to add the docstring in the macro implementation as well? Everything I’ve tried (using @doc ) as well doesn’t seem to work and I’m not able to find other examples of people using a docstring on a struct definition inside a macro.

Use the @__doc__ macro specifically for that:
link to docs.julialang.org.


#7

Thanks for the comment! I’m able to create the struct fine, but I’m not able to create documentation within the eval statement?

julia> structname = :SomeStruct
       eval(Base.@__doc__ :(struct $structname
       x::Int
       end))

help?> SomeStruct
search: SomeStruct

  No documentation found.

  Summary
  ≡≡≡≡≡≡≡≡≡

  struct SomeStruct <: Any

  Fields
  ≡≡≡≡≡≡≡≡

  x :: Int64

julia> structname = :SomeStruct
       eval(Base.@__doc__ :(""" hi """
       struct $structname
       x::Int
       end))
ERROR: syntax: missing comma or ) in argument list

Any suggestions on what I’m doing wrong? Is this even possible? The Julia docs recommends looking at @enum, but even they don’t create the docs within the macro I believe.

The @__doc__ macro just seems to tag the struct being created as something that can be documented. Is there a way to generate the documentation itself from within the eval. I have the eval function wrapped in a for loop for all the types that I’m defining and it would be nice to do it dynamically.


#8

@__doc__ is mainly for pulling an outside docstring into one part of the definition inside a macro. To add your own docstrings, you can use something like:

macro newstruct(name, docstring)
    quote
        struct $name
            a::Int
        end
        @doc $docstring $name
    end
end
julia> @newstruct A "hello, this is A, a new struct"
A

help?> A
search: A Any Aug Apr any all abs ARGS ans axes atan asin asec any! all! acsc acot acos abs2 Array April atanh atand asinh asind asech asecd ascii angle acsch acscd acoth acotd acosh acosd August alamo atexit

  hello, this is A, a new struct

Here’s another example: