Calling to Dynamic Libraries

Mapping a dynamic library (.dll / .so) is a fairly simple process: you specify the library file, and then provide signatures for the procedures you want to use.

For example:

lz4 :: #foreign_library "liblz4";

LZ4_compressBound :: (inputSize: s32) -> s32 #foreign lz4;
LZ4_compress_fast :: (source: *u8, dest: *u8, sourceSize: s32, maxDestSize: s32, acceleration: s32) -> s32 #foreign lz4;
LZ4_sizeofState :: () -> s32 #foreign lz4;
  • We can specify a path to the library inside the double-quotes, but You MUST copy the .dll next to the .exe, or it will not work!
  • Instead of linking to a local library file, you can link to a system library (built into the OS) with #foreign_system_library, e.g. d3d11 :: #foreign_system_library "d3d11";

Your procedure name does not need to exactly match the name in the library: you can rename it if you wish. If you do, then add the original name in quotes at the end:

compress_bound :: (inputSize: s32) -> s32 #foreign lz4 "LZ4_compressBound";
compress_fast :: (source: *u8, dest: *u8, sourceSize: s32, maxDestSize: s32, acceleration: s32) -> s32 #foreign lz4 "LZ4_compress_fast";
size_of_state :: () -> s32 #foreign lz4 "LZ4_sizeofState";

If you are converting a C .h file then some familiarity with C is obviously required, but it can more-or-less be translated mechanically: put in the ::, move the return type to the end and add the #foreign <lib> declaration, and flip the parameter types/names. Things to be aware of:

  • A pointer to char becomes a pointer to u8
  • Elide extraneous prefixes (i.e. const before parameters, macros, etc.)
  • References become pointers (i.e. & becomes *)
  • You should almost always specify a 32-bit size for an enum, i.e. IL_Result :: enum s32 {, or D3DCOMPILE_FLAGS :: enum_flags u32 {.
  • The size of int and float may be hard to discern; if you can’t work it out from the code, comments or documentation then go for 32-bit versions; if your data comes out mangled you can re-apprise.
  • Rename any parameter which happens to coincide with a Jai keyword. contextctx is common, for instance.

Once you have the code compiling and your program running, you need to check the data that’s being passed back and forth from the library: incorrect values will likely indicate incorrectly sized struct members, variables, constants, or parameters. Be especially observant of the types mentioned above.

Callbacks

We use two keywords to specify callback types: #type and #c_call. #type lets us specify the expected parameters of the callback (rather than just using a *void), and #c_call tells the compiler to use the C ABI.

  • We need to specify a void return type if that’s the case.
  • When we write an actual callback procedure to use with our definition, we need to push a new context inside it.

For example:

IL_Logger_Callback :: #type(level: IL_LoggingLevel, text: *u8, ctx: *void) -> void #c_call;

logger_callback :: (level: IL_LoggingLevel, text: *u8, ctx: *void) #c_call {
    new_context : Context;
    push_context new_context {
        log("%", to_string(text));
    }
}

Example Conversion

IL_C_API IL_Result IL_SetAdapter(IL_Context* context, IL_AdapterFunctions* adapterFunctions);

typedef void(*IL_Logger_Callback)(IL_LoggingLevel level, const char* text, void* context);

typedef struct IL_Logger
{
    IL_Logger_Callback callback;
    IL_LoggingLevel level;
    void* context;
} IL_Logger;

typedef enum IL_DeviceNotification
{
    IL_DeviceNotification_None = 0,
    IL_DeviceNotification_UpdatedStreamsAvailable = 1,
    IL_DeviceNotification_UpdatedConfig = 2
} IL_DeviceNotification;

Becomes:

IL :: #foreign_library "ILlibrary";

// Ditch the IL_C_API macro and rename `context` to `ctx`.
IL_SetAdapter :: (ctx: *IL_Context, adapterFunctions: *IL_AdapterFunctions) -> IL_Result #foreign IL;

// See above section on callbacks, `const char*` becomes `*u8`.
IL_Logger_Callback :: #type(level: IL_LoggingLevel, text: *u8, ctx: *void) -> void #c_call;

IL_Logger :: struct {
    callback : IL_Logger_Callback;
    level    : IL_LoggingLevel;
    ctx      : *void;
}

// Add `s32` size info to enum
IL_DeviceNotification :: enum s32 {
    IL_DeviceNotification_None                    :: 0;
    IL_DeviceNotification_UpdatedStreamsAvailable :: 1;
    IL_DeviceNotification_UpdatedConfig           :: 2;
}

Gotchas

When converting C Dynamic Library header files into Jai functions, an int type could be either s32 or s64, depending on how that library was compiled. The compiler currently does not check whether the function signature matches the dynamic library definition, so be careful when calling out to dynamic libraries!

1 Like