Overview Link to heading
Because SDL3 is not included with the Jai releases yet, I decided to setup my own module that wraps it.
What’s a module?
Jai modules are just folders residing in a known “import path”. Conventionally they are Jai sources and compiled into a program or library using the #import
statement.
In some cases they also contain C or C++ sources, or precompiled foreign libraries. In these cases, the source of the module instructs the compiler how to link with the foreign library and provides the Jai bindings.
The bindings themselves are normally generated by an associated metaprogram that you can run when needed. But there are some libraries where the bindings are hand-crafted.
Jai ships with a lot of examples in addition to a growing collection of modules. Many wrap C (or C++) libraries, so thankfully there is a lot of prior work to start with when you want to wrap a library on your own.
Establishing the module Link to heading
Distributed with Jai is a module dedicated to generating bindings for C and C++ APIs called Bindings_Generator
. So our task is mainly to create a short program that uses this module to generate our bindings.
I decided to just include the pre-built SDL3 libraries for Windows, Mac, and Linux rather than also learn about Jai’s modules for building C and C++ code.
I’ve replaced or approximated the SDL3 build enough times to realize “why bother?” in this case because it’s more interesting to be working with Jai than it is to be finding yet another interesting way to build SDL3.
It may not be that bad with the tools provided by the Jai releases but I’m saving that for a future investigation!
I’ve started by creating a modules/SDL3
folder in the root of my repo. In this folder I’ve taken the headers from the SDL3 release selected and place them into a folder called headers
and I created two files generate.jai
and module.jai
.
#import
statement and if you need to import a module from a non-default location you can use jai -import_path <DIR>
or add the location to the build settings in your metaprogram.I created the module.jai
file following convention (but it might actually be required) and it is as simple as it gets:
#load "bindings.jai";
#if OS == .WINDOWS {
// #scope_module
// FILE :: void;
} else #if OS_IS_UNIX {
#import "POSIX";
#library,system,link_always "libm";
} else {
#assert false;
}
For some of the modules in the Jai provided examples, they have bindings for different platforms generated independently, and in their module they only load the appropriate ones. Because SDL itself is designed to abstract over platform differences, I didn’t intend to do that.
The file we import at the top will be generated by our generate.jai
program.
Generate the bindings Link to heading
I started by copying the binding generator program from the stb_image examples, and then deleting everything as I learned how it worked. Rather than detail this process we’ll just go through the result I ended up with.
Setting up the generator Link to heading
The contents of generate.jai
are pretty straight forward and fairly obvious in hindsight. But I admit that my initial exploration was mostly spent trying to suss out which bits were general and which were specific in the various examples provided.
#import "Basic";
#import "Bindings_Generator";
#import "BuildCpp";
#import "Compiler";
#import "File";
#import "Process";
SOURCE_PATH :: "headers/SDL3";
LIB_BASE_NAME :: "SDL3";
#run,stallable {
set_build_options_dc(.{do_output=false});
if !generate_bindings() {
compiler_set_workspace_status(.FAILED);
}
}
generate_bindings :: () -> bool {
}
It’s nice and simple. Since I’m not also attempting to build SDL, I don’t really need to support custom build flags or anything like that.
The name and location of the library Link to heading
I’ve placed the pre-built libraries into platform specific folders in the repo. We select these based on the current OS
:
lib_filename: string;
lib_directory: string;
if OS == {
case .WINDOWS;
lib_directory = "windows";
lib_filename = "SDL3.lib";
case .LINUX;
lib_directory = "linux";
lib_filename = "libSDL3.so";
case .MACOS;
lib_directory = "macos";
lib_filename = "libSDL3.0.dylib";
case;
assert(false);
}
Define the generator options Link to heading
Having decided on the library location we can fill out our options struct:
options: Generate_Bindings_Options;
{
using options;
array_add(*include_paths, "headers");
array_add(*library_search_paths, lib_directory);
array_add(*libraries, .{filename=lib_filename, identifier="SDL3"});
array_add(*source_files, tprint("%/SDL.h", SOURCE_PATH));
array_add(*source_files, tprint("%/SDL_main.h", SOURCE_PATH));
array_add(*extra_clang_arguments, "-DSDL_MAIN_USE_CALLBACKS");
header = HEADER;
footer = tprint(FOOTER_TEMPLATE, LIB_BASE_NAME);
generate_library_declarations = false;
generate_compile_time_struct_checks = false;
}
The clang related options should be pretty familiar looking if you’ve worked with C or C++. Header and library search paths, required preprocessor definitions, input sources, and so on.
The lower portion of this code snippet are all specific to the generator… and I’ll fully admit here that I haven’t methodically tested all of the available options. :D My goal was to get up and running as quickly as possible so I decided I’d worry about the aesthetics or practical matters of the generated bindings later.
Notice the header
and footer
options. These take a string that gets inserted above and below the output from the generator.
Set our output path and run the generator Link to heading
The last step in our generator is to decide where to write the output, and have the Bindings_Generator
module do it’s work:
output_filename := "bindings.jai";
return generate_bindings(options, output_filename);
Header and Footer Link to heading
After the generate_bindings()
definition, I have a #scope_file
section containing the HEADER
and FOOTER
strings.
The FOOTER
is a template (but honestly… why did I do that?) that expects to be formatted with the library name:
FOOTER_TEMPLATE :: #string END
#if OS == .WINDOWS {
%1 :: #library "windows/%1";
} else #if OS == .LINUX {
%1 :: #library "linux/lib%1";
} else #if OS == .MACOS {
%1 :: #library "macos/lib%1.0";
} else {
#assert false;
}
END
This ultimately instructs the module how to link with the library. And this is why we set generate_library_declarations = false;
in our generator options.
The HEADER
is a lot of “stuff” that I just copied from another SDL3 bindings project on GitHub. The “stuff” are all Jai equivalent functions of some key macros in SDL3.
I decided not to include the contents of HEADER
here, but you can see it all in the repo anyway.
Run the generator Link to heading
Now that we have our little program setup, we run it:
chipc@eve SDL3 % jai generate.jai
headers/sdl3/sdl_stdinc.h:4097:33: Stripping function with valist: SDL_vsscanf
headers/sdl3/sdl_stdinc.h:4184:33: Stripping function with valist: SDL_vsnprintf
headers/sdl3/sdl_stdinc.h:4205:33: Stripping function with valist: SDL_vswprintf
headers/sdl3/sdl_stdinc.h:4253:33: Stripping function with valist: SDL_vasprintf
headers/sdl3/sdl_stdinc.h:6046:23: Stripping function that’s missing from foreign libs: SDL_size_mul_check_overflow
headers/sdl3/sdl_stdinc.h:6060:23: Stripping function that’s missing from foreign libs: SDL_size_mul_check_overflow_builtin
headers/sdl3/sdl_stdinc.h:6085:23: Stripping function that’s missing from foreign libs: SDL_size_add_check_overflow
headers/sdl3/sdl_stdinc.h:6098:23: Stripping function that’s missing from foreign libs: SDL_size_add_check_overflow_builtin
headers/sdl3/sdl_endian.h:408:24: Stripping function that’s missing from foreign libs: SDL_SwapFloat
headers/sdl3/sdl_error.h:108:34: Stripping function with valist: SDL_SetErrorV
headers/sdl3/sdl_iostream.h:656:36: Stripping function with valist: SDL_IOvprintf
headers/sdl3/sdl_bits.h:66:22: Stripping function that’s missing from foreign libs: SDL_MostSignificantBitIndex32
headers/sdl3/sdl_bits.h:133:23: Stripping function that’s missing from foreign libs: SDL_HasExactlyOneBitSet32
headers/sdl3/sdl_rect.h:126:23: Stripping function that’s missing from foreign libs: SDL_RectToFRect
headers/sdl3/sdl_rect.h:155:23: Stripping function that’s missing from foreign libs: SDL_PointInRect
headers/sdl3/sdl_rect.h:179:23: Stripping function that’s missing from foreign libs: SDL_RectEmpty
headers/sdl3/sdl_rect.h:203:23: Stripping function that’s missing from foreign libs: SDL_RectsEqual
headers/sdl3/sdl_rect.h:320:23: Stripping function that’s missing from foreign libs: SDL_PointInRectFloat
headers/sdl3/sdl_rect.h:344:23: Stripping function that’s missing from foreign libs: SDL_RectEmptyFloat
headers/sdl3/sdl_rect.h:374:23: Stripping function that’s missing from foreign libs: SDL_RectsEqualEpsilon
headers/sdl3/sdl_rect.h:409:23: Stripping function that’s missing from foreign libs: SDL_RectsEqualFloat
headers/sdl3/sdl_log.h:464:34: Stripping function with valist: SDL_LogMessageV
./headers/sdl3/sdl_main.h:342:47: Stripping function that’s missing from foreign libs: SDL_AppInit
./headers/sdl3/sdl_main.h:393:47: Stripping function that’s missing from foreign libs: SDL_AppIterate
./headers/sdl3/sdl_main.h:442:47: Stripping function that’s missing from foreign libs: SDL_AppEvent
./headers/sdl3/sdl_main.h:480:38: Stripping function that’s missing from foreign libs: SDL_AppQuit
./headers/sdl3/sdl_main.h:529:37: Stripping function that’s missing from foreign libs: SDL_main
headers/sdl3/sdl_main_impl.h:135:17: Stripping function that’s missing from foreign libs: main
generating 2812 global scope members...
1191 functions
160 structs
92 enums
69 system declarations were ignored.
28 declarations were stripped!
OK! generated 'bindings.jai'
The notices regarding functions that are missing from the library worried me at first. So I took a look at SDL_rect.h
and found inline functions such as:
SDL_FORCE_INLINE bool SDL_RectsEqualFloat(const SDL_FRect *a, const SDL_FRect *b)
{
return SDL_RectsEqualEpsilon(a, b, SDL_FLT_EPSILON);
}
These would indeed, not be present in the library then… so yeah. At the moment it doesn’t affect me directly, but it might perhaps be something I have to work around eventually.
Using the bindings Link to heading
As you could see in my previous post, the SDL3 bindings do their job well enough.
I think that some bits are a bit weird to use compared to C, and the generator options available are there to tune exactly that.
For me the main thing that comes to my mind is the scope of enum variants. This isn’t unique to Jai. Essentially every language that isn’t C has to deal with it.
The SDL2 bindings that are distributed with Jai are hand-crafted and as a result, quite nice! But since I’m choosing the trade-off of using the generator, I’ll have to spend more time exploring the generator options and finding the right balance.
What about C++? Link to heading
I haven’t tried this yet, but the Bindings_Generator
examples include a C++ example that apparently supports a lot of C++. Way more than I expected. This is encouraging to me because the alternative is to usually hand-craft a C API over a C++ library and then generate bindings from this.
That same C++ example also builds the library itself using the BuildCpp
module which is yet another very useful looking module to get familiar with.
Parting thoughts Link to heading
The bindings generator is a very good example of what I enjoy about Jai so far, the commitment to being useful and productive. That’s very meaningful because it’s one thing to just have a nice new language, but it’s something entirely different when you are committed to not losing the investments you’ve already made in your previous tools and libraries built with C or C++.
My intitial goals for exploring Jai involved three primary concepts:
- Work with existing C libraries
- Build an application
- Build a dll
The end result of my experiments aren’t going to cause any Sto Plains cabbages to explode but a vital question regarding how feasible Jai is for me personally has been answered affirmatively. 👍