Skip to content
FutureBlitz

    Install

    C interop, 4 of 7

    Structs with @CLayout

    Lay a Struct out as C does, pass it to C by value and by pointer, and measure it.

    What you’ll build

    Two C functions that give back a struct, div and ldiv, whose results are Structs that match C’s, and a Header Struct with a fixed array in it, which you measure and hand to C to clear.

    A Struct C can see

    @CLayout lays a Struct out exactly as C would: each field at its own alignment after the one before, and the whole padded to a multiple of its strictest alignment, the largest of its fields’ alignments. C’s div gives back a div_t, which is two ints:

    Import std.core
    
    @CLayout Struct DivResult
        Field Quotient: Int32
        Field Remainder: Int32
    End Struct
    
    Extern "C"
        Function div(numerator: Int32, denominator: Int32): DivResult
    End Extern
    
    Local r = div(17, 5)
    Print("17 / 5 is {r.Quotient} remainder {r.Remainder}")
    Output
    17 / 5 is 3 remainder 2

    The fields’ names don’t need to match C’s; their order and types do. The Struct keeps everything a Struct has, its constructor, ==, and Methods among them.

    A Struct that an Extern Function takes or gives, by value, by ByRef, or through a Ptr, has to be @CLayout. Without it, the declaration is an error:

    Import std.core
    
    Struct DivResult
        Field Quotient: Int32
        Field Remainder: Int32
    End Struct
    
    Extern "C"
        Function div(numerator: Int32, denominator: Int32): DivResult
    End Extern
    
    Print(div(17, 5).Quotient)
    $ fb run main.fb
       Compiling main.fb
    main.fb:9:57: error: C code sees DivResult here, so it has to be @CLayout, which lays it out as C does
      |
    9 |     Function div(numerator: Int32, denominator: Int32): DivResult
      |                                                         ^^^^^^^^^
    error: couldn't build main.fb because of 1 error

    Sizes that differ by OS

    C’s long is 4 bytes on Windows and 8 on Linux and macOS. CLong is whichever the target has, so a Struct with one matches C’s on each.

    Import std.core
    
    @CLayout Struct LongDivResult
        Field Quotient: CLong
        Field Remainder: CLong
    End Struct
    
    Extern "C"
        Function ldiv(numerator: CLong, denominator: CLong): LongDivResult
    End Extern
    
    Local big = ldiv(1000000, 7)
    Print(big.Quotient, big.Remainder)
    Output
    142857 1

    What a @CLayout Struct holds

    Its fields are what C has: numbers, Bool, Enums, CString, Ptrs, Callbacks, other @CLayout Structs, and fixed arrays of these. A String, an array, or a Class’s object isn’t C data, and is an error that lists what’s allowed. SizeOf<T>() gives the size C would.

    Import std.core
    Import std.ffi
    
    @CLayout Struct Header
        Field Kind: UInt8
        Field Length: UInt32
        Field Tag: UInt8[3]
    End Struct
    
    Extern "C"
        Function memset(target: Ptr<Void>, value: Int32, count: UInt64): Ptr<Void>
    End Extern
    
    Print(SizeOf<Header>())
    
    Local h = Header(1, 99, [65, 66, 67])
    Unsafe
        memset(AddressOf(h), 0, SizeOf<Header>() As UInt64)
    End Unsafe
    Print(h.Kind, h.Length, h.Tag)
    Output
    12
    0 0 [0, 0, 0]

    Header is 12 bytes: Kind at 0, three bytes of padding so that Length starts at 4, Tag at 8, and one byte more to make 12, a multiple of 4, Length’s alignment and the strictest of the three. A fixed array aligns as its elements do, so Tag’s is 1. memset clears all of it through the pointer AddressOf gives.

    By value or by pointer

    A Struct passed by value goes in registers or memory as the target’s C ABI says, as clang would pass it, on every OS and processor FutureBlitz builds for. A C function that takes a pointer to a struct takes a Ptr to the Struct, or the Struct as a ByRef parameter, which passes the variable’s address with no Unsafe code. Declared that way, the same memset clears a Header from safe code:

    Import std.core
    Import std.ffi
    
    @CLayout Struct Header
        Field Kind: UInt8
        Field Length: UInt32
        Field Tag: UInt8[3]
    End Struct
    
    Extern "C"
        Function ClearHeader(ByRef target: Header, value: Int32, count: UInt64): Ptr<Void> = "memset"
    End Extern
    
    Local h = Header(1, 99, [65, 66, 67])
    ClearHeader(ByRef h, 0, SizeOf<Header>() As UInt64)
    Print(h.Kind, h.Length, h.Tag)
    Output
    0 0 [0, 0, 0]

    What you learned

    • @CLayout lays a Struct out as C does, and every Struct C code sees needs it.
    • Fields match C’s by order and type, not by name.
    • CLong keeps a Struct right where C’s long differs by OS.
    • SizeOf<T>() measures a Struct as C lays it out, padding included.

    Report an issue with this page or with FutureBlitz on GitHub.