bunite-core 0.6.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/package.json +5 -2
  2. package/src/bun/core/App.ts +35 -34
  3. package/src/bun/core/BrowserView.ts +3 -9
  4. package/src/bun/core/singleInstanceLock.ts +91 -0
  5. package/src/bun/index.ts +5 -6
  6. package/src/bun/preload/inline.ts +1 -2
  7. package/src/bun/proc/native.ts +54 -78
  8. package/src/native/linux/bunite_linux_appres.cpp +173 -0
  9. package/src/native/linux/bunite_linux_ffi.cpp +263 -0
  10. package/src/native/linux/bunite_linux_internal.h +148 -0
  11. package/src/native/linux/bunite_linux_preload.cpp +1 -0
  12. package/src/native/linux/bunite_linux_runtime.cpp +120 -0
  13. package/src/native/linux/bunite_linux_utils.cpp +114 -0
  14. package/src/native/linux/bunite_linux_view.cpp +244 -0
  15. package/src/native/linux/bunite_linux_window.cpp +101 -0
  16. package/src/native/mac/bunite_mac_appres.mm +163 -0
  17. package/src/native/mac/bunite_mac_ffi.mm +470 -0
  18. package/src/native/mac/bunite_mac_internal.h +151 -0
  19. package/src/native/mac/bunite_mac_runtime.mm +15 -0
  20. package/src/native/mac/bunite_mac_utils.mm +121 -0
  21. package/src/native/mac/bunite_mac_view.mm +279 -0
  22. package/src/native/mac/bunite_mac_window.mm +187 -0
  23. package/src/native/shared/ffi_exports.h +13 -13
  24. package/src/native/shared/permissions.h +14 -0
  25. package/src/native/shared/webview_storage.h +6 -3
  26. package/src/native/win/native_host_cef.cpp +4 -8
  27. package/src/native/win/native_host_ffi.cpp +76 -123
  28. package/src/native/win/native_host_internal.h +5 -3
  29. package/src/native/win/native_host_runtime.cpp +2 -6
  30. package/src/native/win/native_host_utils.cpp +23 -52
  31. package/src/native/win/process_helper_win.cpp +1 -3
  32. package/src/preload/runtime.ts +1 -3
  33. package/src/preload/tsconfig.tsbuildinfo +1 -1
  34. package/src/preload/webviewElement.ts +3 -8
  35. package/src/shared/paths.ts +35 -44
  36. package/src/shared/platform.ts +1 -2
  37. package/src/shared/rpcDemux.ts +47 -2
  38. package/src/shared/webviewPolyfill.ts +64 -6
  39. package/src/bun/core/Utils.ts +0 -301
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "bunite-core",
3
3
  "description": "Uniting UI and Bun",
4
- "version": "0.6.0",
4
+ "version": "0.8.1",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "setup:cef": "bun ../tools/bunite-dev/scripts/setup-cef.ts",
8
8
  "build:native:win": "cmake -S . -B build/win -DBUNITE_TARGET_ARCH=x64 && cmake --build build/win --config Release",
9
+ "build:native:linux": "cmake -S . -B build/linux -DCMAKE_BUILD_TYPE=Release && cmake --build build/linux",
9
10
  "build:preload": "bun build src/preload/runtime.ts --outfile src/preload/runtime.built.js --target browser --minify"
10
11
  },
11
12
  "exports": {
@@ -22,6 +23,8 @@
22
23
  "msgpackr": "^1.11.9"
23
24
  },
24
25
  "optionalDependencies": {
25
- "bunite-native-win-x64": "0.0.3"
26
+ "bunite-native-win-x64": "0.0.4",
27
+ "bunite-native-mac-arm64": "0.0.1",
28
+ "bunite-native-linux-x64": "0.0.1"
26
29
  }
27
30
  }
@@ -1,11 +1,11 @@
1
1
  import { isAbsolute, join, resolve } from "node:path";
2
2
  import { existsSync } from "node:fs";
3
3
  import { getBaseDir } from "../../shared/paths";
4
- import { dlopen, FFIType } from "bun:ffi";
5
4
  import { BuniteEvent } from "../events/event";
6
5
  import { buniteEventEmitter } from "../events/eventEmitter";
7
- import { handleMessageBoxResponse } from "./Utils";
8
6
  import {
7
+ getNativeEngineName,
8
+ getNativeEngineVersion,
9
9
  getNativeLibrary,
10
10
  initNativeRuntime,
11
11
  getNativeRuntimeState,
@@ -24,7 +24,6 @@ import type { LogLevel } from "../../shared/log";
24
24
 
25
25
  type AppOptions = NativeBootstrapOptions & {
26
26
  userDataDir?: string;
27
- cefDir?: string;
28
27
  exitOnLastWindowClosed?: boolean;
29
28
  logLevel?: LogLevel;
30
29
  };
@@ -40,6 +39,7 @@ export class AppRuntime {
40
39
  private readonly globalIPCHandlers = new Map<string, GlobalIPCHandler>();
41
40
  private exitOnLastWindowClosed = true;
42
41
  private quitting = false;
42
+ private pumpActive = false;
43
43
 
44
44
  readonly ready: Promise<void>;
45
45
 
@@ -56,10 +56,6 @@ export class AppRuntime {
56
56
  log.setLevel(options.logLevel);
57
57
  }
58
58
 
59
- if (options.cefDir) {
60
- process.env.BUNITE_CEF_DIR = options.cefDir;
61
- }
62
-
63
59
  if (options.userDataDir) {
64
60
  process.env.BUNITE_USER_DATA_DIR = options.userDataDir;
65
61
  } else if (!process.env.BUNITE_USER_DATA_DIR) {
@@ -90,7 +86,7 @@ export class AppRuntime {
90
86
  allowStub: options.allowStub,
91
87
  hideConsole: options.hideConsole,
92
88
  popupBlocking: options.popupBlocking,
93
- chromiumFlags: options.chromiumFlags
89
+ engineFlags: options.engineFlags
94
90
  });
95
91
 
96
92
  if (options.logLevel && runtime.nativeLoaded) {
@@ -106,12 +102,6 @@ export class AppRuntime {
106
102
  this.globalIPCHandlers.set(channel, handler);
107
103
  }
108
104
 
109
- this.globalIPCHandlers.set("__bunite:messageBoxResponse", (params) => {
110
- const { requestId, response } = params as { requestId: number; response: number };
111
- handleMessageBoxResponse(requestId, response);
112
- return {};
113
- });
114
-
115
105
  setRouteRequestHandler((requestId, path) => this.handleRouteRequest(requestId, path));
116
106
 
117
107
  for (const path of this.appresHandlers.keys()) {
@@ -155,16 +145,28 @@ export class AppRuntime {
155
145
 
156
146
  run() {
157
147
  const runtime = getNativeRuntimeState();
158
- if (runtime?.nativeLoaded) {
159
- getNativeLibrary()?.symbols.bunite_run_loop();
148
+ if (!runtime?.nativeLoaded) {
160
149
  if (!this.stubKeepAliveTimer) {
150
+ log.warn("Running without a native event loop. Keeping the process alive in stub mode.");
161
151
  this.stubKeepAliveTimer = setInterval(() => {}, 60_000);
162
152
  }
163
153
  return;
164
154
  }
165
155
 
166
- if (!this.stubKeepAliveTimer) {
167
- log.warn("Running without a native event loop. Keeping the process alive in stub mode.");
156
+ const lib = getNativeLibrary();
157
+ lib?.symbols.bunite_run_loop();
158
+
159
+ if (process.platform === "darwin" || process.platform === "linux") {
160
+ // AppKit / WebKitGTK share Bun's main thread — step-drive via setImmediate (no blocking loop).
161
+ this.pumpActive = true;
162
+ const pump = () => {
163
+ if (!this.pumpActive) return;
164
+ lib?.symbols.bunite_pump_once();
165
+ setImmediate(pump);
166
+ };
167
+ pump();
168
+ } else if (!this.stubKeepAliveTimer) {
169
+ // Engines with a dedicated UI thread (Windows CEF) only need Bun's loop kept alive.
168
170
  this.stubKeepAliveTimer = setInterval(() => {}, 60_000);
169
171
  }
170
172
  }
@@ -181,6 +183,7 @@ export class AppRuntime {
181
183
  this.quitting = false;
182
184
  return;
183
185
  }
186
+ this.pumpActive = false;
184
187
  if (this.stubKeepAliveTimer) {
185
188
  clearInterval(this.stubKeepAliveTimer);
186
189
  this.stubKeepAliveTimer = null;
@@ -256,22 +259,20 @@ export class AppRuntime {
256
259
  }
257
260
  }
258
261
 
259
- private cachedCefVersion: string | null | undefined;
262
+ private cachedEngineName: string | null | undefined;
263
+ private cachedEngineVersion: string | null | undefined;
260
264
 
261
- get cefVersion(): string | null {
262
- if (this.cachedCefVersion !== undefined) return this.cachedCefVersion;
263
- this.cachedCefVersion = null;
264
- const arts = getNativeRuntimeState()?.artifacts;
265
- if (!arts?.cefDir) return null;
266
- const libcefPath = join(arts.cefDir, "libcef.dll");
267
- if (!existsSync(libcefPath)) return null;
268
- try {
269
- const lib = dlopen(libcefPath, {
270
- cef_version_info: { returns: FFIType.i32, args: [FFIType.i32] },
271
- });
272
- const v = (entry: number) => lib.symbols.cef_version_info(entry);
273
- this.cachedCefVersion = `${v(0)}.${v(1)}.${v(2)}+chromium-${v(4)}.${v(5)}.${v(6)}.${v(7)}`;
274
- } catch { /* leave as null */ }
275
- return this.cachedCefVersion;
265
+ /** Active engine identifier reported by the native adapter (e.g. `"cef"`, `"wkwebview"`, `"webkitgtk"`). */
266
+ get engineName(): string | null {
267
+ if (this.cachedEngineName !== undefined) return this.cachedEngineName;
268
+ this.cachedEngineName = getNativeEngineName();
269
+ return this.cachedEngineName;
270
+ }
271
+
272
+ /** Engine version string reported by the native adapter. Format depends on engine. */
273
+ get engineVersion(): string | null {
274
+ if (this.cachedEngineVersion !== undefined) return this.cachedEngineVersion;
275
+ this.cachedEngineVersion = getNativeEngineVersion();
276
+ return this.cachedEngineVersion;
276
277
  }
277
278
  }
@@ -8,7 +8,6 @@ import { attachBrowserViewRegistry, getRpcPort, sendMessageToView } from "./Sock
8
8
  import { randomBytes } from "node:crypto";
9
9
  import { resolveDefaultAppResRoot } from "../../shared/paths";
10
10
  import { removeSurfacesForHostView } from "./SurfaceRegistry";
11
- import { cancelPendingMessageBoxesForView } from "./Utils";
12
11
 
13
12
  const BrowserViewMap: Record<number, BrowserView<any>> = {};
14
13
  let nextWebviewId = 1;
@@ -119,8 +118,7 @@ export class BrowserView<T extends RpcWithTransport = RpcWithTransport> {
119
118
 
120
119
  BrowserViewMap[this.id] = this;
121
120
  this.rpc?.setTransport(this.transport);
122
- // Register ready waiter BEFORE native create — OnAfterCreated can fire
123
- // on the CEF UI thread before bunite_view_create returns to JS.
121
+ // Register before native create — view-ready can fire on the UI thread before bunite_view_create returns.
124
122
  this._readyPromise = waitForViewReady(this.id);
125
123
  this.nativeAttached =
126
124
  getNativeLibrary()?.symbols.bunite_view_create(
@@ -141,13 +139,10 @@ export class BrowserView<T extends RpcWithTransport = RpcWithTransport> {
141
139
  ) ?? false;
142
140
 
143
141
  if (this.nativeAttached) {
144
- // Clean up owned surfaces when this view navigates (page refresh/navigation
145
- // destroys the JS context without firing disconnectedCallback).
146
- // Uses did-navigate (not will-navigate) because will-navigate fires even
147
- // when navigation is denied by navigationRules.
142
+ // did-navigate (not will-): nav destroys JS context without disconnectedCallback;
143
+ // will-navigate fires even when rules deny → would leak surfaces.
148
144
  this.on("did-navigate", (event: any) => {
149
145
  this.url = event.data?.detail ?? this.url;
150
- cancelPendingMessageBoxesForView(this.id);
151
146
  removeSurfacesForHostView(this.id);
152
147
  });
153
148
  } else {
@@ -298,7 +293,6 @@ export class BrowserView<T extends RpcWithTransport = RpcWithTransport> {
298
293
  }
299
294
 
300
295
  detachFromNative() {
301
- cancelPendingMessageBoxesForView(this.id);
302
296
  removeSurfacesForHostView(this.id);
303
297
  cancelWaitForViewReady(this.id);
304
298
  this.nativeAttached = false;
@@ -0,0 +1,91 @@
1
+ import { createHash } from "node:crypto";
2
+ import { closeSync, openSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ export type SingleInstanceLock =
7
+ | { acquired: true; release(): void }
8
+ | { acquired: false; holderPid?: number };
9
+
10
+ export function lockPathFor(key: string): string {
11
+ const slug = createHash("sha1").update(key).digest("hex").slice(0, 16);
12
+ return join(tmpdir(), `bunite-instance-${slug}.lock`);
13
+ }
14
+
15
+ function isProcessAlive(pid: number): boolean {
16
+ if (!Number.isFinite(pid) || pid <= 0) return false;
17
+ try {
18
+ process.kill(pid, 0);
19
+ return true;
20
+ } catch (e) {
21
+ const code = (e as NodeJS.ErrnoException).code;
22
+ // EPERM means the process exists but is owned by someone else.
23
+ return code === "EPERM";
24
+ }
25
+ }
26
+
27
+ function readHolderPid(path: string): number | undefined {
28
+ try {
29
+ const pid = parseInt(readFileSync(path, "utf8").trim(), 10);
30
+ return Number.isFinite(pid) && pid > 0 ? pid : undefined;
31
+ } catch {
32
+ return undefined;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Acquire a process-wide single-instance lock keyed by `key`. Returns a release
38
+ * handle on success, or the current holder's PID on contention. Dead-PID locks
39
+ * are reclaimed automatically; in-flight contention (lockfile present but PID
40
+ * not yet written / unreadable) is surfaced as `acquired: false` without a PID
41
+ * rather than racing to unlink the contender's lockfile.
42
+ *
43
+ * Use BEFORE app.init() — second instance can prompt UX and exit instead of
44
+ * crashing on engine-specific userDataDir locks.
45
+ *
46
+ * Caveats: `process.on("exit")` cleanup does not run on SIGKILL or crash; the
47
+ * next instance reclaims via PID-liveness probe. PID reuse can mask a dead
48
+ * original holder until the unrelated process exits.
49
+ */
50
+ export function acquireSingleInstanceLock(key: string): SingleInstanceLock {
51
+ const path = lockPathFor(key);
52
+
53
+ for (let attempt = 0; attempt < 2; attempt++) {
54
+ try {
55
+ const fd = openSync(path, "wx");
56
+ writeFileSync(fd, String(process.pid));
57
+ closeSync(fd);
58
+ let released = false;
59
+ const release = () => {
60
+ if (released) return;
61
+ released = true;
62
+ // Verify ownership before unlinking — guards against deleting a lockfile
63
+ // that has been reclaimed by another process if our exit was delayed.
64
+ if (readHolderPid(path) === process.pid) {
65
+ try { unlinkSync(path); } catch { /* already gone */ }
66
+ }
67
+ process.off("exit", release);
68
+ };
69
+ process.on("exit", release);
70
+ return { acquired: true, release };
71
+ } catch (e) {
72
+ const code = (e as NodeJS.ErrnoException).code;
73
+ if (code !== "EEXIST") throw e;
74
+
75
+ const holderPid = readHolderPid(path);
76
+ if (holderPid === undefined) {
77
+ // Lockfile present but PID not written yet (contender between
78
+ // openSync and writeFileSync) or content corrupt. Don't race to unlink.
79
+ return { acquired: false };
80
+ }
81
+ if (isProcessAlive(holderPid)) {
82
+ return { acquired: false, holderPid };
83
+ }
84
+ // Stale: holder is dead. Drop and retry once.
85
+ try { unlinkSync(path); } catch { /* lost the race; loop retries */ }
86
+ }
87
+ }
88
+
89
+ // Second attempt also lost the race — surface as contention without a PID.
90
+ return { acquired: false };
91
+ }
package/src/bun/index.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import { AppRuntime } from "./core/App";
2
2
  import { BrowserWindow, type WindowOptionsType } from "./core/BrowserWindow";
3
3
  import { BrowserView, type BrowserViewOptions } from "./core/BrowserView";
4
- import * as Utils from "./core/Utils";
5
4
  import { buniteEventEmitter } from "./events/eventEmitter";
6
5
  import { BuniteEvent } from "./events/event";
7
6
  import { completePermissionRequest } from "./proc/native";
@@ -13,17 +12,17 @@ import {
13
12
  type RpcSchema,
14
13
  type RpcWithTransport
15
14
  } from "../shared/rpc";
16
- import { createRpcTransportDemuxer, type RpcChannelHandle, type RpcTransportDemuxer, type RpcTransportDemuxerOptions } from "../shared/rpcDemux";
15
+ import { createRpcTransportDemuxer, type RpcChannelHandle, type RpcDemuxBufferPolicy, type RpcTransportDemuxer, type RpcTransportDemuxerOptions } from "../shared/rpcDemux";
17
16
  import { createWebSocketTransport, type WebSocketLike, type WebSocketTransportPipe } from "../shared/webSocketTransport";
18
17
  import { createWebRpcHandler, type WebRpcClient } from "../shared/webRpcHandler";
19
- import type { MessageBoxOptions, MessageBoxResponse } from "./core/Utils";
18
+ import { acquireSingleInstanceLock, type SingleInstanceLock } from "./core/singleInstanceLock";
20
19
  import { log, type LogLevel } from "../shared/log";
21
20
 
22
21
  export {
22
+ acquireSingleInstanceLock,
23
23
  AppRuntime,
24
24
  BrowserWindow,
25
25
  BrowserView,
26
- Utils,
27
26
  buniteEventEmitter,
28
27
  completePermissionRequest,
29
28
  createRpc,
@@ -41,12 +40,12 @@ export type {
41
40
  BuniteRpcSchema,
42
41
  BrowserViewOptions,
43
42
  RpcChannelHandle,
44
- MessageBoxOptions,
45
- MessageBoxResponse,
43
+ RpcDemuxBufferPolicy,
46
44
  RpcSchema,
47
45
  RpcWithTransport,
48
46
  RpcTransportDemuxer,
49
47
  RpcTransportDemuxerOptions,
48
+ SingleInstanceLock,
50
49
  WebRpcClient,
51
50
  WebSocketLike,
52
51
  WebSocketTransportPipe,
@@ -53,8 +53,7 @@ function readCustomPreload(preload: string | null, appresRoot: string | null) {
53
53
  }
54
54
  }
55
55
 
56
- // Pre-built preload runtime (built via `bun run build:preload` in package/).
57
- // Embedded at bundle time so bun --compile includes it without filesystem access.
56
+ // Bundled at build time so bun --compile works without filesystem access.
58
57
  // @ts-ignore — text import attribute
59
58
  import embeddedPreloadRuntime from "../../preload/runtime.built.js" with { type: "text" };
60
59
 
@@ -9,7 +9,12 @@ export type NativeBootstrapOptions = {
9
9
  allowStub?: boolean;
10
10
  hideConsole?: boolean;
11
11
  popupBlocking?: boolean;
12
- chromiumFlags?: Record<string, string | boolean>;
12
+ /**
13
+ * Engine-specific opaque config. Each adapter parses its own keys.
14
+ * - CEF (Windows): Chromium command-line flags as `Record<flag, value | true>`.
15
+ * - WKWebView, WebKitGTK: defined per adapter; refer to the adapter's bootstrap docs.
16
+ */
17
+ engineFlags?: Record<string, string | boolean>;
13
18
  };
14
19
 
15
20
  export type NativeRuntimeState = {
@@ -23,15 +28,17 @@ type CStringPointer = Pointer;
23
28
 
24
29
  type NativeSymbols = {
25
30
  bunite_abi_version: () => number;
31
+ bunite_engine_name: () => CString;
32
+ bunite_engine_version: () => CString;
26
33
  bunite_set_log_level: (level: number) => void;
27
34
  bunite_init: (
28
- processHelperPath: CStringPointer,
29
- cefDir: CStringPointer,
35
+ engineDir: CStringPointer,
30
36
  hideConsole: boolean,
31
37
  popupBlocking: boolean,
32
- chromiumFlagsJson: CStringPointer
38
+ engineConfigJson: CStringPointer
33
39
  ) => boolean;
34
40
  bunite_run_loop: () => void;
41
+ bunite_pump_once: () => void;
35
42
  bunite_quit: () => void;
36
43
  bunite_free_cstring: (value: Pointer) => void;
37
44
  bunite_window_create: (
@@ -101,16 +108,6 @@ type NativeSymbols = {
101
108
  bunite_view_close_devtools: (viewId: number) => void;
102
109
  bunite_view_toggle_devtools: (viewId: number) => void;
103
110
  bunite_complete_permission_request: (requestId: number, state: number) => void;
104
- bunite_show_message_box: (
105
- windowId: number,
106
- type: CStringPointer,
107
- title: CStringPointer,
108
- message: CStringPointer,
109
- detail: CStringPointer,
110
- buttons: CStringPointer,
111
- defaultId: number,
112
- cancelId: number
113
- ) => number;
114
111
  bunite_set_webview_event_handler: (handler: JSCallback) => void;
115
112
  bunite_set_window_event_handler: (handler: JSCallback) => void;
116
113
  };
@@ -119,26 +116,35 @@ type LoadedNativeLibrary = {
119
116
  symbols: NativeSymbols;
120
117
  };
121
118
 
122
- const messageBoxButtonSeparator = "\x1f";
123
- const unsetCancelId = -1;
124
-
125
119
  const nativeSymbolDefinitions = {
126
120
  bunite_abi_version: {
127
121
  args: [],
128
122
  returns: FFIType.i32
129
123
  },
124
+ bunite_engine_name: {
125
+ args: [],
126
+ returns: FFIType.cstring
127
+ },
128
+ bunite_engine_version: {
129
+ args: [],
130
+ returns: FFIType.cstring
131
+ },
130
132
  bunite_set_log_level: {
131
133
  args: [FFIType.i32],
132
134
  returns: FFIType.void
133
135
  },
134
136
  bunite_init: {
135
- args: [FFIType.cstring, FFIType.cstring, FFIType.bool, FFIType.bool, FFIType.cstring],
137
+ args: [FFIType.cstring, FFIType.bool, FFIType.bool, FFIType.cstring],
136
138
  returns: FFIType.bool
137
139
  },
138
140
  bunite_run_loop: {
139
141
  args: [],
140
142
  returns: FFIType.void
141
143
  },
144
+ bunite_pump_once: {
145
+ args: [],
146
+ returns: FFIType.void
147
+ },
142
148
  bunite_quit: {
143
149
  args: [],
144
150
  returns: FFIType.void
@@ -310,19 +316,6 @@ const nativeSymbolDefinitions = {
310
316
  args: [FFIType.u32, FFIType.u32],
311
317
  returns: FFIType.void
312
318
  },
313
- bunite_show_message_box: {
314
- args: [
315
- FFIType.u32,
316
- FFIType.cstring,
317
- FFIType.cstring,
318
- FFIType.cstring,
319
- FFIType.cstring,
320
- FFIType.cstring,
321
- FFIType.i32,
322
- FFIType.i32
323
- ],
324
- returns: FFIType.i32
325
- },
326
319
  bunite_set_webview_event_handler: {
327
320
  args: [FFIType.function],
328
321
  returns: FFIType.void
@@ -361,9 +354,7 @@ export function toCString(value: string): CStringPointer {
361
354
  const normalized = value.endsWith("\0") ? value : `${value}\0`;
362
355
  const buffer = Buffer.from(normalized, "utf8");
363
356
 
364
- // Keep recent CString buffers alive long enough for native code to copy them.
365
- // This is not a long-term ownership model for retained native pointers, but it
366
- // avoids immediate GC hazards across the current FFI call boundary.
357
+ // Keep recent CString buffers alive across the FFI call (not long-term retention).
367
358
  retainedCStringBuffers.push(buffer);
368
359
  if (retainedCStringBuffers.length > 1024) {
369
360
  retainedCStringBuffers.shift();
@@ -373,20 +364,21 @@ export function toCString(value: string): CStringPointer {
373
364
  }
374
365
 
375
366
  function applyEnvironment(artifacts: ResolvedNativeArtifacts) {
376
- const cefBinaryDir = artifacts.cefDir && existsSync(join(artifacts.cefDir, "Release", "libcef.dll"))
377
- ? join(artifacts.cefDir, "Release")
378
- : artifacts.cefDir;
379
- const cefResourceDir = artifacts.cefDir && existsSync(join(artifacts.cefDir, "Resources", "resources.pak"))
380
- ? join(artifacts.cefDir, "Resources")
381
- : artifacts.cefDir;
382
-
383
- if (cefResourceDir && !process.env.ICU_DATA) {
384
- process.env.ICU_DATA = cefResourceDir;
367
+ // CEF needs engine dir on PATH (libcef.dll) and ICU_DATA pointing at resources. Null for mac/linux.
368
+ const engineBinaryDir = artifacts.engineDir && existsSync(join(artifacts.engineDir, "Release", "libcef.dll"))
369
+ ? join(artifacts.engineDir, "Release")
370
+ : artifacts.engineDir;
371
+ const engineResourceDir = artifacts.engineDir && existsSync(join(artifacts.engineDir, "Resources", "resources.pak"))
372
+ ? join(artifacts.engineDir, "Resources")
373
+ : artifacts.engineDir;
374
+
375
+ if (engineResourceDir && !process.env.ICU_DATA) {
376
+ process.env.ICU_DATA = engineResourceDir;
385
377
  }
386
- if (cefBinaryDir) {
378
+ if (engineBinaryDir) {
387
379
  const pathEntries = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
388
- if (!pathEntries.includes(cefBinaryDir)) {
389
- process.env.PATH = [cefBinaryDir, ...pathEntries].join(delimiter);
380
+ if (!pathEntries.includes(engineBinaryDir)) {
381
+ process.env.PATH = [engineBinaryDir, ...pathEntries].join(delimiter);
390
382
  }
391
383
  }
392
384
  }
@@ -594,10 +586,7 @@ export async function initNativeRuntime(
594
586
  const allowStub = options.allowStub ?? true;
595
587
  const artifacts = resolveNativeArtifacts();
596
588
  const hasNativeArtifacts = Boolean(
597
- artifacts.nativeLibPath &&
598
- artifacts.processHelperPath &&
599
- existsSync(artifacts.nativeLibPath) &&
600
- existsSync(artifacts.processHelperPath)
589
+ artifacts.nativeLibPath && existsSync(artifacts.nativeLibPath)
601
590
  );
602
591
 
603
592
  applyEnvironment(artifacts);
@@ -611,7 +600,7 @@ export async function initNativeRuntime(
611
600
  nativeLibrary = hasNativeArtifacts ? tryLoadNativeLibrary(artifacts) : null;
612
601
 
613
602
  if (nativeLibrary) {
614
- const EXPECTED_ABI = 2;
603
+ const EXPECTED_ABI = 4;
615
604
  const nativeAbi = nativeLibrary.symbols.bunite_abi_version();
616
605
  if (nativeAbi !== EXPECTED_ABI) {
617
606
  throw new Error(
@@ -620,15 +609,14 @@ export async function initNativeRuntime(
620
609
  );
621
610
  }
622
611
  registerNativeCallbacks(nativeLibrary);
623
- const chromiumFlagsJson = options.chromiumFlags
624
- ? JSON.stringify(options.chromiumFlags)
612
+ const engineConfigJson = options.engineFlags
613
+ ? JSON.stringify(options.engineFlags)
625
614
  : "";
626
615
  const initOk = nativeLibrary.symbols.bunite_init(
627
- toCString(artifacts.processHelperPath ?? ""),
628
- toCString(artifacts.cefDir ?? ""),
616
+ toCString(artifacts.engineDir ?? ""),
629
617
  options.hideConsole ?? false,
630
618
  options.popupBlocking ?? false,
631
- toCString(chromiumFlagsJson)
619
+ toCString(engineConfigJson)
632
620
  );
633
621
 
634
622
  if (!initOk) {
@@ -675,28 +663,16 @@ export function completePermissionRequest(requestId: number, stateValue: number)
675
663
  nativeLibrary?.symbols.bunite_complete_permission_request(requestId, stateValue);
676
664
  }
677
665
 
678
- export function showNativeMessageBox(windowId: number, params: {
679
- type?: string;
680
- title?: string;
681
- message?: string;
682
- detail?: string;
683
- buttons?: string[];
684
- defaultId?: number;
685
- cancelId?: number;
686
- }): number {
666
+ export function getNativeEngineName(): string | null {
687
667
  const native = getNativeLibrary();
688
- if (!native) {
689
- return params.cancelId ?? params.defaultId ?? 0;
690
- }
668
+ if (!native) return null;
669
+ const cstr = native.symbols.bunite_engine_name();
670
+ return cstr.toString();
671
+ }
691
672
 
692
- return native.symbols.bunite_show_message_box(
693
- windowId,
694
- toCString(params.type ?? "info"),
695
- toCString(params.title ?? ""),
696
- toCString(params.message ?? ""),
697
- toCString(params.detail ?? ""),
698
- toCString((params.buttons ?? ["OK"]).join(messageBoxButtonSeparator)),
699
- params.defaultId ?? 0,
700
- params.cancelId ?? unsetCancelId
701
- );
673
+ export function getNativeEngineVersion(): string | null {
674
+ const native = getNativeLibrary();
675
+ if (!native) return null;
676
+ const cstr = native.symbols.bunite_engine_version();
677
+ return cstr.toString();
702
678
  }