@crossbind/core-app-host 0.0.0-bootstrap.0 → 2.0.0-beta.65

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 (124) hide show
  1. package/README.md +55 -3
  2. package/android/app/src/main/AndroidManifest.xml +15 -0
  3. package/android/app/src/main/java/dev/crossbind/app/CrossbindActivity.java +366 -0
  4. package/android/app/src/main/res/xml/network_security_config.xml +8 -0
  5. package/android/build.gradle +14 -0
  6. package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  7. package/android/gradle/wrapper/gradle-wrapper.properties +10 -0
  8. package/android/gradle.properties +2 -0
  9. package/android/gradlew +248 -0
  10. package/android/gradlew.bat +112 -0
  11. package/android/settings.gradle +14 -0
  12. package/cpp/CMakeLists.txt +110 -0
  13. package/cpp/src/android/jni.cpp +167 -0
  14. package/cpp/src/app.cpp +40 -0
  15. package/cpp/src/app.h +21 -0
  16. package/cpp/src/attach.cpp +40 -0
  17. package/cpp/src/darwin/bundle.mm +19 -0
  18. package/cpp/src/darwin/channels.mm +196 -0
  19. package/cpp/src/darwin/navigation.mm +69 -0
  20. package/cpp/src/host_core.cpp +335 -0
  21. package/cpp/src/host_core.h +84 -0
  22. package/cpp/src/ios/bundle.mm +15 -0
  23. package/cpp/src/ios/main.mm +71 -0
  24. package/cpp/src/linux/app.cpp +29 -0
  25. package/cpp/src/linux/channels.cpp +143 -0
  26. package/cpp/src/linux/main_thread.h +16 -0
  27. package/cpp/src/linux/navigation.cpp +55 -0
  28. package/cpp/src/main.cpp +57 -0
  29. package/cpp/src/platform.h +57 -0
  30. package/cpp/src/windows/app.cpp +36 -0
  31. package/cpp/src/windows/app.manifest +8 -0
  32. package/cpp/src/windows/app.rc +3 -0
  33. package/cpp/src/windows/channels.cpp +336 -0
  34. package/cpp/src/windows/com.cpp +92 -0
  35. package/cpp/src/windows/com.h +57 -0
  36. package/cpp/src/windows/navigation.cpp +90 -0
  37. package/js/codec.js +123 -0
  38. package/js/host.js +8 -0
  39. package/js/lean-client.js +147 -0
  40. package/js/lean-host.js +237 -0
  41. package/js/page.js +79 -0
  42. package/js/polyfills.js +32 -0
  43. package/package.json +30 -6
  44. package/third_party/jsi/LICENSE +21 -0
  45. package/third_party/jsi/README.md +18 -0
  46. package/third_party/jsi/include/jsi/hermes-interfaces.h +189 -0
  47. package/third_party/jsi/include/jsi/instrumentation.h +132 -0
  48. package/third_party/jsi/include/jsi/jsi-inl.h +434 -0
  49. package/third_party/jsi/include/jsi/jsi.h +2266 -0
  50. package/third_party/jsi/jsi.cpp +1110 -0
  51. package/third_party/webview/LICENSE +22 -0
  52. package/third_party/webview/README.md +24 -0
  53. package/third_party/webview/core/include/webview/api.h +251 -0
  54. package/third_party/webview/core/include/webview/backends.hh +40 -0
  55. package/third_party/webview/core/include/webview/c_api_impl.hh +258 -0
  56. package/third_party/webview/core/include/webview/detail/backends/cocoa_webkit.hh +633 -0
  57. package/third_party/webview/core/include/webview/detail/backends/gtk_webkitgtk.hh +355 -0
  58. package/third_party/webview/core/include/webview/detail/backends/win32_edge.hh +915 -0
  59. package/third_party/webview/core/include/webview/detail/basic_result.hh +119 -0
  60. package/third_party/webview/core/include/webview/detail/engine_base.hh +382 -0
  61. package/third_party/webview/core/include/webview/detail/exceptions.hh +42 -0
  62. package/third_party/webview/core/include/webview/detail/json.hh +337 -0
  63. package/third_party/webview/core/include/webview/detail/native_library.hh +176 -0
  64. package/third_party/webview/core/include/webview/detail/optional.hh +118 -0
  65. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/NSApplication.hh +107 -0
  66. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/NSBundle.hh +56 -0
  67. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/NSEvent.hh +71 -0
  68. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/NSInvocation.hh +66 -0
  69. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/NSMethodSignature.hh +52 -0
  70. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/NSNotification.hh +51 -0
  71. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/NSNumber.hh +53 -0
  72. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/NSObject.hh +51 -0
  73. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/NSOpenPanel.hh +71 -0
  74. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/NSPoint.hh +53 -0
  75. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/NSRect.hh +53 -0
  76. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/NSSavePanel.hh +52 -0
  77. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/NSSize.hh +53 -0
  78. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/NSString.hh +87 -0
  79. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/NSURL.hh +63 -0
  80. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/NSURLRequest.hh +52 -0
  81. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/NSValue.hh +56 -0
  82. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/NSView.hh +91 -0
  83. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/NSWindow.hh +137 -0
  84. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/cocoa.hh +59 -0
  85. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/types.hh +49 -0
  86. package/third_party/webview/core/include/webview/detail/platform/darwin/objc/Class.hh +55 -0
  87. package/third_party/webview/core/include/webview/detail/platform/darwin/objc/autoreleasepool.hh +70 -0
  88. package/third_party/webview/core/include/webview/detail/platform/darwin/objc/invoke.hh +75 -0
  89. package/third_party/webview/core/include/webview/detail/platform/darwin/objc/memory.hh +57 -0
  90. package/third_party/webview/core/include/webview/detail/platform/darwin/objc/objc.hh +47 -0
  91. package/third_party/webview/core/include/webview/detail/platform/darwin/webkit/WKOpenPanelParameters.hh +57 -0
  92. package/third_party/webview/core/include/webview/detail/platform/darwin/webkit/WKScriptMessage.hh +51 -0
  93. package/third_party/webview/core/include/webview/detail/platform/darwin/webkit/WKUserContentController.hh +61 -0
  94. package/third_party/webview/core/include/webview/detail/platform/darwin/webkit/WKUserScript.hh +71 -0
  95. package/third_party/webview/core/include/webview/detail/platform/darwin/webkit/WKWebView.hh +108 -0
  96. package/third_party/webview/core/include/webview/detail/platform/darwin/webkit/WKWebViewConfiguration.hh +60 -0
  97. package/third_party/webview/core/include/webview/detail/platform/darwin/webkit/webkit.hh +46 -0
  98. package/third_party/webview/core/include/webview/detail/platform/linux/gtk/compat.hh +137 -0
  99. package/third_party/webview/core/include/webview/detail/platform/linux/webkitgtk/compat.hh +142 -0
  100. package/third_party/webview/core/include/webview/detail/platform/linux/webkitgtk/dmabuf.hh +167 -0
  101. package/third_party/webview/core/include/webview/detail/platform/windows/com_init_wrapper.hh +126 -0
  102. package/third_party/webview/core/include/webview/detail/platform/windows/dpi.hh +161 -0
  103. package/third_party/webview/core/include/webview/detail/platform/windows/dwmapi.hh +67 -0
  104. package/third_party/webview/core/include/webview/detail/platform/windows/iid.hh +78 -0
  105. package/third_party/webview/core/include/webview/detail/platform/windows/ntdll.hh +58 -0
  106. package/third_party/webview/core/include/webview/detail/platform/windows/reg_key.hh +135 -0
  107. package/third_party/webview/core/include/webview/detail/platform/windows/shcore.hh +59 -0
  108. package/third_party/webview/core/include/webview/detail/platform/windows/theme.hh +76 -0
  109. package/third_party/webview/core/include/webview/detail/platform/windows/user32.hh +90 -0
  110. package/third_party/webview/core/include/webview/detail/platform/windows/version.hh +151 -0
  111. package/third_party/webview/core/include/webview/detail/platform/windows/webview2/loader.hh +391 -0
  112. package/third_party/webview/core/include/webview/detail/user_script.hh +76 -0
  113. package/third_party/webview/core/include/webview/detail/utility/string.hh +99 -0
  114. package/third_party/webview/core/include/webview/errors.h +67 -0
  115. package/third_party/webview/core/include/webview/errors.hh +85 -0
  116. package/third_party/webview/core/include/webview/json_deprecated.hh +61 -0
  117. package/third_party/webview/core/include/webview/macros.h +123 -0
  118. package/third_party/webview/core/include/webview/types.h +81 -0
  119. package/third_party/webview/core/include/webview/types.hh +48 -0
  120. package/third_party/webview/core/include/webview/version.h +67 -0
  121. package/third_party/webview/core/include/webview/webview.h +32 -0
  122. package/third_party/webview/core/include/webview.h +15 -0
  123. package/third_party/webview2/LICENSE +27 -0
  124. package/third_party/webview2/README.md +13 -0
@@ -0,0 +1,57 @@
1
+ #pragma once
2
+
3
+ // What the Windows host needs of COM without WRL's implementations, which llvm-mingw lacks: WebView2's events as
4
+ // functions, its UTF-16 strings, and the UI thread.
5
+ #include <windows.h>
6
+
7
+ #include "WebView2.h"
8
+
9
+ #include <atomic>
10
+ #include <functional>
11
+ #include <string>
12
+ #include <utility>
13
+
14
+ namespace crossbind::windows {
15
+
16
+ // One WebView2 event or completion as a function.
17
+ template <typename Interface, typename... Args>
18
+ class Handler final : public Interface {
19
+ public:
20
+ Handler(REFIID iid, std::function<HRESULT(Args...)> fn) : iid_(iid), fn_(std::move(fn)) {}
21
+
22
+ ULONG STDMETHODCALLTYPE AddRef() override { return ++refs_; }
23
+ ULONG STDMETHODCALLTYPE Release() override {
24
+ const ULONG left = --refs_;
25
+ if (left == 0) delete this;
26
+ return left;
27
+ }
28
+ HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **out) override {
29
+ if (!out) return E_POINTER;
30
+ if (!IsEqualIID(riid, IID_IUnknown) && !IsEqualIID(riid, iid_)) {
31
+ *out = nullptr;
32
+ return E_NOINTERFACE;
33
+ }
34
+ *out = static_cast<Interface *>(this);
35
+ AddRef();
36
+ return S_OK;
37
+ }
38
+ HRESULT STDMETHODCALLTYPE Invoke(Args... args) override { return fn_(args...); }
39
+
40
+ private:
41
+ std::atomic<ULONG> refs_{1};
42
+ IID iid_;
43
+ std::function<HRESULT(Args...)> fn_;
44
+ };
45
+
46
+ std::wstring wide(const std::string &text);
47
+ std::string utf8(const wchar_t *text);
48
+ // A string WebView2 hands out, which the caller owns.
49
+ std::string take(LPWSTR text);
50
+ // A copy WebView2 takes ownership of.
51
+ LPWSTR give(const std::wstring &text);
52
+ // The webview of the controller main() gets from webview.
53
+ ICoreWebView2 *coreWebView(void *controller);
54
+ // Runs fn on the UI thread: at once when the caller is on it, else when the thread's message loop gets to it.
55
+ void onMainThread(std::function<void()> fn);
56
+
57
+ } // namespace crossbind::windows
@@ -0,0 +1,90 @@
1
+ #include "platform.h"
2
+ #include "windows/com.h"
3
+
4
+ #include <shellapi.h>
5
+
6
+ #include <algorithm>
7
+
8
+ using namespace crossbind::windows;
9
+
10
+ // A page from another origin in any frame would reach the module's channel and its crossbind:// routes, so the webview
11
+ // never loads one. A link the user follows to such a page opens in the default browser instead, and no page opens a
12
+ // window of the app's own.
13
+ namespace {
14
+
15
+ struct Lock {
16
+ std::vector<std::string> origins;
17
+ std::function<void()> newPage;
18
+ };
19
+
20
+ std::string originOf(const std::string &uri) {
21
+ const auto scheme = uri.find("://");
22
+ if (scheme == std::string::npos) return uri;
23
+ return uri.substr(0, uri.find_first_of("/?#", scheme + 3));
24
+ }
25
+
26
+ bool allows(const Lock &lock, const std::string &uri) {
27
+ if (uri.rfind("about:", 0) == 0) return true;
28
+ // A blob URL carries the origin of the page that made it.
29
+ if (uri.rfind("blob:", 0) == 0) return allows(lock, uri.substr(5));
30
+ return std::find(lock.origins.begin(), lock.origins.end(), originOf(uri)) != lock.origins.end();
31
+ }
32
+
33
+ void openOutside(const std::string &uri) {
34
+ const std::string scheme = uri.substr(0, uri.find(':'));
35
+ if (scheme != "http" && scheme != "https" && scheme != "mailto") return;
36
+ // The URL goes into the command line of its scheme's handler, where a quote or a space would split it.
37
+ if (std::any_of(uri.begin(), uri.end(), [](unsigned char c) { return c <= ' ' || c == '"' || c == 0x7f; })) return;
38
+ ShellExecuteW(nullptr, L"open", wide(uri).c_str(), nullptr, nullptr, SW_SHOWNORMAL);
39
+ }
40
+
41
+ using Starting = Handler<ICoreWebView2NavigationStartingEventHandler, ICoreWebView2 *, ICoreWebView2NavigationStartingEventArgs *>;
42
+
43
+ Starting *decide(Lock *lock) {
44
+ return new Starting(IID_ICoreWebView2NavigationStartingEventHandler,
45
+ [lock](ICoreWebView2 *, ICoreWebView2NavigationStartingEventArgs *args) -> HRESULT {
46
+ LPWSTR requested = nullptr;
47
+ args->get_Uri(&requested);
48
+ const std::string uri = take(requested);
49
+ if (allows(*lock, uri)) return S_OK;
50
+ BOOL followed = FALSE;
51
+ args->get_IsUserInitiated(&followed);
52
+ if (followed) openOutside(uri);
53
+ args->put_Cancel(TRUE);
54
+ return S_OK;
55
+ });
56
+ }
57
+
58
+ } // namespace
59
+
60
+ void restrictNavigation(void *webView, std::vector<std::string> origins, std::function<void()> newPage) {
61
+ ICoreWebView2 *core = coreWebView(webView);
62
+ auto *lock = new Lock{std::move(origins), std::move(newPage)};
63
+ EventRegistrationToken token;
64
+ Starting *top = decide(lock);
65
+ core->add_NavigationStarting(top, &token);
66
+ top->Release();
67
+ Starting *frames = decide(lock);
68
+ core->add_FrameNavigationStarting(frames, &token);
69
+ frames->Release();
70
+ auto *windows = new Handler<ICoreWebView2NewWindowRequestedEventHandler, ICoreWebView2 *, ICoreWebView2NewWindowRequestedEventArgs *>(
71
+ IID_ICoreWebView2NewWindowRequestedEventHandler, [](ICoreWebView2 *, ICoreWebView2NewWindowRequestedEventArgs *args) -> HRESULT {
72
+ LPWSTR requested = nullptr;
73
+ args->get_Uri(&requested);
74
+ const std::string uri = take(requested);
75
+ BOOL followed = FALSE;
76
+ args->get_IsUserInitiated(&followed);
77
+ if (followed) openOutside(uri);
78
+ args->put_Handled(TRUE);
79
+ return S_OK;
80
+ });
81
+ core->add_NewWindowRequested(windows, &token);
82
+ windows->Release();
83
+ auto *loading = new Handler<ICoreWebView2ContentLoadingEventHandler, ICoreWebView2 *, ICoreWebView2ContentLoadingEventArgs *>(
84
+ IID_ICoreWebView2ContentLoadingEventHandler, [lock](ICoreWebView2 *, ICoreWebView2ContentLoadingEventArgs *) -> HRESULT {
85
+ lock->newPage();
86
+ return S_OK;
87
+ });
88
+ core->add_ContentLoading(loading, &token);
89
+ loading->Release();
90
+ }
package/js/codec.js ADDED
@@ -0,0 +1,123 @@
1
+ // Tagged JSON for the webview channel: the values structured clone carries and JSON does not.
2
+ const TAG = '\u0001t';
3
+ const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
4
+ const B64_INDEX = new Uint8Array(128);
5
+ for (let i = 0; i < 64; i += 1) B64_INDEX[B64.charCodeAt(i)] = i;
6
+
7
+ const VIEWS = {
8
+ Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array,
9
+ Float32Array, Float64Array, BigInt64Array, BigUint64Array, DataView,
10
+ };
11
+
12
+ // The engine's own base64 where it has one (the page), the host's where it does not (Hermes, which interprets JS).
13
+ export function toBase64(bytes) {
14
+ if (typeof bytes.toBase64 === 'function') return bytes.toBase64();
15
+ if (typeof globalThis.__host_b64encode === 'function') return globalThis.__host_b64encode(bytes.buffer, bytes.byteOffset, bytes.byteLength);
16
+ const parts = [];
17
+ const CHUNK = 3 * 4096;
18
+ for (let start = 0; start < bytes.length; start += CHUNK) {
19
+ const end = Math.min(start + CHUNK, bytes.length);
20
+ let out = '';
21
+ let i = start;
22
+ for (; i + 2 < end; i += 3) {
23
+ const n = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
24
+ out += B64[n >> 18] + B64[(n >> 12) & 63] + B64[(n >> 6) & 63] + B64[n & 63];
25
+ }
26
+ if (i < end) {
27
+ const n = (bytes[i] << 16) | ((i + 1 < end ? bytes[i + 1] : 0) << 8);
28
+ out += B64[n >> 18] + B64[(n >> 12) & 63] + (i + 1 < end ? B64[(n >> 6) & 63] : '=') + '=';
29
+ }
30
+ parts.push(out);
31
+ }
32
+ return parts.join('');
33
+ }
34
+
35
+ export function fromBase64(text) {
36
+ if (typeof Uint8Array.fromBase64 === 'function') return Uint8Array.fromBase64(text);
37
+ if (typeof globalThis.__host_b64decode === 'function') return new Uint8Array(globalThis.__host_b64decode(text));
38
+ const padding = text.endsWith('==') ? 2 : text.endsWith('=') ? 1 : 0;
39
+ const length = (text.length / 4) * 3 - padding;
40
+ const bytes = new Uint8Array(length);
41
+ let o = 0;
42
+ for (let i = 0; i < text.length; i += 4) {
43
+ const n = (B64_INDEX[text.charCodeAt(i)] << 18) | (B64_INDEX[text.charCodeAt(i + 1)] << 12)
44
+ | (B64_INDEX[text.charCodeAt(i + 2)] << 6) | B64_INDEX[text.charCodeAt(i + 3)];
45
+ if (o < length) bytes[o++] = n >> 16;
46
+ if (o < length) bytes[o++] = (n >> 8) & 255;
47
+ if (o < length) bytes[o++] = n & 255;
48
+ }
49
+ return bytes;
50
+ }
51
+
52
+ export const CODEC_TAG = TAG;
53
+
54
+ // `hook` sees every value first and may replace it with a tagged one of its own: a handle, an enum, an attachment.
55
+ export function pack(value, hook) {
56
+ if (hook) {
57
+ const replaced = hook(value);
58
+ if (replaced !== undefined) return replaced;
59
+ }
60
+ const next = (item) => pack(item, hook);
61
+ switch (typeof value) {
62
+ case 'undefined': return { [TAG]: 'u' };
63
+ case 'bigint': return { [TAG]: 'b', v: value.toString() };
64
+ case 'number':
65
+ if (Number.isNaN(value)) return { [TAG]: 'n', v: 'NaN' };
66
+ if (!Number.isFinite(value)) return { [TAG]: 'n', v: value > 0 ? 'Infinity' : '-Infinity' };
67
+ if (Object.is(value, -0)) return { [TAG]: 'n', v: '-0' };
68
+ return value;
69
+ case 'object': {
70
+ if (value === null) return null;
71
+ if (Array.isArray(value)) return value.map(next);
72
+ if (value instanceof Set) return { [TAG]: 'set', v: [...value].map(next) };
73
+ if (value instanceof Map) return { [TAG]: 'map', v: [...value].map(([key, item]) => [next(key), next(item)]) };
74
+ if (value instanceof Date) return { [TAG]: 'date', v: value.getTime() };
75
+ if (value instanceof ArrayBuffer) return { [TAG]: 'ab', v: toBase64(new Uint8Array(value)) };
76
+ if (ArrayBuffer.isView(value)) {
77
+ const bytes = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
78
+ return { [TAG]: 'view', k: value.constructor.name, v: toBase64(bytes) };
79
+ }
80
+ const out = {};
81
+ for (const key of Object.keys(value)) out[key] = next(value[key]);
82
+ return out;
83
+ }
84
+ // As structured clone does, which is what a worker-backed web build gives the same values.
85
+ case 'function':
86
+ case 'symbol': {
87
+ const error = new Error(`${String(value)} could not be cloned`);
88
+ error.name = 'DataCloneError';
89
+ throw error;
90
+ }
91
+ default: return value;
92
+ }
93
+ }
94
+
95
+ // `hook` turns the tags it owns back into values; it returns undefined for the codec's own tags.
96
+ export function unpack(value, hook) {
97
+ if (value === null || typeof value !== 'object') return value;
98
+ const next = (item) => unpack(item, hook);
99
+ if (Array.isArray(value)) return value.map(next);
100
+ const tag = value[TAG];
101
+ if (tag !== undefined) {
102
+ if (hook) {
103
+ const replaced = hook(tag, value);
104
+ if (replaced !== undefined) return replaced;
105
+ }
106
+ if (tag === 'u') return undefined;
107
+ if (tag === 'b') return BigInt(value.v);
108
+ if (tag === 'n') return value.v === '-0' ? -0 : Number(value.v);
109
+ if (tag === 'set') return new Set(value.v.map(next));
110
+ if (tag === 'map') return new Map(value.v.map(([key, item]) => [next(key), next(item)]));
111
+ if (tag === 'date') return new Date(value.v);
112
+ if (tag === 'ab') return fromBase64(value.v).buffer;
113
+ if (tag === 'view') {
114
+ const bytes = fromBase64(value.v);
115
+ const View = VIEWS[value.k] ?? Uint8Array;
116
+ if (View === DataView) return new DataView(bytes.buffer);
117
+ return new View(bytes.buffer, 0, bytes.byteLength / (View.BYTES_PER_ELEMENT ?? 1));
118
+ }
119
+ }
120
+ const out = {};
121
+ for (const key of Object.keys(value)) out[key] = next(value[key]);
122
+ return out;
123
+ }
package/js/host.js ADDED
@@ -0,0 +1,8 @@
1
+ // What runs in Hermes: the embind runtime holding the project's bindings, and the side of the lean protocol that
2
+ // answers the page. The host calls __crossbind_start once every binding is registered.
3
+ import './polyfills.js';
4
+ import Module from '@crossbind/core-embind-jsi';
5
+ import systemConfig from 'crossbind/systemConfig';
6
+ import { installHost } from './lean-host.js';
7
+
8
+ globalThis.__crossbind_start = () => installHost(Module, systemConfig.env);
@@ -0,0 +1,147 @@
1
+ // The lean protocol's page side: the awaitable module a worker-backed web build hands out, over one message per call.
2
+ // Byte arrays from transport.attachBytes up travel as attachments of a frame, a large result comes back on its own, and
3
+ // a frame waits for the messages sent before it, so order holds where the two take different channels.
4
+ import { pack, unpack, CODEC_TAG as TAG } from './codec.js';
5
+
6
+ const FUNCTION = Symbol('remote function');
7
+ const LOCAL_MEMBERS = {
8
+ toJSON: () => ({}),
9
+ [Symbol.toPrimitive]: (hint) => (hint === 'number' ? NaN : '[object Object]'),
10
+ };
11
+
12
+ function concatenate(views) {
13
+ const total = views.reduce((sum, view) => sum + view.byteLength, 0);
14
+ const body = new Uint8Array(total);
15
+ let offset = 0;
16
+ for (const view of views) {
17
+ body.set(new Uint8Array(view.buffer, view.byteOffset, view.byteLength), offset);
18
+ offset += view.byteLength;
19
+ }
20
+ return body;
21
+ }
22
+
23
+ function createSession(transport) {
24
+ const handleIds = new WeakMap();
25
+ const tokens = new Map();
26
+ const tokenIds = new WeakMap();
27
+
28
+ const pendingRelease = [];
29
+ const collector = typeof FinalizationRegistry === 'function' ? new FinalizationRegistry((id) => {
30
+ pendingRelease.push(id);
31
+ if (pendingRelease.length > 1) return;
32
+ setTimeout(() => {
33
+ send(['release', 0, [], [pendingRelease.splice(0)]]).catch(() => {});
34
+ }, 0);
35
+ }) : null;
36
+
37
+ // Messages wait for a request in flight, and a request for the messages sent before it.
38
+ const inflight = new Set();
39
+ let gate = Promise.resolve();
40
+ const track = (promise) => {
41
+ inflight.add(promise);
42
+ promise.then(() => inflight.delete(promise), () => inflight.delete(promise));
43
+ return promise;
44
+ };
45
+ function send(message) {
46
+ const text = transport.token + JSON.stringify(message);
47
+ return track(gate.then(() => transport.send(text)));
48
+ }
49
+ function sendWithAttachments(message, attachments) {
50
+ const query = `m=${encodeURIComponent(JSON.stringify([...message, attachments.map((view) => [view.constructor.name, view.byteLength])]))}`;
51
+ const body = concatenate(attachments);
52
+ const done = Promise.allSettled([...inflight]).then(() => transport.frame(`${query}&t=${transport.token}`, body));
53
+ gate = done.then(() => undefined, () => undefined);
54
+ return track(done);
55
+ }
56
+
57
+ function tokenFor(id, value) {
58
+ let token = tokens.get(id);
59
+ if (!token) {
60
+ token = Object.freeze({ __embindEnumRef: id, value });
61
+ tokens.set(id, token);
62
+ tokenIds.set(token, id);
63
+ }
64
+ return token;
65
+ }
66
+
67
+ function handleFor(id) {
68
+ const handle = remote(id, []);
69
+ handleIds.set(handle, id);
70
+ collector?.register(handle, id);
71
+ return handle;
72
+ }
73
+
74
+ function encodeArgs(args, attachments) {
75
+ return pack(args, (value) => {
76
+ if (typeof value === 'function') {
77
+ if (handleIds.has(value)) return { [TAG]: 'h', v: handleIds.get(value) };
78
+ const error = new Error('A function cannot be passed to the native module');
79
+ error.name = 'DataCloneError';
80
+ throw error;
81
+ }
82
+ if (value !== null && typeof value === 'object' && tokenIds.has(value)) return { [TAG]: 'e', v: tokenIds.get(value) };
83
+ if (ArrayBuffer.isView(value) && value.byteLength >= transport.attachBytes) {
84
+ attachments.push(value);
85
+ return { [TAG]: 'att', i: attachments.length - 1 };
86
+ }
87
+ return undefined;
88
+ });
89
+ }
90
+
91
+ async function decodeReply(text) {
92
+ const [failed, value] = JSON.parse(text);
93
+ if (failed) throw Object.assign(new Error(value.message), value);
94
+ if (value !== null && typeof value === 'object' && value[TAG] === 'big') {
95
+ const bytes = await transport.result(`id=${value.v}&t=${transport.token}`);
96
+ const View = globalThis[value.k] ?? Uint8Array;
97
+ return new View(bytes, 0, bytes.byteLength / (View.BYTES_PER_ELEMENT ?? 1));
98
+ }
99
+ return unpack(value, (tag, item) => {
100
+ if (tag === 'h') return handleFor(item.v);
101
+ if (tag === 'e') return tokenFor(item.v, item.n);
102
+ if (tag === 'fn') return FUNCTION;
103
+ return undefined;
104
+ });
105
+ }
106
+
107
+ async function request(op, target, path, args) {
108
+ const attachments = [];
109
+ const message = [op, target, path, encodeArgs(args, attachments)];
110
+ const reply = await (attachments.length ? sendWithAttachments(message, attachments) : send(message));
111
+ const value = await decodeReply(reply);
112
+ return value === FUNCTION ? remote(target, path, true) : value;
113
+ }
114
+
115
+ // A path from the module or an object: awaiting it reads the value, calling or constructing it runs on the host.
116
+ // A function read that way is no longer awaitable, or awaiting it would read it again and again.
117
+ function remote(target, path, isRead = false) {
118
+ return new Proxy(function remoteMember() {}, {
119
+ get(_, prop) {
120
+ if (prop === 'then') {
121
+ if (path.length === 0 || isRead) return undefined;
122
+ return (resolve, reject) => request('get', target, path, []).then(resolve, reject);
123
+ }
124
+ if (Object.hasOwn(LOCAL_MEMBERS, prop)) return LOCAL_MEMBERS[prop];
125
+ if (typeof prop === 'symbol') return undefined;
126
+ return remote(target, [...path, prop]);
127
+ },
128
+ set(_, prop, value) {
129
+ request('set', target, [...path, prop], [value]).catch(() => {});
130
+ return true;
131
+ },
132
+ apply: (_, thisArg, args) => request('call', target, path, args),
133
+ construct: (_, args) => request('new', target, path, args),
134
+ });
135
+ }
136
+
137
+ return { request, module: remote(0, []) };
138
+ }
139
+
140
+ // transport: { send(text) → Promise<reply text>; frame(query, body) → Promise<reply text>, a message whose byte arrays
141
+ // are the body; result(query) → Promise<ArrayBuffer>, a large result; attachBytes; token: what the host gave the page,
142
+ // which every message, frame and result request carries }. A frame or result the host refuses rejects with its answer.
143
+ export async function connect(transport, env) {
144
+ const session = createSession(transport);
145
+ if (env && Object.keys(env).length > 0) await session.request('env', 0, [], [env]);
146
+ return session.module;
147
+ }
@@ -0,0 +1,237 @@
1
+ // The lean protocol's host side, with the contract a worker-backed web build has: objects as handles the page releases,
2
+ // enum values with their identity, vectors returned as arrays, plain arrays coerced into vector parameters, errors
3
+ // with their own fields, and large byte arrays as attachments of a crossbind:// request.
4
+ /* global __host_reply */
5
+ import { pack, unpack, CODEC_TAG as TAG } from './codec.js';
6
+ import { setCoercionModule, unwrapCoercionProxy, wrapWithVectorCoercion } from 'crossbind/runtime/adapters/vector-coercion.js';
7
+ import { UNBOUND_MESSAGE } from 'crossbind/runtime/adapters/unboundMessage.js';
8
+
9
+ // Results holding a typed array at least this large go back as a crossbind:// response body.
10
+ const BIG_RESULT_BYTES = 4096;
11
+
12
+ const registry = new Map();
13
+ const holders = new WeakMap();
14
+ let nextHandle = 1;
15
+
16
+ const enumValues = new WeakSet();
17
+ const enumIds = new WeakMap();
18
+ const enumsById = new Map();
19
+ let nextEnum = 1;
20
+
21
+ const bigResults = new Map();
22
+ let nextBig = 1;
23
+
24
+ let module = null;
25
+ let root = null;
26
+
27
+ const isEmbindObject = (value) => typeof value.delete === 'function' && typeof value.isDeleted === 'function';
28
+ const isVector = (value) => isEmbindObject(value) && typeof value.size === 'function' && typeof value.get === 'function';
29
+ // What JavaScript owns, as the worker adapter decides it: a smart pointer or a copy embind marked as owned.
30
+ const ownsObject = (value) => Boolean(value.$$ && (value.$$.smartPtr || value.$$.count?.owned));
31
+
32
+ // The module's own objects, its globals above all, live as long as the module, so letting go of a handle to one
33
+ // frees nothing.
34
+ const moduleObjects = new WeakSet();
35
+
36
+ function freeIfOwned(value) {
37
+ if (ownsObject(value) && !moduleObjects.has(value) && !value.isDeleted()) value.delete();
38
+ }
39
+
40
+ function register(value) {
41
+ const id = nextHandle;
42
+ nextHandle += 1;
43
+ registry.set(id, value);
44
+ holders.set(value, (holders.get(value) ?? 0) + 1);
45
+ return id;
46
+ }
47
+
48
+ function release(ids) {
49
+ for (const id of ids) {
50
+ const value = registry.get(id);
51
+ if (!registry.delete(id)) continue;
52
+ const left = holders.get(value) - 1;
53
+ if (left > 0) {
54
+ holders.set(value, left);
55
+ } else {
56
+ holders.delete(value);
57
+ freeIfOwned(value);
58
+ }
59
+ }
60
+ }
61
+
62
+ // embind hangs enum values off the enum type; an instance with a numeric value and no lifecycle methods is one.
63
+ function collectEnums() {
64
+ for (const key of Object.keys(module)) {
65
+ const holder = module[key];
66
+ if (holder == null || (typeof holder !== 'object' && typeof holder !== 'function') || ArrayBuffer.isView(holder)) continue;
67
+ for (const value of Object.values(holder)) {
68
+ if (value != null && typeof value === 'object' && typeof value.value === 'number'
69
+ && typeof value.delete !== 'function' && value.constructor && value.constructor !== Object) {
70
+ enumValues.add(value);
71
+ }
72
+ }
73
+ }
74
+ }
75
+
76
+ function enumIdOf(value) {
77
+ let id = enumIds.get(value);
78
+ if (!id) {
79
+ id = nextEnum;
80
+ nextEnum += 1;
81
+ enumIds.set(value, id);
82
+ enumsById.set(id, value);
83
+ }
84
+ return id;
85
+ }
86
+
87
+ // The build's env never replaces a variable the process already has; values the page passes to initNative() do.
88
+ function setEnv(env, overwrite) {
89
+ const dataPath = module.Crossbind.getEnv('CROSSBIND_DATA_PATH');
90
+ for (const [key, value] of Object.entries(env)) {
91
+ module.Crossbind.setEnv(key, String(value).replace('_CROSSBIND_DATA_PATH_', dataPath), overwrite);
92
+ }
93
+ }
94
+
95
+ function encode(value) {
96
+ return pack(value, (item) => {
97
+ if (typeof item === 'function') return { [TAG]: 'fn' };
98
+ if (item === null || typeof item !== 'object' || ArrayBuffer.isView(item)) return undefined;
99
+ const raw = unwrapCoercionProxy(item);
100
+ if (enumValues.has(raw)) return { [TAG]: 'e', v: enumIdOf(raw), n: raw.value };
101
+ if (isVector(raw)) {
102
+ const elements = [];
103
+ for (let i = 0; i < raw.size(); i += 1) elements.push(encode(raw.get(i)));
104
+ freeIfOwned(raw);
105
+ return elements;
106
+ }
107
+ if (isEmbindObject(raw)) return { [TAG]: 'h', v: register(raw) };
108
+ return undefined;
109
+ });
110
+ }
111
+
112
+ function decode(value, attachments) {
113
+ return unpack(value, (tag, item) => {
114
+ if (tag === 'h') {
115
+ if (!registry.has(item.v)) throw new Error(`crossbind: object ${item.v} was already released`);
116
+ return registry.get(item.v);
117
+ }
118
+ if (tag === 'e') return enumsById.get(item.v);
119
+ if (tag === 'att') return attachments[item.i];
120
+ return undefined;
121
+ });
122
+ }
123
+
124
+ function encodeResult(op, value) {
125
+ // What JavaScript constructs stays an object, a vector too, as the worker adapter hands out what `new` makes.
126
+ if (op === 'new' && value !== null && typeof value === 'object' && isEmbindObject(unwrapCoercionProxy(value))) {
127
+ return { [TAG]: 'h', v: register(unwrapCoercionProxy(value)) };
128
+ }
129
+ if (ArrayBuffer.isView(value) && value.byteLength >= BIG_RESULT_BYTES) {
130
+ const id = nextBig;
131
+ nextBig += 1;
132
+ bigResults.set(id, value);
133
+ return { [TAG]: 'big', v: id, k: value.constructor.name };
134
+ }
135
+ return encode(value);
136
+ }
137
+
138
+ function encodeError(error) {
139
+ const fields = { name: error?.name ?? 'Error', message: String(error?.message ?? error) };
140
+ if (error !== null && typeof error === 'object') {
141
+ for (const key of Object.keys(error)) {
142
+ const item = error[key];
143
+ if (item === null || ['string', 'number', 'boolean'].includes(typeof item)) fields[key] = item;
144
+ }
145
+ }
146
+ return fields;
147
+ }
148
+
149
+ // An operation on a path from the module, or from an object the page holds a handle to.
150
+ function dispatch(op, target, path, args) {
151
+ if (op === 'release') {
152
+ release(args[0]);
153
+ return null;
154
+ }
155
+ if (op === 'env') {
156
+ setEnv(args[0], true);
157
+ return null;
158
+ }
159
+ const base = target === 0 ? root : wrapWithVectorCoercion(registry.get(target));
160
+ if (base === undefined) throw new Error(`crossbind: object ${target} was already released`);
161
+ const parent = path.slice(0, -1).reduce((object, key) => object[key], base);
162
+ const key = path[path.length - 1];
163
+ if (op === 'get') return path.length === 0 ? base : parent[key];
164
+ if (op === 'set') {
165
+ parent[key] = args[0];
166
+ return true;
167
+ }
168
+ const member = parent[key];
169
+ if (typeof member !== 'function') throw new Error(`crossbind: ${path.join('.')} ${UNBOUND_MESSAGE}`);
170
+ if (op === 'new') return new member(...args);
171
+ return member.apply(parent, args);
172
+ }
173
+
174
+ function answer(message, attachments) {
175
+ const [op, target, path, args] = message;
176
+ try {
177
+ return JSON.stringify([0, encodeResult(op, dispatch(op, target, path, decode(args, attachments)))]);
178
+ } catch (error) {
179
+ return JSON.stringify([1, encodeError(error)]);
180
+ }
181
+ }
182
+
183
+ function viewOf(kind, buffer, offset, length) {
184
+ const View = globalThis[kind] ?? Uint8Array;
185
+ const bytes = new Uint8Array(buffer, offset, length);
186
+ if (View === Uint8Array) return bytes;
187
+ const copy = bytes.slice();
188
+ return new View(copy.buffer, 0, length / (View.BYTES_PER_ELEMENT ?? 1));
189
+ }
190
+
191
+ function parseQuery(query) {
192
+ const params = {};
193
+ for (const part of query.split('&')) {
194
+ const at = part.indexOf('=');
195
+ params[decodeURIComponent(at < 0 ? part : part.slice(0, at))] = at < 0 ? '' : decodeURIComponent(part.slice(at + 1));
196
+ }
197
+ return params;
198
+ }
199
+
200
+ export function installHost(embindModule, env) {
201
+ module = embindModule;
202
+ // A plain array passed for a vector parameter becomes one of the module's vector classes.
203
+ setCoercionModule(module);
204
+ root = wrapWithVectorCoercion(module);
205
+ collectEnums();
206
+ for (const value of Object.values(module)) {
207
+ if (value !== null && typeof value === 'object' && isEmbindObject(value)) moduleObjects.add(value);
208
+ }
209
+ setEnv(env, false);
210
+
211
+ globalThis.__crossbind_receive = (call, text) => __host_reply(call, answer(JSON.parse(text), []));
212
+
213
+ // A new page replaced the one that held these, and its garbage collector will never release them.
214
+ globalThis.__crossbind_reset = () => {
215
+ release([...registry.keys()]);
216
+ bigResults.clear();
217
+ };
218
+
219
+ // A message in the query, its attachments one after another in the body.
220
+ globalThis.__crossbind_frame = (query, buffer) => {
221
+ const message = JSON.parse(parseQuery(query).m);
222
+ const attachments = [];
223
+ let offset = 0;
224
+ for (const [kind, length] of message[4]) {
225
+ attachments.push(viewOf(kind, buffer, offset, length));
226
+ offset += length;
227
+ }
228
+ return answer(message, attachments);
229
+ };
230
+
231
+ globalThis.__crossbind_result = (query) => {
232
+ const id = Number(parseQuery(query).id);
233
+ const value = bigResults.get(id);
234
+ bigResults.delete(id);
235
+ return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
236
+ };
237
+ }