Skip to content

Commit 5fb5575

Browse files
committed
Improved bridge so wrapped objects pass from the JS to Java code
1 parent 2506c3c commit 5fb5575

4 files changed

Lines changed: 281 additions & 20 deletions

File tree

Ports/JavaScriptPort/STATUS.md

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,47 +22,54 @@ Current State
2222
- Bundle generation ordering bug identified and fixed for worker mode:
2323
- Cause: `worker.js` was generated before `port.js` was copied into the output bundle, so worker never imported JavaScriptPort natives.
2424
- Fix: copy JavaScriptPort assets before `worker.js` generation and keep service-worker/shell scripts excluded from worker imports.
25-
- Latest CI artifact confirms `worker.js` now imports `port.js`, but startup still fails in `HTML5Implementation.__init`:
26-
- first failure remains `TypeError: Cannot read properties of null (reading '__classDef')`
27-
- crash site moved to `HTMLDocument.createElement(...)` invocation with null document target.
2825
- Added explicit worker-native bindings for initial DOM bridge path:
2926
- `Window.getDocument()`
3027
- `HTMLDocument.createElement(String)`
3128
- `HTMLDocument.getBody()`
3229
- `HTMLDocument.getElementById(String)`
33-
- Latest CI artifacts now run worker mode but fail early before suite start with:
34-
- `PARPAR:DIAG:FIRST_FAILURE:category=runtime_error`
35-
- `TypeError: Cannot read properties of null (reading '__classDef')`
36-
- stack rooted in `HTML5Implementation.__init` after `Window.current()` returned `null`.
30+
- Worker/main-thread JSO bridge introduced:
31+
- `parparvm_runtime.js` now routes JSO calls on host refs through `host-call` (`__cn1_jso_bridge__`).
32+
- `browser_bridge.js` now resolves host refs and executes getter/setter/method operations on main-thread DOM objects.
33+
- `Window.current()` now requests host window in worker mode (instead of using `self.window` shim).
34+
- Host-ref class metadata added:
35+
- host markers include `__cn1HostClass`, and runtime class inference consumes it.
36+
- this moved startup failure from null receiver to later bridge/cast and callback transport issues.
37+
- Latest local repro first blocker moved again:
38+
- `DataCloneError: Failed to execute 'postMessage' ... function(...) could not be cloned`
39+
- cause: non-cloneable callback/function payload crossing worker host-call boundary.
40+
- mitigation added: runtime host-call argument sanitization (`toHostTransferArg`) before `emitVmMessage`.
3741
- Existing form-constructor recovery diagnostics remain active in `port.js` and are still relevant while migrating.
3842

3943
Next Steps
4044
----------
4145

42-
1. Validate worker-only boot in CI and local:
46+
1. Validate latest host-call serialization fix in CI artifacts:
47+
- Check whether first failure moved off `DataCloneError`.
48+
- If still present, capture offending host symbol + argument type and add explicit callback-handle transport (not null coercion) for that symbol.
49+
2. Continue worker-only boot validation:
4350
- Required markers: `PARPAR:worker-mode`, `PARPAR:DIAG:BOOT:bridgeMode=worker`.
4451
- Any `main-thread-mode` marker now indicates stale artifact or wrong bundle.
45-
2. Confirm worker native rebind fix is present in produced bundle:
52+
3. Confirm worker native rebind fix is present in produced bundle:
4653
- In generated `worker.js`, ensure `__parparInstallNativeBindings()` is invoked after imports and before `start`.
4754
- This must eliminate `Window.current()` null stubs from startup execution.
48-
3. Separate VM/EDT execution from main-thread host services cleanly:
55+
4. Separate VM/EDT execution from main-thread host services cleanly:
4956
- Keep VM/EDT scheduling in worker.
5057
- Ensure main-thread browser APIs are reached through explicit host-call handlers rather than direct worker DOM access.
51-
4. Re-triage screenshot correctness in worker mode only:
58+
5. Re-triage screenshot correctness in worker mode only:
5259
- Re-run screenshot suite and classify first blocker using the existing `TOP_BLOCKER` output.
5360
- Prioritize deterministic runtime failures before throughput tuning.
54-
5. Restore full screenshot count and correctness:
61+
6. Restore full screenshot count and correctness:
5562
- Exit gate remains `CN1SS:SUITE:FINISHED` with expected screenshot artifacts and no `BROWSER:PARPAR_ERROR`.
5663

5764
Important Notes
5865
--------------
5966

60-
- Current CI artifact (`~/Downloads/javascript-ui-tests/browser.log`) shows:
67+
- Current local debug artifact (`/tmp/js-ci-debug/browser.log`) shows:
6168
- `PARPAR:worker-mode`
6269
- `PARPAR:DIAG:BOOT:bridgeMode=worker`
6370
- `TOP_BLOCKER=runtime_error|none|none`
64-
- first crash in `HTML5Implementation.__init` due null window wrapper.
65-
- This is consistent with native rebind order being a primary startup blocker in worker mode.
71+
- first crash currently as `DataCloneError` during worker->host message transport.
72+
- This indicates startup has progressed beyond initial null DOM receiver failure and is now blocked by host-call payload transport semantics.
6673

6774
Known Important Context
6875
-----------------------
@@ -78,5 +85,6 @@ Known Important Context
7885
- `CN1SS:SUITE:FINISHED`
7986
- Current local patch set touches:
8087
- `vm/ByteCodeTranslator/src/javascript/browser_bridge.js`
88+
- `vm/ByteCodeTranslator/src/javascript/parparvm_runtime.js`
8189
- `Ports/JavaScriptPort/src/main/webapp/port.js`
8290
- `scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java`

Ports/JavaScriptPort/src/main/webapp/port.js

Lines changed: 85 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -727,7 +727,17 @@ bindNative(["cn1_com_codename1_html5_js_core_JSArray_create_int_R_com_codename1_
727727
});
728728

729729
bindNative(["cn1_com_codename1_html5_js_browser_Window_current_R_com_codename1_html5_js_browser_Window", "cn1_com_codename1_html5_js_browser_Window_current___R_com_codename1_html5_js_browser_Window"], function*() {
730-
const wrapper = jvm.wrapJsObject(global.window || global.self || global, "com_codename1_html5_js_browser_Window");
730+
const nativeWindow = global.window;
731+
const hasDomWindow = !!(nativeWindow && nativeWindow.document);
732+
if (!hasDomWindow && typeof jvm.invokeHostNative === "function") {
733+
const hostWindow = yield jvm.invokeHostNative("__cn1_dom_window_current__", []);
734+
if (hostWindow != null) {
735+
const workerWrapper = jvm.wrapJsObject(hostWindow, "com_codename1_html5_js_browser_Window");
736+
jvm.enhanceJsWrapper(workerWrapper, "com_codename1_impl_html5_JSOImplementations_WindowExt");
737+
return workerWrapper;
738+
}
739+
}
740+
const wrapper = jvm.wrapJsObject((hasDomWindow ? nativeWindow : null) || global.self || global, "com_codename1_html5_js_browser_Window");
731741
jvm.enhanceJsWrapper(wrapper, "com_codename1_impl_html5_JSOImplementations_WindowExt");
732742
return wrapper;
733743
});
@@ -737,6 +747,27 @@ bindNative([
737747
"cn1_com_codename1_html5_js_browser_Window_getDocument___R_com_codename1_html5_js_dom_HTMLDocument"
738748
], function*(__cn1ThisObject) {
739749
const win = jvm.unwrapJsValue(__cn1ThisObject);
750+
if (win && win.__cn1HostRef != null && typeof jvm.invokeHostNative === "function") {
751+
const hostResult = yield jvm.invokeHostNative("__cn1_jso_bridge__", [{
752+
receiver: win,
753+
kind: "getter",
754+
member: "document",
755+
args: []
756+
}]);
757+
return hostResult == null ? null : jvm.wrapJsObject(hostResult, "com_codename1_html5_js_dom_HTMLDocument");
758+
}
759+
if (typeof jvm.invokeHostNative === "function" && (!win || !win.document)) {
760+
const hostWindow = yield jvm.invokeHostNative("__cn1_dom_window_current__", []);
761+
if (hostWindow != null) {
762+
const hostDocument = yield jvm.invokeHostNative("__cn1_jso_bridge__", [{
763+
receiver: hostWindow,
764+
kind: "getter",
765+
member: "document",
766+
args: []
767+
}]);
768+
return hostDocument == null ? null : jvm.wrapJsObject(hostDocument, "com_codename1_html5_js_dom_HTMLDocument");
769+
}
770+
}
740771
if (!win || !win.document) {
741772
return null;
742773
}
@@ -748,19 +779,47 @@ bindNative([
748779
"cn1_com_codename1_html5_js_dom_HTMLDocument_createElement___java_lang_String_R_com_codename1_html5_js_dom_HTMLElement"
749780
], function*(__cn1ThisObject, tagName) {
750781
const doc = jvm.unwrapJsValue(__cn1ThisObject);
782+
const tag = tagName == null ? "" : jvm.toNativeString(tagName);
783+
const canvasClass = "com_codename1_html5_js_dom_HTMLCanvasElement";
784+
if (doc && doc.__cn1HostRef != null && typeof jvm.invokeHostNative === "function") {
785+
const hostResult = yield jvm.invokeHostNative("__cn1_jso_bridge__", [{
786+
receiver: doc,
787+
kind: "method",
788+
member: "createElement",
789+
args: [tag]
790+
}]);
791+
if (hostResult == null) {
792+
return null;
793+
}
794+
const expectedClass = String(tag).toLowerCase() === "canvas"
795+
? canvasClass
796+
: jvm.inferJsObjectClass(hostResult, "com_codename1_html5_js_dom_HTMLElement");
797+
return jvm.wrapJsObject(hostResult, expectedClass);
798+
}
751799
if (!doc || typeof doc.createElement !== "function") {
752800
return null;
753801
}
754-
const tag = tagName == null ? "" : jvm.toNativeString(tagName);
755802
const element = doc.createElement(tag);
756-
return jvm.wrapJsObject(element, jvm.inferJsObjectClass(element, "com_codename1_html5_js_dom_HTMLElement"));
803+
const expectedClass = String(tag).toLowerCase() === "canvas"
804+
? canvasClass
805+
: jvm.inferJsObjectClass(element, "com_codename1_html5_js_dom_HTMLElement");
806+
return jvm.wrapJsObject(element, expectedClass);
757807
});
758808

759809
bindNative([
760810
"cn1_com_codename1_html5_js_dom_HTMLDocument_getBody_R_com_codename1_html5_js_dom_HTMLElement",
761811
"cn1_com_codename1_html5_js_dom_HTMLDocument_getBody___R_com_codename1_html5_js_dom_HTMLElement"
762812
], function*(__cn1ThisObject) {
763813
const doc = jvm.unwrapJsValue(__cn1ThisObject);
814+
if (doc && doc.__cn1HostRef != null && typeof jvm.invokeHostNative === "function") {
815+
const hostResult = yield jvm.invokeHostNative("__cn1_jso_bridge__", [{
816+
receiver: doc,
817+
kind: "getter",
818+
member: "body",
819+
args: []
820+
}]);
821+
return hostResult == null ? null : jvm.wrapJsObject(hostResult, "com_codename1_html5_js_dom_HTMLBodyElement");
822+
}
764823
if (!doc || !doc.body) {
765824
return null;
766825
}
@@ -772,10 +831,32 @@ bindNative([
772831
"cn1_com_codename1_html5_js_dom_HTMLDocument_getElementById___java_lang_String_R_com_codename1_html5_js_dom_HTMLElement"
773832
], function*(__cn1ThisObject, id) {
774833
const doc = jvm.unwrapJsValue(__cn1ThisObject);
834+
const nativeId = id == null ? "" : jvm.toNativeString(id);
835+
const canvasClass = "com_codename1_html5_js_dom_HTMLCanvasElement";
836+
if (doc && doc.__cn1HostRef != null && typeof jvm.invokeHostNative === "function") {
837+
const hostResult = yield jvm.invokeHostNative("__cn1_jso_bridge__", [{
838+
receiver: doc,
839+
kind: "method",
840+
member: "getElementById",
841+
args: [nativeId]
842+
}]);
843+
if (hostResult == null) {
844+
return null;
845+
}
846+
const tagName = yield jvm.invokeHostNative("__cn1_jso_bridge__", [{
847+
receiver: hostResult,
848+
kind: "getter",
849+
member: "tagName",
850+
args: []
851+
}]);
852+
const expectedClass = String(tagName || "").toUpperCase() === "CANVAS"
853+
? canvasClass
854+
: jvm.inferJsObjectClass(hostResult, "com_codename1_html5_js_dom_HTMLElement");
855+
return jvm.wrapJsObject(hostResult, expectedClass);
856+
}
775857
if (!doc || typeof doc.getElementById !== "function") {
776858
return null;
777859
}
778-
const nativeId = id == null ? "" : jvm.toNativeString(id);
779860
const element = doc.getElementById(nativeId);
780861
return element == null ? null : jvm.wrapJsObject(element, jvm.inferJsObjectClass(element, "com_codename1_html5_js_dom_HTMLElement"));
781862
});

vm/ByteCodeTranslator/src/javascript/browser_bridge.js

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,128 @@
100100
}
101101
}
102102
};
103+
104+
var hostRefNextId = 1;
105+
var hostRefById = {};
106+
var hostRefByObject = (typeof WeakMap === 'function') ? new WeakMap() : null;
107+
108+
function isHostRefMarker(value) {
109+
return !!(value && typeof value === 'object'
110+
&& value.__cn1HostRef != null
111+
&& value.__cn1HostRef !== 0);
112+
}
113+
114+
function storeHostRef(value) {
115+
if (value == null || (typeof value !== 'object' && typeof value !== 'function')) {
116+
return value;
117+
}
118+
var inferredClass = inferHostClass(value);
119+
if (hostRefByObject && hostRefByObject.has(value)) {
120+
var existing = { __cn1HostRef: hostRefByObject.get(value) };
121+
if (inferredClass) {
122+
existing.__cn1HostClass = inferredClass;
123+
}
124+
return existing;
125+
}
126+
var id = hostRefNextId++;
127+
hostRefById[id] = value;
128+
if (hostRefByObject) {
129+
hostRefByObject.set(value, id);
130+
}
131+
var marker = { __cn1HostRef: id };
132+
if (inferredClass) {
133+
marker.__cn1HostClass = inferredClass;
134+
}
135+
return marker;
136+
}
137+
138+
function resolveHostRef(marker) {
139+
if (!isHostRefMarker(marker)) {
140+
return marker;
141+
}
142+
return hostRefById[marker.__cn1HostRef] || null;
143+
}
144+
145+
function mapHostArgs(args) {
146+
var out = [];
147+
var list = args || [];
148+
for (var i = 0; i < list.length; i++) {
149+
out.push(resolveHostRef(list[i]));
150+
}
151+
return out;
152+
}
153+
154+
function hostResult(value) {
155+
if (value == null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
156+
return value;
157+
}
158+
return storeHostRef(value);
159+
}
160+
161+
function inferHostClass(value) {
162+
if (value === global.window) {
163+
return 'com_codename1_html5_js_browser_Window';
164+
}
165+
if (value && value.nodeType === 9) {
166+
return 'com_codename1_html5_js_dom_HTMLDocument';
167+
}
168+
if (value && value.canvas && typeof value.drawImage === 'function' && typeof value.fillRect === 'function') {
169+
return 'com_codename1_html5_js_canvas_CanvasRenderingContext2D';
170+
}
171+
if (value && value.setProperty && value.removeProperty) {
172+
return 'com_codename1_html5_js_dom_CSSStyleDeclaration';
173+
}
174+
if (value && value.tagName) {
175+
var tagName = String(value.tagName).toUpperCase();
176+
if (tagName === 'CANVAS') {
177+
return 'com_codename1_html5_js_dom_HTMLCanvasElement';
178+
}
179+
if (tagName === 'BODY') {
180+
return 'com_codename1_html5_js_dom_HTMLBodyElement';
181+
}
182+
return 'com_codename1_html5_js_dom_HTMLElement';
183+
}
184+
if (value && value.nodeType === 1) {
185+
return 'com_codename1_html5_js_dom_Element';
186+
}
187+
return null;
188+
}
189+
190+
hostBridge.register('__cn1_dom_window_current__', function() {
191+
if (global.window) {
192+
return hostResult(global.window);
193+
}
194+
return null;
195+
});
196+
197+
hostBridge.register('__cn1_jso_bridge__', function(request) {
198+
var payload = request || {};
199+
var receiver = resolveHostRef(payload.receiver);
200+
if (receiver == null) {
201+
throw new Error('Missing host receiver for JSO bridge');
202+
}
203+
var kind = payload.kind;
204+
var member = payload.member;
205+
var args = mapHostArgs(payload.args || []);
206+
var value;
207+
if (kind === 'getter') {
208+
value = receiver[member];
209+
} else if (kind === 'setter') {
210+
receiver[member] = args.length ? args[0] : null;
211+
value = null;
212+
} else {
213+
var fn = receiver[member];
214+
if (typeof fn === 'function') {
215+
value = fn.apply(receiver, args);
216+
} else if (!args.length && Object.prototype.hasOwnProperty.call(receiver, member)) {
217+
value = receiver[member];
218+
} else {
219+
throw new Error('Missing JS member ' + member + ' for host receiver');
220+
}
221+
}
222+
return hostResult(value);
223+
});
224+
103225
global.__parparMessages = [];
104226
global.cn1Initialized = false;
105227
global.cn1Started = false;

0 commit comments

Comments
 (0)