diff --git a/build.zig b/build.zig index ba0fd0c9..382c9791 100644 --- a/build.zig +++ b/build.zig @@ -111,6 +111,24 @@ pub fn build(b: *std.build.Builder) void { const compile_all = b.step("compile-all", "Compile all examples and applications"); compile_all.dependOn(b.getInstallStep()); + + // compiles the `libmach` shared library + const lib = b.addSharedLibrary("mach", "src/bindings.zig", .unversioned); + const app_pkg = std.build.Pkg{ + .name = "app", + .source = .{ .path = "src/platform/libmach.zig" }, + }; + lib.addPackage(app_pkg); + lib.addPackage(gpu.pkg); + lib.addPackage(glfw.pkg); + const gpu_options = gpu.Options{ + .glfw_options = @bitCast(@import("gpu/libs/mach-glfw/build.zig").Options, options.glfw_options), + .gpu_dawn_options = @bitCast(@import("gpu/libs/mach-gpu-dawn/build.zig").Options, options.gpu_dawn_options), + }; + glfw.link(b, lib, options.glfw_options); + gpu.link(b, lib, gpu_options); + lib.setOutputDir("./libmach/build"); + lib.install(); } pub const Options = struct { diff --git a/libmach/.gitignore b/libmach/.gitignore new file mode 100644 index 00000000..b018793c --- /dev/null +++ b/libmach/.gitignore @@ -0,0 +1,2 @@ +build/ +test diff --git a/libmach/Makefile b/libmach/Makefile new file mode 100644 index 00000000..f5dd8ce8 --- /dev/null +++ b/libmach/Makefile @@ -0,0 +1,12 @@ +build/libmach.dylib: ../build.zig ../src/*.zig ../src/**/*.zig + cd ..; zig build + +test: test.c build/libmach.dylib + clang -L./build -lmach -o test test.c + +test_c: test + # my best attempt at cross-platform dynamic linking (for now) + DYLD_LIBRARY_PATH=./build LD_LIBRARY_OATH=./build ./test + +test_lisp: build/libmach.dylib + sbcl --load test.lisp diff --git a/libmach/README.md b/libmach/README.md new file mode 100644 index 00000000..a30f077c --- /dev/null +++ b/libmach/README.md @@ -0,0 +1,11 @@ +# libmach + +Build the `libmach` dynamic library by running `make` (or running `zig build` in the parent directory). +The resulting binary should be located in `libmach/build/`. + +Test the functionality of `libmach` using `make test_c` and `make test_lisp`. +These commands use C and Lisp to call into `libmach`, and both should show a blank window for exactly 1 second. + +Note: `make test_lisp` requires a relatively recent version of Steel Bank Common Lisp (`sbcl`) to be installed. + +You can find the Zig source code for `libmach` in `src/bindings.zig`. diff --git a/libmach/test.c b/libmach/test.c new file mode 100644 index 00000000..4583a45b --- /dev/null +++ b/libmach/test.c @@ -0,0 +1,40 @@ +#include +#include + +typedef void mach_core_callback(void*); + +// `libmach` exported API bindings +void mach_core_set_init(mach_core_callback); +void mach_core_set_update(mach_core_callback); +void mach_core_set_deinit(mach_core_callback); +void mach_run(void); +void core_set_should_close(void*); +float core_delta_time(void*); + +static float elapsed = 0; + +void my_init(void* core) { + printf("My init!\n"); +} + +void my_update(void* core) { + float dt = core_delta_time(core); + if (elapsed < 1.0) { + elapsed += dt; + } else { + core_set_should_close(core); + } + printf("My update! total time = %f\n", elapsed); +} + +void my_deinit(void* core) { + printf("My deinit!\n"); +} + +int main() { + mach_core_set_init(my_init); + mach_core_set_update(my_update); + mach_core_set_deinit(my_deinit); + mach_run(); + return 0; +} diff --git a/libmach/test.lisp b/libmach/test.lisp new file mode 100644 index 00000000..9423b4ba --- /dev/null +++ b/libmach/test.lisp @@ -0,0 +1,64 @@ +;; Tests the behavior of `libmach` using Common Lisp's CFFI +;; This Lisp script is basically a one-to-one translation of `test.c` + +(ql:quickload :cffi) + +(defpackage :cffi-user + (:use :cl :cffi)) + +(in-package :cffi-user) + +(define-foreign-library libmach + (t (:default "./build/libmach"))) + +(use-foreign-library libmach) + +;; Note: CFFI automatically translates C_style names into lispier kebab-case ones + +;; typedef void mach_core_callback(void*); +(defctype mach-core-callback :pointer) + +;; void mach_core_set_init(mach_core_callback); +(defcfun "mach_core_set_init" :void + (callback mach-core-callback)) + +;; void mach_core_set_update(mach_core_callback); +(defcfun "mach_core_set_update" :void + (callback mach-core-callback)) + +;; void mach_core_set_deinit(mach_core_callback); +(defcfun "mach_core_set_deinit" :void + (callback mach-core-callback)) + +;; void mach_run(void); +(defcfun "mach_run" :void) + +;; void core_set_should_close(void*); +(defcfun "core_set_should_close" :void (core :pointer)) + +;; float core_delta_time(void*); +(defcfun "core_delta_time" :float (core :pointer)) + +(defcallback my-init :void ((core :pointer)) + (format t "Hello from my-init!~%")) + +(defvar *elapsed* 0.0) + +(defcallback my-update :void ((core :pointer)) + (format t "Hello from my-update ~a~%" *elapsed*) + (if (< *elapsed* 1.0) + (incf *elapsed* (core-delta-time core)) + (core-set-should-close core))) + +(defcallback my-deinit :void ((core :pointer)) + (format t "Hello from my-deinit!~%")) + +(mach-core-set-init (callback my-init)) + +(mach-core-set-update (callback my-update)) + +(mach-core-set-deinit (callback my-deinit)) + +(mach-run) + +(sb-ext:exit) diff --git a/src/bindings.zig b/src/bindings.zig new file mode 100644 index 00000000..71f5e78c --- /dev/null +++ b/src/bindings.zig @@ -0,0 +1,52 @@ +const std = @import("std"); +const gpu = @import("gpu"); +const Core = @import("Core.zig"); +const libmach = @import("platform/libmach.zig"); +const native = @import("platform/native.zig"); + +// Current Limitations: +// 1. Currently, ecs seems to be using some weird compile-time type trickery, so I'm not exactly sure how +// `engine` should be integrated into the C API +// 2. Core might need to expose more state so more API functions can be exposed (for example, the WebGPU API) +// 3. Be very careful about arguments, types, memory, etc - any mismatch will result in undefined behavior + +pub const App = libmach; + +pub export fn mach_core_set_init(core_init: libmach.CoreCallback) void { + std.debug.print("mach core set init\n", .{}); + libmach.core_callbacks.core_init = core_init; +} + +pub export fn mach_core_set_deinit(core_deinit: libmach.CoreCallback) void { + std.debug.print("mach core set deinit\n", .{}); + libmach.core_callbacks.core_deinit = core_deinit; +} + +pub export fn mach_core_set_update(core_update: libmach.CoreCallback) void { + std.debug.print("mach core set update\n", .{}); + libmach.core_callbacks.core_update = core_update; +} + +pub export fn mach_run() void { + if (libmach.core_callbacks.core_init == null) { + std.debug.print("Did not provide a core_init callback\n", .{}); + return; + } + if (libmach.core_callbacks.core_update == null) { + std.debug.print("Did not provide a core_update callback\n", .{}); + return; + } + if (libmach.core_callbacks.core_deinit == null) { + std.debug.print("Did not provide a core_deinit callback\n", .{}); + return; + } + native.main() catch unreachable; +} + +pub export fn core_set_should_close(core: *Core) void { + core.*.setShouldClose(true); +} + +pub export fn core_delta_time(core: *Core) f32 { + return core.*.delta_time; +} diff --git a/src/platform/libmach.zig b/src/platform/libmach.zig new file mode 100644 index 00000000..d521cf07 --- /dev/null +++ b/src/platform/libmach.zig @@ -0,0 +1,35 @@ +const std = @import("std"); +const Core = @import("../Core.zig"); +const gpu = @import("gpu"); +const ecs = @import("ecs"); + +pub const App = @This(); + +// Zig says that *App has a size of 0 bits, and it won't compile if +// pub const core_callback_t = fn (*App, *Core) callconv(.C) void; +// What is *App needed for anyway? +pub const CoreCallback = fn (*Core) callconv(.C) void; + +pub const CoreCallbacks = struct { + core_init: ?CoreCallback, + core_update: ?CoreCallback, + core_deinit: ?CoreCallback, +}; + +pub var core_callbacks = CoreCallbacks { + .core_init = null, + .core_update = null, + .core_deinit = null, +}; + +pub fn init(_: *App, core: *Core) !void { + core_callbacks.core_init.?(core); +} + +pub fn deinit(_: *App, core: *Core) void { + core_callbacks.core_deinit.?(core); +} + +pub fn update(_: *App, core: *Core) !void { + core_callbacks.core_update.?(core); +}