mostly tests, more reload module examples

This commit is contained in:
Judah Caruso 2025-05-15 15:42:38 -06:00
parent 5562037b1d
commit 3f2d499025
9 changed files with 224 additions and 45 deletions

37
README
View file

@ -1 +1,36 @@
My base library for Jai
------
jx.jai
------
cd [jai install directory]/modules
git clone https://git.brut.systems/judah/jx.jai.git jx
#import "jx";
What
----
A set of modules for things I usually want in Jai, namely,
utilities, bindings, and experiments.
How
---
If you'd like to learn more about *what* a specific module
does, take a look at its 'module.jai' file.
Why
---
Because Jai is still in closed beta (as of May 15, 2025),
updates to the compiler and "standard library" will break
projects of mine; sometimes in a very annoying way.
jx.jai was made to 1) give myself an escape
hatch/skin-suit to cause fewer breaking changes when
updating the compiler, and 2) put all of my non-project
code in a single place that's easier to manage.
While I do use many of the modules shipped with the
compiler, my goal is to eventually replace them.

10
_run_all_tests.jai Normal file
View file

@ -0,0 +1,10 @@
#scope_file #run {
compiler :: #import "Compiler";
compiler.set_build_options_dc(.{ do_output = false });
#import,file "./module.jai"(true);
#import,file "./encoding/module.jai"(true);
}

View file

@ -2,7 +2,7 @@ Static_Array :: struct(capacity: int, T: Type) {
items: [capacity]T;
count: int;
Zero :: #run zero_value(T);
Default :: #run default_of(T);
}
operator [] :: inline (a: Static_Array, index: int, loc := #caller_location) -> a.T #no_abc {
@ -55,7 +55,7 @@ reset :: inline (a: *Static_Array, $keep_memory := true) {
a.count = 0;
}
else {
for 0..a.count - 1 a.items[it] = a.Zero;
for 0..a.count - 1 a.items[it] = a.Default;
a.count = 0;
}
}

View file

@ -1,6 +1,7 @@
#module_parameters(RUN_TESTS := false);
#load "base64.jai";
#load "json.jai";
#scope_module;

View file

@ -13,9 +13,7 @@ basic :: #import "Basic";
compiler :: #import "Compiler";
#if RUN_TESTS {
#run compiler.set_build_options_dc(.{ do_output = false });
// so files within this module can use 'test' without directly importing it.
test :: #import,file "test/module.jai";
#import,file "./encoding/module.jai"(RUN_TESTS);
}

View file

@ -1,26 +1,72 @@
/*
Building this program will result in two outputs:
1) an executable that controls memory,
hot-reloading, and the main loop.
2) a dynamic library the executable will call into.
This program is the library. The "host" executable is
automatically generated at build-time.
If you'd like to see code that isn't heavily annotated,
see: 'quickstart.jai'
*/
// The global state for this program.
// It's a pointer because the memory is allocated by the
// reload module and given to this program when init is called.
G: *struct {
allocator: Allocator;
iterations: int;
};
// The maximum amount of memory your program is allowed to use.
// Its true size is max_memory - state_size bytes.
#program_export max_memory: u64 = 4 * Gigabyte;
// The current size of the global state structure.
#program_export state_size: u64 = size_of(type_of(G.*));
#program_export init :: (state: *void, allocator: Allocator, reset: bool) {
// Called at program startup or on reload.
//
// 'allocator' should be stored in the global state to ensure
// memory allocated by this program persists between reloads
// and does not leak.
//
// 'full_reset' will be 'true' when the program is expected
// to (re)initialize its global state, as if the program restarted.
#program_export init :: (state: *void, allocator: Allocator, full_reset: bool) {
print("in: init\n");
G = xx state;
G = state.(type_of(G));
G.allocator = allocator;
if !full_reset return;
// initialize global state
G.iterations = 0;
}
// Called once at program startup (after init).
// Your window/rendering context should be created here.
#program_export setup :: () {
print("in: setup\n");
}
#program_export teardown :: () {
print("in: teardown\n");
}
// Called in a loop until this program exits.
// This is your classic update/render procedure.
//
// The status this returns tells the reload module
// what to do after this frame is done.
//
// .none: continue running
// .quit: stop running
// .soft_reload: reload without reinitializing state
// .hard_reload: reload then reinitialize state
//
// Unless given explicitly, soft_reload or hard_reload
// will be done automatically depending on how this
// program has changed between compiles.
#program_export frame :: () -> reload.Status {
G.iterations += 1; // change this line and rebuild the program library
@ -29,7 +75,11 @@ G: *struct {
return .none;
}
#import "Basic";
// Called once at program exit.
// This will be called before any program memory has been deallocated.
#program_export teardown :: () {
print("in: teardown\n");
}
#import "jx";
@ -39,3 +89,5 @@ G: *struct {
#poke_name reload teardown;
reload :: #import "jx/reload";
#import "Basic";

View file

@ -0,0 +1,59 @@
G: *struct {
allocator: Allocator;
};
#program_export max_memory: u64 = 4 * Gigabyte;
#program_export state_size: u64 = size_of(type_of(G.*));
#program_export init :: (state: *void, allocator: Allocator, full_reset: bool) {
G = state.(type_of(G));
G.allocator = allocator;
if !full_reset return;
}
#program_export setup :: () {
}
#program_export teardown :: () {
}
#program_export frame :: () -> reload.Status {
print("doing nothing for one second...\n");
sleep_milliseconds(1000);
return .none;
}
#import "jx";
#import "Basic";
DISABLE_HOT_RELOADING :: false;
#if DISABLE_HOT_RELOADING {
// @note(judah): dumb workaround because module parameters are weird
reload :: struct { Status :: (#import "jx/reload"(disabled = true)).Status; };
main :: () {
system_allocator := context.allocator;
state := alloc(xx state_size);
init(state, system_allocator, true);
setup();
while true {
status := frame();
if status == .quit break;
}
teardown();
}
}
else {
reload :: #import "jx/reload";
#poke_name reload frame;
#poke_name reload init;
#poke_name reload setup;
#poke_name reload teardown;
}

View file

@ -1,4 +1,11 @@
#module_parameters(disable := false, loc := #caller_location);
/*
A module that hijacks the build process of your project
to enable easy hot-reloading.
See: 'examples/everything-you-need.jai' for how this
module is intended to be used.
*/
#module_parameters(disabled := false, loc := #caller_location);
Status :: enum {
none;
@ -7,7 +14,7 @@ Status :: enum {
hard_reload;
}
Init_Proc :: #type (*void, Allocator, bool);
Init_Proc :: #type (state: *void, allocator: Allocator, full_reset: bool);
Setup_Proc :: #type ();
Teardown_Proc :: #type ();
Frame_Proc :: #type () -> Status;
@ -20,6 +27,10 @@ Frame_Proc :: #type () -> Status;
#placeholder setup;
#placeholder teardown;
#if !disabled {
#load "reload_main.jai";
}
// Ensure laptops use the higher performance GPU on Windows.
#if OS == .WINDOWS {
/*
@ -37,25 +48,24 @@ Frame_Proc :: #type () -> Status;
#program_export AmdPowerXpressRequestHighPerformance: u32 = 0x00000001;
}
#if !disable {
#load "reload_main.jai";
#scope_module;
#if !disabled {
#assert (type_of(init) == Init_Proc) "init was the wrong type!";
#assert (type_of(setup) == Setup_Proc) "setup was the wrong type!";
#assert (type_of(teardown) == Teardown_Proc) "teardown was the wrong type!";
#assert (type_of(frame) == Frame_Proc) "frame was the wrong type!";
}
#scope_file;
#assert (type_of(init) == Init_Proc) "init was the wrong type!";
#assert (type_of(setup) == Setup_Proc) "setup was the wrong type!";
#assert (type_of(teardown) == Teardown_Proc) "teardown was the wrong type!";
#assert (type_of(frame) == Frame_Proc) "frame was the wrong type!";
file_path :: #run -> string { return loc.fully_pathed_filename; }
proj_path :: #run -> string { return path_strip_filename(file_path); }
temp_path :: #run -> string { return tprint("%/.build/tmp.jai", proj_path); };
temp_exists :: #run -> bool { return file_exists(temp_path); }
file_path :: #run loc.fully_pathed_filename;
proj_path :: #run path_strip_filename(file_path);
temp_path :: #run tprint("%/.build/tmp.jai", proj_path);
temp_exists :: #run file_exists(temp_path);
lib_name :: #run tprint("%_lib", path_basename(file_path));
// only run the build when we the module importing us is compiled
#if !(disable || temp_exists) #run {
#if !(disabled || temp_exists) #run {
set_build_options_dc(.{ do_output = false });
make_directory_if_it_does_not_exist(".build");
@ -63,7 +73,7 @@ temp_exists :: #run -> bool { return file_exists(temp_path); }
out_path := path_strip_extension(proj_path);
out_name := path_strip_extension(path_filename(file_path));
// build target executable
// build host executable
{
ws := compiler_create_workspace(file_path);
defer compiler_destroy_workspace(ws);
@ -74,6 +84,10 @@ temp_exists :: #run -> bool { return file_exists(temp_path); }
opts.output_type = .EXECUTABLE;
opts.output_executable_name = out_name;
// create a temporary file that contains the hijacked main.
// we need to #load our program library so the #placeholders
// are filled.
tmp: String_Builder;
print_to_builder(*tmp, #string END
#load "%1";
@ -96,7 +110,7 @@ temp_exists :: #run -> bool { return file_exists(temp_path); }
compiler_end_intercept(ws);
}
// build library
// build program as dynamic library
{
ws := compiler_create_workspace(file_path);
defer compiler_destroy_workspace(ws);
@ -104,7 +118,7 @@ temp_exists :: #run -> bool { return file_exists(temp_path); }
opts := get_build_options(ws);
opts.output_path = out_path;
opts.output_type = .DYNAMIC_LIBRARY;
opts.output_executable_name = "lib";
opts.output_executable_name = lib_name;
compiler_begin_intercept(ws);
set_build_options(opts, ws);

View file

@ -1,26 +1,27 @@
reload_main :: () {
basic.set_working_directory(strings.path_strip_filename(system.get_path_of_running_executable()));
system_allocator := context.allocator;
// Ensure we're not allocating anywhere unexpected
context.allocator = Crash_Allocator;
basic.set_working_directory(strings.path_strip_filename(system.get_path_of_running_executable()));
lib_api, ok := load_library(0);
basic.assert(ok, "host: failed to load library");
lib_versions: [..]Library;
lib_versions.allocator = system_allocator;
// H always points to the currently loaded dynamic library.
H := lib_api;
next_lib_version := 1;
defer {
for * lib_versions {
unload_library(it);
cleanup_library(it);
}
unload_library(*H);
cleanup_library(*H);
basic.free(lib_versions.data,, allocator = system_allocator);
}
@ -72,17 +73,23 @@ reload_main :: () {
old_msize := H.max_memory;
new_ssize := new_api.state_size;
new_msize := new_api.max_memory;
if old_ssize != new_ssize || old_msize != new_msize {
status = .hard_reload;
}
// hard reloads are when full initialization has to happen again,
// as if we restarted the program. this is mostly due to memory/state
// size changing between reloads. however, programs can explicitly request this.
if status == .hard_reload {
basic.print("host: performing hard reload...\n");
// the currently allocated memory block is only safe to reuse
// if the memory size hasn't changed between reloads.
if old_msize <= new_msize {
lib_arena.offset = 0;
}
// otherwise we need to reallocate the program memory
// and reinitialize.
else {
free(lib_memory,, allocator = system_allocator);
@ -90,17 +97,20 @@ reload_main :: () {
basic.assert(lib_memory != null, "host: failed to allocator new memory");
init_arena(*lib_arena, lib_memory, new_msize);
lib_state = basic.alloc(xx new_ssize,, allocator = lib_allocator);
}
// in either case, we want ensure lib_state is at the start of our program's memory.
lib_state = basic.alloc(xx new_ssize,, allocator = lib_allocator);
}
else {
basic.print("host: performing soft reload...\n");
basic.array_add(*lib_versions, H);
// we don't need to do anything special for soft reloads, only transfer
// the current state pointer and allocator to the library.
basic.print("host: performing soft reload...\n");
basic.array_add(*lib_versions, H);
}
H = new_api;
next_lib_version += 1;
H.init(lib_state, lib_allocator, status == .hard_reload);
}
@ -124,8 +134,8 @@ Library :: struct {
mod_time: basic.Apollo_Time;
}
Default_Lib_Path :: "lib.%";
Temp_Lib_Path :: "__temp_lib_%.%";
Default_Lib_Path :: #run tprint("%.\%", lib_name);
Temp_Lib_Path :: #run tprint("__temp_%_\%.\%", lib_name);
#if OS == {
case .WINDOWS;
@ -171,7 +181,7 @@ load_library :: (version: int) -> Library, bool {
}, true;
}
unload_library :: (library: *Library) {
cleanup_library :: (library: *Library) {
path := basic.tprint(Temp_Lib_Path, library.version, Lib_Extension);
if library.handle && !os_unload_library(library.handle) {
basic.print("host: failed to unload temporary library: %\n", path);