windows implementation

This commit is contained in:
Peter Li 2026-09-18 02:50:00 -07:00
parent 0c8270fbef
commit d1c4f14877
13 changed files with 1947 additions and 36 deletions

View File

@ -1,20 +1,25 @@
# Uhdk
A small macOS desktop platform library written in Zig 0.16.0 with a C99 ABI.
A small desktop platform library written in Zig 0.16.0 with a C99 ABI.
It provides windows, input and IME, clipboard, file dialogs, drag and drop, menus,
async file operations, file watching, and OpenGL context creation. The macOS
backend uses AppKit, OpenGL, and FSEvents through a private Objective-C bridge.
This directory is a standalone source package. It does not depend on the parent
repository, its examples, fonts, skills, or development tools. Currently macOS is
the only implemented backend. Building requires Xcode Command Line Tools.
repository, its examples, fonts, skills, or development tools. macOS builds require
Xcode Command Line Tools. The Windows Win32/WGL backend cross-compiles for
`x86_64-windows-gnu`; runtime verification on Windows is pending. It requires
Windows 10 version 1703 or later and an OpenGL 3.3-capable driver.
## Build
```sh
zig build # zig-out/lib/libuhdk.a and zig-out/include/
zig build -Dshared=true # zig-out/lib/libuhdk.dylib and headers
zig build test # core library tests
zig build test # run core library tests on the native target
zig build -Dtarget=x86_64-windows-gnu
zig build -Dtarget=x86_64-windows-gnu -Dshared=true
zig build check -Dtarget=x86_64-windows-gnu # compile only
```
## Use from Zig
@ -63,3 +68,12 @@ The library includes the GLAD loader implementation. With a current context insi
`uhdk_gl_context_get_proc`. Keep a `GladGLContext` per native context; its members
are named `Clear`, `DrawArrays`, etc. Check `VERSION_3_3` after loading. Do not define
`GLAD_GL_IMPLEMENTATION` in your application when linking libuhdk.
On Windows, the static artifact is `zig-out/lib/uhdk.lib`; shared builds install
`zig-out/bin/uhdk.dll` and its import library under `zig-out/lib/`. Keep the DLL
beside your executable. The first lifecycle caller establishes the Windows
platform thread; its message loop also owns COM dialogs, menus, clipboard and
watch completion. Input uses IMM32 composition; TSF reconversion and pen/touch
are not implemented. Native UTF-16 path bytes are kept separately from the WTF-8
paths passed to Zig I/O. If a native path cannot be exported losslessly as a shell
URI, URI export returns `UNSUPPORTED`; native-path export still preserves it.

View File

@ -2,7 +2,7 @@ const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
if (target.result.os.tag != .macos) @panic("Uhdk currently implements macOS only.");
if (target.result.os.tag != .macos and target.result.os.tag != .windows) @panic("Uhdk supports macOS and Windows.");
const optimize = b.standardOptimizeOption(.{});
const shared = b.option(bool, "shared", "Build libuhdk as a shared library instead of static") orelse false;
const module = b.createModule(.{
@ -12,25 +12,30 @@ pub fn build(b: *std.Build) void {
.link_libc = true,
});
const native = b.addTranslateC(.{
.root_source_file = b.path("src/macos/native.h"),
.root_source_file = b.path("src/native.h"),
.target = target,
.optimize = optimize,
});
native.addIncludePath(b.path("include"));
module.addImport("native", native.createModule());
module.addIncludePath(b.path("include"));
module.addIncludePath(b.path("src/macos"));
module.addCSourceFile(.{
.file = b.path("src/macos/native.m"),
.flags = &.{
"-fobjc-arc", "-fblocks", "-Wno-deprecated-declarations",
"-Wall", "-Wextra", "-Werror",
},
});
module.addCSourceFile(.{ .file = b.path("src/glad.c"), .flags = &.{"-std=c99"} });
module.linkFramework("Cocoa", .{});
module.linkFramework("OpenGL", .{});
module.linkFramework("CoreServices", .{});
module.addIncludePath(b.path("src"));
if (target.result.os.tag == .macos) {
module.addCSourceFile(.{
.file = b.path("src/macos/native.m"),
.flags = &.{
"-fobjc-arc", "-fblocks", "-Wno-deprecated-declarations",
"-Wall", "-Wextra", "-Werror",
},
});
module.linkFramework("Cocoa", .{});
module.linkFramework("OpenGL", .{});
module.linkFramework("CoreServices", .{});
} else {
module.addCSourceFile(.{ .file = b.path("src/windows/native.c"), .flags = &.{ "-std=c11", "-Wall", "-Wextra", "-Werror", "-DUNICODE", "-D_UNICODE" } });
for ([_][]const u8{ "user32", "gdi32", "opengl32", "shell32", "ole32", "uuid", "imm32", "comctl32", "shlwapi", "advapi32" }) |name| module.linkSystemLibrary(name, .{});
}
module.addCSourceFile(.{ .file = b.path("src/glad.c"), .flags = if (shared) &.{ "-std=c99", "-DGLAD_API_CALL_EXPORT", "-DGLAD_API_CALL_EXPORT_BUILD" } else &.{"-std=c99"} });
const lib = b.addLibrary(.{
.name = "uhdk",
.linkage = if (shared) .dynamic else .static,
@ -48,6 +53,9 @@ pub fn build(b: *std.Build) void {
b.getInstallStep().dependOn(&install_lib.step);
b.getInstallStep().dependOn(&install_headers.step);
const unit = b.addTest(.{ .root_module = module });
const check_step = b.step("check", "Compile library and core tests without running them");
check_step.dependOn(&lib.step);
check_step.dependOn(&unit.step);
const unit_run = b.addRunArtifact(unit);
b.step("test", "Run core library tests").dependOn(&unit_run.step);
const public_c = b.addTranslateC(.{
@ -56,6 +64,7 @@ pub fn build(b: *std.Build) void {
.optimize = optimize,
});
public_c.addIncludePath(b.path("include"));
if (shared) public_c.defineCMacro("UHDK_SHARED", "1");
const public_module = b.addModule("uhdk", .{
.root_source_file = public_c.getOutput(),
.target = target,

View File

@ -2,7 +2,7 @@
#define UHDK_H_INCLUDED
/*
* Uhdk desktop platform API — v0.1, macOS backend.
* Uhdk desktop platform API — v0.1, macOS and Windows backends.
*
* Implementation: Zig. ABI: C. Header baseline: C99 (also usable from C++).
* No libc runtime objects or allocation contract.
@ -484,7 +484,10 @@ UHDK_API uhdk_status UHDK_CALL uhdk_window_cancel_composition_async(uhdk_window
/* File references preserve native names, including non-UTF-8 POSIX paths.
* Display names are UTF-8. URI/native-path export is explicit and owned by the
* caller's buffer. Windows native path bytes are UTF-16LE (no terminator),
* POSIX native paths are uninterpreted bytes. A reference can carry an access
* POSIX native paths are uninterpreted bytes. Windows URI export returns
* UNSUPPORTED when the shell cannot represent a native name losslessly (such
* as unpaired UTF-16 surrogates); native-path export remains available.
* A reference can carry an access
* grant; retaining it keeps that grant alive where the OS permits. Persistence
* across launches/bookmark serialization is deferred to a future extension.
* Buffer queries: NULL/0 gets required byte count in out_len and returns UHDK_STATUS_OK.
@ -523,9 +526,11 @@ typedef struct uhdk_file_info {
* Read/write accept regular files only; final-component symlinks are rejected.
* Write copies bytes at submission, stages an exclusive same-directory temp,
* preserves POSIX mode of an existing regular file, then renames atomically.
* Windows rejects read-only destinations; replacement gets the directory's
* inherited permissions and does not preserve other destination attributes.
* Symlinks/nonregular destinations are rejected. ACLs/xattrs/ownership are not
* preserved; replacement requires a directory writable by the current user.
* New files use mode 0600. No power-loss durability or conflict detection promise.
* New POSIX files use mode 0600. No power-loss durability or conflict detection promise.
* No silent truncate fallback. Cancellation before rename leaves the target intact.
*/
UHDK_API uhdk_status UHDK_CALL uhdk_file_read_all_async(uhdk_file *, uint64_t max_bytes,

View File

@ -11,6 +11,9 @@
* uhdk_gl_context_get_proc, inside a begin_frame/end_frame bracket. Keep one
* table per context and use it only while that context is current.
*/
#if defined(UHDK_SHARED)
#define GLAD_API_CALL_EXPORT
#endif
#include "glad/gl.h"
#endif /* UHDK_OPENGL_H_INCLUDED */

View File

@ -73,17 +73,19 @@ pub fn step(self: *FileOperation) !Result {
};
if (stat) |existing| {
if (existing.kind != .file) return error.NotRegularFile;
if (@import("builtin").os.tag == .windows and existing.permissions.toAttributes().READONLY)
return error.AccessDenied;
}
self.atomic_file = try cwd.createFileAtomic(self.io, self.path, .{
.permissions = .fromMode(0o600),
.permissions = if (@import("builtin").os.tag == .windows) .default_file else .fromMode(0o600),
.replace = true,
});
if (stat) |existing| {
if (@import("builtin").os.tag != .windows) if (stat) |existing| {
try self.atomic_file.?.file.setPermissions(
self.io,
.fromMode(existing.permissions.toMode() & 0o777),
);
}
};
},
.info => unreachable,
}

View File

@ -45,6 +45,7 @@ pub const Gl = struct {
};
pub const File = struct {
native_path: []const u8 = &.{},
path: [:0]const u8 = "",
uri: []const u8 = "",
display: []const u8 = "",

View File

@ -1,5 +1,6 @@
//! C ABI entry points for app.
const std = @import("std");
const backend_kind = if (@import("builtin").os.tag == .windows) c.UHDK_BACKEND_WINDOWS else c.UHDK_BACKEND_MACOS;
const c = @import("native");
const runtime = @import("../runtime.zig");
const abi = @import("../abi.zig");
@ -19,7 +20,7 @@ pub export fn uhdk_app_create(desc: [*c]const c.uhdk_app_desc, out: [*c]?*c.uhdk
desc.*.reserved != 0 or
!abi.validText(desc.*.display_name, true) or
!abi.validText(desc.*.application_id, true)) return abi.INVALID;
if (desc.*.preferred_backend != c.UHDK_BACKEND_AUTO and desc.*.preferred_backend != c.UHDK_BACKEND_MACOS) return c.UHDK_STATUS_UNSUPPORTED;
if (desc.*.preferred_backend != c.UHDK_BACKEND_AUTO and desc.*.preferred_backend != backend_kind) return c.UHDK_STATUS_UNSUPPORTED;
if (singleton) return c.UHDK_STATUS_BUSY;
const gpa = std.heap.smp_allocator;
const backend = gpa.create(std.Io.Threaded) catch return abi.OOM;
@ -218,7 +219,7 @@ pub export fn uhdk_app_set_wake_deadline(handle: ?*c.uhdk_app, deadline: u64) ab
}
pub export fn uhdk_app_get_backend(_: ?*c.uhdk_app) c.uhdk_backend {
return c.UHDK_BACKEND_MACOS;
return backend_kind;
}
pub export fn uhdk_app_has_capability(_: ?*c.uhdk_app, cap: c.uhdk_capability) c.uhdk_bool {

View File

@ -38,9 +38,9 @@ pub export fn uhdk_file_create_from_native_path(
if (handle == null or
out == null or
!abi.validSpan(path.ptr, path.len) or
path.len == 0 or
abi.bytes(path)[0] != '/' or
std.mem.findScalar(u8, abi.bytes(path), 0) != null) return abi.INVALID;
path.len == 0) return abi.INVALID;
if (@import("builtin").os.tag != .windows and
(abi.bytes(path)[0] != '/' or std.mem.findScalar(u8, abi.bytes(path), 0) != null)) return abi.INVALID;
if (runtime.checkThread(runtime.fromAppHandle(handle)) != abi.OK) return c.UHDK_STATUS_WRONG_THREAD;
const object = runtime.createObject(runtime.fromAppHandle(handle), c.Z_KIND_FILE) catch return abi.OOM;
const status = c.z_native_file_init(runtime.castHandle(*c.uhdk_file, object), path, .{ .ptr = null, .len = 0 }, null);
@ -91,9 +91,11 @@ pub export fn uhdk_file_copy_representation(
const data = switch (form) {
c.UHDK_FILE_DISPLAY_NAME => object.payload.file.display,
c.UHDK_FILE_URI => object.payload.file.uri,
c.UHDK_FILE_NATIVE_PATH => object.payload.file.path,
c.UHDK_FILE_NATIVE_PATH => object.payload.file.native_path,
else => return abi.INVALID,
};
if (@import("builtin").os.tag == .windows and form == c.UHDK_FILE_URI and data.len == 0)
return c.UHDK_STATUS_UNSUPPORTED;
out.* = data.len;
if (buffer == null and capacity == 0) return abi.OK;
if (capacity < data.len) return c.UHDK_STATUS_BUFFER_TOO_SMALL;

View File

@ -1,8 +1,8 @@
//! C ABI entry points for callbacks.
const std = @import("std");
const c = @import("native");
const abi = @import("../abi.zig");
const runtime = @import("../runtime.zig");
const abi = @import("abi.zig");
const runtime = @import("runtime.zig");
export fn z_emit(handle: ?*c.uhdk_app, event: [*c]const c.uhdk_event) void {
runtime.emit(handle, event);
@ -65,7 +65,15 @@ export fn z_file_init(
) abi.Status {
const object = runtime.fromObjectHandle(handle);
const arena = object.arena.allocator();
object.payload.file.path = arena.dupeZ(u8, abi.bytes(path)) catch return abi.OOM;
object.payload.file.native_path = arena.dupe(u8, abi.bytes(path)) catch return abi.OOM;
if (@import("builtin").os.tag == .windows) {
if (path.len % 2 != 0) return abi.INVALID;
const wide = arena.alloc(u16, @intCast(path.len / 2)) catch return abi.OOM;
@memcpy(std.mem.sliceAsBytes(wide), abi.bytes(path));
object.payload.file.path = std.unicode.wtf16LeToWtf8AllocZ(arena, wide) catch return abi.OOM;
} else {
object.payload.file.path = arena.dupeZ(u8, abi.bytes(path)) catch return abi.OOM;
}
object.payload.file.uri = arena.dupe(u8, abi.textBytes(uri)) catch return abi.OOM;
object.payload.file.display = arena.dupe(u8, abi.textBytes(display)) catch return abi.OOM;
object.native = grant;
@ -87,7 +95,7 @@ export fn z_file_from_url(
export fn z_file_path(handle: ?*c.uhdk_file) c.uhdk_bytes {
const object = runtime.fromObjectHandle(handle);
return .{ .ptr = object.payload.file.path.ptr, .len = object.payload.file.path.len };
return .{ .ptr = object.payload.file.native_path.ptr, .len = object.payload.file.native_path.len };
}
export fn z_menu_items(handle: ?*c.uhdk_menu, len: [*c]u64) [*c]const c.uhdk_menu_item {

View File

@ -10,7 +10,7 @@ comptime {
checkExports(@import("api/files.zig"));
checkExports(@import("api/menus.zig"));
checkExports(@import("api/desktop.zig"));
_ = @import("macos/callbacks.zig");
_ = @import("callbacks.zig");
}
test {

View File

@ -294,6 +294,7 @@ pub fn applicationMain(application: *Application) void {
application.worker_id = std.Thread.getCurrentId();
application.unlock();
application.entry.?(toAppHandle(application), application.user);
if (@import("builtin").os.tag == .windows) _ = c.z_native_gl(null, 4, null);
application.lock();
application.stopping = true;
application.worker_done = true;
@ -364,7 +365,11 @@ pub fn applicationError(err: anyerror) abi.Status {
pub fn destroyObject(object: *Resource) void {
if (!object.alive) return;
std.debug.assert(checkThread(object.app) == abi.OK);
if (object.payload == .gl) std.debug.assert(!object.payload.gl.bracket);
if (object.payload == .gl) {
std.debug.assert(!object.payload.gl.bracket);
if (@import("builtin").os.tag == .windows and object.native != null)
_ = c.z_native_gl(object.native, 8, null);
}
if (object.payload == .window) if (object.payload.window.context) |ctx| {
destroyObject(ctx);
object.payload.window.context = null;

1861
src/windows/native.c Normal file

File diff suppressed because it is too large Load Diff