Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
4b84c18
docs(brief): add M0.3 milestone brief
guysenpai May 25, 2026
d4a986d
docs(brief): confirm specs read for M0.3
guysenpai May 25, 2026
a3f689b
docs(brief): activate M0.3
guysenpai May 25, 2026
8511e75
feat(platform): common layer (fs/time/threading/dynamic_lib/once)
guysenpai May 25, 2026
0e84d91
feat(platform): dummy audio stub + platform commun tests
guysenpai May 25, 2026
70fb914
fix(platform): win32 thread safety on class globals (M0.3)
guysenpai May 25, 2026
1404fe2
docs(brief): journal update — waves 1-3 delivered (M0.3)
guysenpai May 25, 2026
78656a2
docs(brief): record decision — disable reactive split for M0.3
guysenpai May 25, 2026
d142224
docs(brief): bindgen-verify drift diagnosed as cache false-positive
guysenpai May 25, 2026
66295a3
docs(brief): wayland protocols already emitted by M0.2 bindgen
guysenpai May 25, 2026
ea7ee8e
feat(platform): extend Window interface + KeyCode enum (M0.3)
guysenpai May 25, 2026
c9169ee
feat(platform): win32 backend events + multi-monitor (M0.3)
guysenpai May 25, 2026
b281e7f
feat(platform): wayland backend events + multi-monitor (M0.3)
guysenpai May 25, 2026
292d18a
fix(platform): wire live_state + cleanup in wayland destroy (M0.3)
guysenpai May 25, 2026
0c68d30
feat(platform): input tier 0 (raw_state, xinput, evdev) (M0.3)
guysenpai May 25, 2026
1c6e5df
feat(platform): final tests + lefthook update + close M0.3
guysenpai May 25, 2026
8ce8a37
fix(platform): guard test-tsan-wayland to linux-only hosts (M0.3)
guysenpai May 25, 2026
2eb071f
fix(platform): tolerate pthread_setschedparam EPERM in CI containers
guysenpai May 25, 2026
40a4b17
fix(platform): windows debug CI — tighten error sets + cap stress test
guysenpai May 25, 2026
a0ac7cb
fix(platform): windows ci — portable test path + stress timeout
guysenpai May 25, 2026
1a0c677
fix(platform): tolerate < 5% transient create failures in win32 stress
guysenpai May 25, 2026
ba29a68
fix(platform): warm up class atom before stability check in win32 stress
guysenpai May 25, 2026
7428b81
docs(platform): record Phase 1+ transfer notes on M0.3 debts
guysenpai May 25, 2026
2f977db
docs(brief): journal STOP-MERGE — segfault + leak at Fedora
guysenpai May 25, 2026
f84a100
test(platform): stabilize wayland_thread_safety on Phase 0 invariant
guysenpai May 25, 2026
e97b971
fix(bindgen): emit non-null types arrays for WlMessage entries
guysenpai May 25, 2026
9ed7393
fix(bindgen): expand wire args for untyped new_id in types arrays
guysenpai May 25, 2026
7194b87
feat(wayland): regenerate protocol bindings with proper types arrays
guysenpai May 25, 2026
af6b52b
fix(bindgen): qualify cross-module interface refs in types arrays
guysenpai May 25, 2026
23199a9
feat(wayland): regenerate cross-module refs (xdg + decoration)
guysenpai May 25, 2026
09a27b8
fix(bindgen): break cycle on self-referential types arrays
guysenpai May 25, 2026
3b5f2a0
feat(wayland): regenerate with explicit-size types arrays
guysenpai May 25, 2026
d803011
docs(brief): record .types=null bindgen bug + Phase 0+ debts
guysenpai May 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
318 changes: 318 additions & 0 deletions briefs/M0.3-platform-extend-and-input.md

Large diffs are not rendered by default.

33 changes: 33 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@ pub fn build(b: *std.Build) void {
});
etch_module.addImport("weld_core", core_module);

// M0.3 — `weld_audio` module exposes the Tier 1 audio module entry
// (Dummy backend Phase 0, real backends Phase 1). Consumed by the
// audio tests and, later, by the runtime once the audio strategy
// selection wires in.
const audio_module = b.createModule(.{
.root_source_file = b.path("src/modules/audio/main.zig"),
.target = target,
.optimize = optimize,
});

// M0.2 / E6 — plugin loader ABI module shared with the stub
// plugin sub-projects under `tests/core/plugin_loader/stub_plugin/`.
// Exposes the C ABI types from `desc.zig` (no `WeldAPI` itself,
Expand Down Expand Up @@ -204,6 +214,8 @@ pub fn build(b: *std.Build) void {
/// `stub_install_steps[]` so the three stub libraries are
/// built before the test runs.
needs_stub_plugins: bool = false,
/// M0.3 — when set, imports the `weld_audio` module.
audio: bool = false,
};
const test_specs = [_]TestSpec{
.{ .path = "tests/smoke_test.zig" },
Expand Down Expand Up @@ -247,6 +259,24 @@ pub fn build(b: *std.Build) void {
.{ .path = "tests/bindings/wayland_abi_test.zig", .wl_protocols = true },
.{ .path = "tests/etch/corpus_test.zig", .etch = true },
.{ .path = "tests/etch_interp/corpus_test.zig", .etch_interp = true },
// M0.3 — platform commun layer tests.
.{ .path = "tests/platform/fs_vfs_test.zig" },
.{ .path = "tests/platform/time_test.zig" },
.{ .path = "tests/platform/threading_test.zig" },
.{ .path = "tests/platform/dynamic_lib_test.zig" },
// M0.3 — Win32 thread safety stress (Windows runner only).
.{ .path = "tests/platform/win32_thread_safety_test.zig" },
// M0.3 — Wayland thread safety stress (Linux runner only).
.{ .path = "tests/platform/wayland_thread_safety_test.zig" },
// M0.3 — Multi-monitor enumeration + current monitor + per-monitor DPI.
.{ .path = "tests/platform/multi_monitor_test.zig" },
// M0.3 — WindowEvent union surface validation.
.{ .path = "tests/platform/window_events_test.zig" },
// M0.3 — Input Tier 0 (event-driven path, runs on all OSes).
.{ .path = "tests/platform/input_raw_state_test.zig" },
.{ .path = "tests/platform/input_gamepad_test.zig" },
// M0.3 — Audio Dummy stub test.
.{ .path = "tests/audio/dummy_stub_test.zig", .audio = true },
};
for (test_specs) |spec| {
const t_mod = b.createModule(.{
Expand All @@ -271,6 +301,9 @@ pub fn build(b: *std.Build) void {
t_mod.addImport("diff_runner", etch_interp_driver_module);
t_mod.addImport("runner_interp", etch_interp_runner_module);
}
if (spec.audio) {
t_mod.addImport("weld_audio", audio_module);
}
const t = b.addTest(.{ .root_module = t_mod });
const t_run = b.addRunArtifact(t);
if (spec.needs_stub_plugins) {
Expand Down
18 changes: 18 additions & 0 deletions lefthook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
# M0.0: `commit-msg` now invokes the Zig linter (`zig build lint-commit`)
# instead of the shell-script fallback, and `pre-commit` adds a
# `zig build lint` pass alongside `zig fmt --check`.
# M0.3: `pre-push` adds a TSan rerun of the Wayland stress test
# (Linux-only compensation for the absent TSan path in the CI matrix).

pre-commit:
parallel: true
Expand All @@ -28,3 +30,19 @@ pre-push:
run: zig build test
test-release:
run: zig build test -Doptimize=ReleaseSafe
# M0.3 — local-only thread-safety rerun on the Wayland stress test
# with ThreadSanitizer (TSan). The Linux CI matrix does not ship
# TSan toolchains for the runner image we target, so this hook acts
# as the M0.3 garde-fou. Wrapped in a Linux-only guard because the
# `-fsanitize=thread` CLI flag isn't recognized as a standalone
# `zig test` argument on macOS / Windows hosts (Zig's flag set is
# frontend-target-dependent). On macOS / Windows dev boxes this
# entire command short-circuits to `true`, keeping the pre-push
# green; on Linux it runs the TSan-instrumented test.
test-tsan-wayland:
run: |
if [ "$(uname -s)" = "Linux" ]; then
zig test tests/platform/wayland_thread_safety_test.zig -fsanitize=thread --dep weld_core -Mroot=tests/platform/wayland_thread_safety_test.zig -Mweld_core=src/core/root.zig -lc
else
true
fi
147 changes: 147 additions & 0 deletions src/core/platform/dynamic_lib.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
//! Dynamic library loader — `DynamicLib { open, lookup, close }`.
//!
//! Phase 0.3 / M0.3 deliverable. Documented in `engine-platform.md` §4
//! (Dynamic loader section) and the M0.3 brief.
//!
//! Cohérent avec `engine-c-bindings.md` §4.6 (pattern dlopen par stratégie)
//! et `engine-c-bindings.md` §4.6.5 (lifecycle de chargement par module).
//! Le générateur bindgen produira les Symbols structs au-dessus de cette
//! couche basse — `DynamicLib` est l'API portable utilisée par les
//! bindings générés Phase 1+.
//!
//! Backends:
//! - Win32 : `LoadLibraryW` + `GetProcAddress` + `FreeLibrary`
//! - POSIX : `dlopen` + `dlsym` + `dlclose`
//!
//! ## Path semantics
//!
//! Paths are passed as UTF-8 byte slices. On Win32 they are converted to
//! UTF-16 (`LoadLibraryW`); on POSIX they are passed null-terminated to
//! `dlopen`. The caller is responsible for picking an OS-appropriate path
//! (`opus-0.dll` vs `libopus.so.0` vs `libopus.0.dylib`) — `DynamicLib`
//! itself does not do soname mangling.
//!
//! The higher-level bindgen `dlopen` strategy adds the multi-version
//! fallback on top (cf. `engine-c-bindings.md` §4.6.1).

const std = @import("std");
const builtin = @import("builtin");

/// Errors surfaced by `DynamicLib.open` / `lookup` / `close`.
pub const Error = error{
LibraryNotFound,
SymbolNotFound,
InvalidPath,
} || std.mem.Allocator.Error;

const win = struct {
extern "kernel32" fn LoadLibraryW(lpLibFileName: [*:0]const u16) callconv(.winapi) ?*anyopaque;
extern "kernel32" fn GetProcAddress(hModule: *anyopaque, lpProcName: [*:0]const u8) callconv(.winapi) ?*const anyopaque;
extern "kernel32" fn FreeLibrary(hLibModule: *anyopaque) callconv(.winapi) i32;
};

const posix = struct {
extern "c" fn dlopen(filename: ?[*:0]const u8, flag: c_int) ?*anyopaque;
extern "c" fn dlsym(handle: ?*anyopaque, symbol: [*:0]const u8) ?*anyopaque;
extern "c" fn dlclose(handle: *anyopaque) c_int;
// RTLD_NOW = resolve all symbols at open. RTLD_LOCAL keeps the lib
// private to this handle. These values are stable across glibc, musl,
// and macOS dyld.
const RTLD_LAZY: c_int = 1;
const RTLD_NOW: c_int = switch (builtin.os.tag) {
.linux => 2,
.macos => 2,
else => 2,
};
const RTLD_LOCAL: c_int = 0;
};

/// Opaque OS-level dynamic library handle. Returned by `open`, consumed by
/// `lookup` and `close`.
pub const DynamicLib = struct {
handle: *anyopaque,

/// Open a shared library by path. Path is interpreted by the OS loader
/// rules (PATH, LD_LIBRARY_PATH, dyld search, etc.). Returns
/// `error.LibraryNotFound` if the loader cannot resolve.
pub fn open(gpa: std.mem.Allocator, path: []const u8) Error!DynamicLib {
switch (builtin.os.tag) {
.windows => {
// Convert UTF-8 -> UTF-16 (LoadLibraryW). std.unicode helpers.
const wide = std.unicode.utf8ToUtf16LeAllocZ(gpa, path) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
else => return error.InvalidPath,
};
defer gpa.free(wide);
const h = win.LoadLibraryW(wide.ptr) orelse return error.LibraryNotFound;
return .{ .handle = h };
},
.linux, .macos => {
const path_z = gpa.dupeZ(u8, path) catch return error.OutOfMemory;
defer gpa.free(path_z);
const h = posix.dlopen(path_z.ptr, posix.RTLD_NOW | posix.RTLD_LOCAL) orelse {
return error.LibraryNotFound;
};
return .{ .handle = h };
},
else => return error.LibraryNotFound,
}
}

/// Resolve a symbol from the opened library. Returns the raw pointer;
/// caller is responsible for `@ptrCast` to the appropriate function
/// pointer type.
pub fn lookup(self: DynamicLib, gpa: std.mem.Allocator, symbol: []const u8) Error!*const anyopaque {
const symbol_z = gpa.dupeZ(u8, symbol) catch return error.OutOfMemory;
defer gpa.free(symbol_z);
switch (builtin.os.tag) {
.windows => {
const p = win.GetProcAddress(self.handle, symbol_z.ptr) orelse return error.SymbolNotFound;
return p;
},
.linux, .macos => {
const p = posix.dlsym(self.handle, symbol_z.ptr) orelse return error.SymbolNotFound;
return @ptrCast(p);
},
else => return error.SymbolNotFound,
}
}

/// Close the library. After this call `self.handle` is invalid.
pub fn close(self: *DynamicLib) void {
switch (builtin.os.tag) {
.windows => _ = win.FreeLibrary(self.handle),
.linux, .macos => _ = posix.dlclose(self.handle),
else => {},
}
self.handle = undefined;
}
};

test "dynamic_lib.DynamicLib: open + lookup + close on system library" {
const gpa = std.testing.allocator;
// Use libSystem on macOS, libc.so.6 on Linux, kernel32.dll on Win32.
const lib_path = switch (builtin.os.tag) {
.linux => "libc.so.6",
.macos => "/usr/lib/libSystem.B.dylib",
.windows => "kernel32.dll",
else => return error.SkipZigTest,
};
const sym = switch (builtin.os.tag) {
.linux, .macos => "memcpy",
.windows => "GetTickCount",
else => return error.SkipZigTest,
};

var lib = try DynamicLib.open(gpa, lib_path);
defer lib.close();

const ptr = try lib.lookup(gpa, sym);
try std.testing.expect(@intFromPtr(ptr) != 0);
}

test "dynamic_lib.DynamicLib: open returns LibraryNotFound for missing lib" {
const gpa = std.testing.allocator;
const result = DynamicLib.open(gpa, "definitely_not_a_real_library_name_xyz123.so.999");
try std.testing.expectError(error.LibraryNotFound, result);
}
Loading
Loading