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 tou8
- 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 {
, orD3DCOMPILE_FLAGS :: enum_flags u32 {
. - The size of
int
andfloat
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.
context
→ctx
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!