windows implementation
This commit is contained in:
parent
0c8270fbef
commit
d1c4f14877
22
README.md
22
README.md
|
|
@ -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.
|
||||
|
|
|
|||
37
build.zig
37
build.zig
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = "",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue