JS port: isSupported() must answer false for an unbound native interface - #5515
Conversation
NativeLookup.create() can never return null on the JavaScript port -- the
builder generates and registers an <Iface>Impl for EVERY NativeInterface it
finds in the app, whether or not a JS implementation ships with the bundle.
That makes isSupported() the developer's only "is this bound here?" signal,
and it was throwing rather than answering: the host bridge rejected the call
with "No native interface implementation registered for <iface>", which
surfaced in the worker as a RuntimeException out of the standard
NativeLookup.create(X.class) != null && x.isSupported()
guard. On iOS/Android the same guard works because a missing native impl
means no *Impl class at all, so create() returns null.
Two coordinated changes:
* browser_bridge.js resolves false for the isSupported_ key when the
interface is absent from cn1_native_interfaces, or when the registered
stub defines no isSupported (warning once per interface). Every other
method still rejects -- calling an unimplemented native is a genuine bug
and must stay loud.
* The generated <Iface>Impl wraps its isSupported() bridge call in a
try/catch that degrades to false, so the contract holds even if the call
fails for some other reason.
Tests: a node harness drives browser_bridge.js's dispatch directly with the
page boot suppressed, and the generated-impl source is asserted without
running a build.
Fixes #5512
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes JavaScript-port NativeInterface.isSupported() behavior so that “missing/unbound” native interface implementations reliably answer false instead of throwing, restoring the standard NativeLookup.create(..) != null && ni.isSupported() guard semantics on the JS port.
Changes:
- Update
browser_bridge.jsnative-interface dispatch to resolvefalseforisSupported_when an interface (or itsisSupported_) is not registered, while keeping other methods rejecting loudly. - Update JavaScript builder codegen so the generated
<Iface>Impl.isSupported()wraps the bridge call in a try/catch and degrades tofalseon failure; expose impl source generation for test assertions. - Add unit tests for both the bridge dispatch behavior (Node-driven) and the builder codegen contract.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
vm/tests/src/test/java/com/codename1/tools/translator/JavaScriptNativeInterfaceBridgeTest.java |
Adds Node-based tests asserting isSupported_ resolves false for unbound/partial interfaces, while other methods still reject. |
vm/ByteCodeTranslator/src/javascript/browser_bridge.js |
Implements special-case isSupported_ fallback to false when interface or isSupported_ implementation is missing. |
maven/codenameone-maven-plugin/src/test/java/com/codename1/builders/JavaScriptBuilderNativeInterfaceTest.java |
Adds tests verifying generated impl guards only isSupported() and preserves failure propagation elsewhere. |
maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/JavaScriptBuilder.java |
Refactors native-impl source generation for testability and adds guarded isSupported() generation. |
Suppressed comments (1)
maven/codenameone-maven-plugin/src/test/java/com/codename1/builders/JavaScriptBuilderNativeInterfaceTest.java:63
- This test counts occurrences of
catch (Throwable)to ensure only isSupported() swallows bridge failures. If the generated impl is updated to catch a narrower type (recommended), update the counted substring so the assertion remains accurate.
assertEquals(1, countOccurrences(source, "catch (Throwable"),
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| sb.append(" try {\n"); | ||
| sb.append(" return com.codename1.impl.platform.js.NativeInterfaceBridge.callBoolean(__NI, \"") | ||
| .append(methodKey).append("\", new Object[0]);\n"); | ||
| sb.append(" } catch (Throwable __t) {\n"); |
| "Generated impl should implement isSupported(). source=" + source); | ||
| assertTrue(source.contains("callBoolean(__NI, \"isSupported_\", new Object[0])"), | ||
| "isSupported() should still ask the host bridge first. source=" + source); | ||
| assertTrue(source.contains("} catch (Throwable __t) {") && source.contains("return false;"), |
| // interface (issue #5512). Every other method still rejects -- calling an | ||
| // unimplemented native is a genuine bug and must stay loud. | ||
| var NI_IS_SUPPORTED = 'isSupported_'; | ||
| var niUnboundWarned = {}; |
✅ ByteCodeTranslator Quality ReportTest & Coverage
Benchmark Results
Static Analysis
Generated automatically by the PR CI workflow. |
✅ Continuous Quality ReportTest & Coverage
Static Analysis
Generated automatically by the PR CI workflow. |
Cloudflare Preview
|
|
Compared 146 screenshots: 146 matched. Benchmark ResultsDetailed Performance Metrics
|
|
Compared 151 screenshots: 151 matched. Native Android coverage
✅ Native Android screenshot tests passed. Native Android coverage
Benchmark ResultsDetailed Performance Metrics
|
|
Compared 146 screenshots: 146 matched. Benchmark ResultsDetailed Performance Metrics
|
|
Compared 147 screenshots: 147 matched. |
|
Compared 147 screenshots: 147 matched. |
|
Compared 146 screenshots: 146 matched. Benchmark ResultsDetailed Performance Metrics
|
|
Compared 181 screenshots: 181 matched. |
|
Compared 148 screenshots: 148 matched. Benchmark Results
Detailed Performance Metrics
|
|
Compared 144 screenshots: 144 matched. |
|
Compared 149 screenshots: 149 matched. Benchmark Results
Build and Run Timing
Detailed Performance Metrics
|
|
Compared 217 screenshots: 217 matched. |
|
Compared 143 screenshots: 143 matched. Benchmark Results
Build and Run Timing
Detailed Performance Metrics
|
Fixes #5512.
The problem
NativeLookup.create()can never returnnullon the JavaScript port. The builder scans the app for everyNativeInterfacesubtype and generates + registers an<Iface>Implfor each one (JavaScriptBuilder.generateNativeInterfaceImpls/writeLauncher), whether or not a JS implementation ships in the bundle. On iOS/Android the same guard works because a missing native implementation means no*Implclass at all, socreate()returnsnull.That makes
isSupported()the developer's only "is this bound here?" signal — and it was throwing rather than answering. The generatedisSupported()delegated blindly to the host bridge, which rejected an unregistered interface withNo native interface implementation registered for <iface>;resolveHostCallturns a rejection intogenerator.throw(...), so it surfaced in Java as aRuntimeExceptionthrown straight out of the standard guard:Note the generated JS stub template itself already defaults
isSupported_tocallback.complete(false)(StubGenerator.generateJavaScriptFile) — the missing-stub path just never got that far.The fix
Two coordinated changes:
browser_bridge.jsresolvesfalsefor theisSupported_key when the interface is absent fromcn1_native_interfaces, or when the registered stub defines noisSupported(with a once-per-interface console warning). Every other method still rejects — calling an unimplemented native is a genuine bug and must stay loud.<Iface>Implwraps itsisSupported()bridge call in atry/catchthat degrades tofalse, so the contract holds even if the call fails for another reason. The impl-source generation moved into a package-visiblenativeInterfaceImplSource(Class)so it can be asserted without running a build.Tests
JavaScriptNativeInterfaceBridgeTestdrivesbrowser_bridge.js's native-interface dispatch directly in node, with the page boot suppressed (adocument.readyStateof"loading"makes the bridge wait for aDOMContentLoadedthat never fires). Verified to fail against the pre-fix bridge: unregisteredisSupported_rejected with the reported message, and a stub withoutisSupported_rejected too.JavaScriptBuilderNativeInterfaceTestasserts the generated impl guardsisSupported()and onlyisSupported().Companion PR
The daemon's
ParparVMJavascriptBuildercarries the twin builder change: codenameone/BuildDaemon#170🤖 Generated with Claude Code