Skip to content

Commit b019dc2

Browse files
Googlercopybara-github
authored andcommitted
[WASM] JsInterop - Add invoke logic to execute the JavaScript function within a Wasm JsFunction adapter.
Also added a test case to enable once more functionality is implemented. Doing this incrementally as this is logic that's common to all possible implementation paths PiperOrigin-RevId: 926806623
1 parent f0d829e commit b019dc2

31 files changed

Lines changed: 371 additions & 36 deletions

File tree

transpiler/java/com/google/j2cl/transpiler/ast/LambdaAdaptorTypeDescriptors.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ private static ImmutableList<MethodDescriptor> getWasmJsFunctionAdaptorMethodDes
140140
return ImmutableList.of(
141141
getLambdaAdaptorDefaultConstructor(adaptorTypeDescriptor),
142142
getWasmJsFunctionAdaptorConstructor(adaptorTypeDescriptor),
143+
getWasmJsFunctionInvokeMethod(adaptorTypeDescriptor),
143144
getAdaptorForwardingMethod(adaptorTypeDescriptor));
144145
}
145146

@@ -249,6 +250,43 @@ public static MethodDescriptor getWasmJsFunctionAdaptorConstructor(
249250
.build();
250251
}
251252

253+
/**
254+
* Returns the MethodDescriptor for the static native invoke method of the Wasm JS function
255+
* adaptor class.
256+
*
257+
* <p>This method imports j2wasm.JsInteropRuntime > invokeJsFunction for the particular adapter.
258+
*/
259+
@SuppressWarnings("ReferenceEquality")
260+
public static MethodDescriptor getWasmJsFunctionInvokeMethod(
261+
DeclaredTypeDescriptor adaptorTypeDescriptor) {
262+
DeclaredTypeDescriptor functionalInterfaceTypeDescriptor =
263+
adaptorTypeDescriptor.getFunctionalInterface();
264+
checkState(
265+
functionalInterfaceTypeDescriptor.getFunctionalInterface()
266+
== functionalInterfaceTypeDescriptor);
267+
268+
MethodDescriptor functionalInterfaceMethodDescriptor =
269+
functionalInterfaceTypeDescriptor.getSingleAbstractMethodDescriptor();
270+
return MethodDescriptor.builder()
271+
.setEnclosingTypeDescriptor(adaptorTypeDescriptor)
272+
.setName("$invoke")
273+
.setStatic(true)
274+
.setNative(true)
275+
.setOriginalJsInfo(
276+
JsInfo.builder()
277+
.setJsMemberType(JsMemberType.METHOD)
278+
.setJsName("invokeJsFunction")
279+
.setJsNamespace("j2wasm.JsInteropRuntime")
280+
.build())
281+
.setParameterTypeDescriptors(
282+
ImmutableList.<TypeDescriptor>builder()
283+
.add(TypeDescriptors.get().javaemulInternalWasmExtern)
284+
.addAll(functionalInterfaceMethodDescriptor.getParameterTypeDescriptors())
285+
.build())
286+
.setReturnTypeDescriptor(functionalInterfaceMethodDescriptor.getReturnTypeDescriptor())
287+
.build();
288+
}
289+
252290
/** Returns the MethodDescriptor for the SAM implementation in the LambdaAdaptor class. */
253291
@SuppressWarnings("ReferenceEquality")
254292
public static MethodDescriptor getAdaptorForwardingMethod(

transpiler/java/com/google/j2cl/transpiler/backend/wasm/ExpressionTranspiler.java

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -194,19 +194,24 @@ public boolean enterCastExpression(CastExpression castExpression) {
194194
TypeDescriptor castTypeDescriptor =
195195
castExpression.getCastTypeDescriptor().toRawTypeDescriptor();
196196

197-
if (TypeDescriptors.isJavaLangObject(castTypeDescriptor)) {
198-
// Avoid unnecessary casts to j.l.Object. This also helps avoiding a Wasm cast whem going
199-
// from native arrays to WasmOpaque objects (and vice versa) which would otherwise fail.
200-
render(castExpression.getExpression());
201-
return false;
202-
}
197+
// TODO(b/515144544): Revisit this check for Wasm intrinsic calls when we have more robust
198+
// handling of types on the boundary.
199+
if (!isWasmCall(castExpression.getExpression())) {
200+
if (TypeDescriptors.isJavaLangObject(castTypeDescriptor)) {
201+
// Avoid unnecessary casts to j.l.Object. This also helps avoiding a Wasm cast whem
202+
// going from native arrays to WasmOpaque objects (and vice versa) which would otherwise
203+
// fail.
204+
render(castExpression.getExpression());
205+
return false;
206+
}
203207

204-
if (castTypeDescriptor.isInterface()) {
205-
// TODO(b/183769034): At the moment the actual cast check is performed at interface
206-
// method call. Depending on whether Wasm NPEs become catchable there might need to be
207-
// instrumentation code here.
208-
render(castExpression.getExpression());
209-
return false;
208+
if (castTypeDescriptor.isInterface()) {
209+
// TODO(b/183769034): At the moment the actual cast check is performed at interface
210+
// method call. Depending on whether Wasm NPEs become catchable there might need to be
211+
// instrumentation code here.
212+
render(castExpression.getExpression());
213+
return false;
214+
}
210215
}
211216

212217
// TODO(b/184675805): implement array cast expressions beyond this nominal
@@ -701,5 +706,10 @@ private static boolean isNativeConstructor(MethodDescriptor method) {
701706
return method.getEnclosingTypeDescriptor().isNative() && method.isConstructor();
702707
}
703708

709+
private static boolean isWasmCall(Expression expression) {
710+
return expression instanceof Invocation invocation
711+
&& getWasmInfo(invocation.getTarget()) != null;
712+
}
713+
704714
private ExpressionTranspiler() {}
705715
}

transpiler/java/com/google/j2cl/transpiler/passes/AddFunctionalInterfaceAdaptors.java

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,14 @@
1818
import com.google.j2cl.transpiler.ast.AstUtils;
1919
import com.google.j2cl.transpiler.ast.CompilationUnit;
2020
import com.google.j2cl.transpiler.ast.DeclaredTypeDescriptor;
21+
import com.google.j2cl.transpiler.ast.Expression;
2122
import com.google.j2cl.transpiler.ast.Field;
2223
import com.google.j2cl.transpiler.ast.FieldAccess;
2324
import com.google.j2cl.transpiler.ast.LambdaAdaptorTypeDescriptors;
2425
import com.google.j2cl.transpiler.ast.Method;
26+
import com.google.j2cl.transpiler.ast.MethodCall;
2527
import com.google.j2cl.transpiler.ast.MethodDescriptor;
28+
import com.google.j2cl.transpiler.ast.ThisReference;
2629
import com.google.j2cl.transpiler.ast.Type;
2730
import com.google.j2cl.transpiler.ast.TypeDeclaration;
2831
import com.google.j2cl.transpiler.ast.TypeDescriptors;
@@ -92,6 +95,7 @@ public void exitType(Type type) {
9295
addDefaultConstructor(adaptorType);
9396
addJsFuncrefField(adaptorType);
9497
addJsFuncrefConstructor(adaptorType);
98+
addJsFunctionInvokeMethod(adaptorType);
9599
addJsFunctionForwardingMethod(adaptorType);
96100
}
97101

@@ -166,24 +170,64 @@ private static void addJsFuncrefConstructor(Type adaptorType) {
166170
.build());
167171
}
168172

173+
/**
174+
* Adds a native invoke method to the JsFunction adaptor class to call the underlying JavaScript
175+
* function.
176+
*/
177+
private static void addJsFunctionInvokeMethod(Type adaptorType) {
178+
DeclaredTypeDescriptor adaptorTypeDescriptor = adaptorType.getTypeDescriptor();
179+
MethodDescriptor invokeMethodDescriptor =
180+
LambdaAdaptorTypeDescriptors.getWasmJsFunctionInvokeMethod(adaptorTypeDescriptor);
181+
182+
// Generates:
183+
// @JsMethod(namespace = "j2wasm.JsInteropRuntime", name = "invokeJsFunction")
184+
// native R invoke(WasmExtern jsFuncref, A a, B b, ...);
185+
adaptorType.addMember(
186+
Method.builder()
187+
.setMethodDescriptor(invokeMethodDescriptor)
188+
.setParameters(
189+
AstUtils.createParameterVariables(
190+
invokeMethodDescriptor.getParameterTypeDescriptors()))
191+
.setSourcePosition(adaptorType.getSourcePosition())
192+
.build());
193+
}
194+
169195
/** Adds a method to the JsFunction adaptor class to forward to the JavaScript function. */
170196
private static void addJsFunctionForwardingMethod(Type adaptorType) {
171197
DeclaredTypeDescriptor adaptorTypeDescriptor = adaptorType.getTypeDescriptor();
198+
SourcePosition sourcePosition = adaptorType.getSourcePosition();
172199
MethodDescriptor forwardingMethodDescriptor =
173200
LambdaAdaptorTypeDescriptors.getAdaptorForwardingMethod(adaptorTypeDescriptor);
201+
List<Variable> forwardedVariables =
202+
AstUtils.createParameterVariables(forwardingMethodDescriptor.getParameterTypeDescriptors());
203+
204+
List<Expression> invokeArguments = new ArrayList<>();
205+
invokeArguments.add(
206+
FieldAccess.builderFrom(
207+
LambdaAdaptorTypeDescriptors.getWasmJsFunctionAdaptorJsFuncrefField(
208+
adaptorTypeDescriptor))
209+
.setQualifier(new ThisReference(adaptorTypeDescriptor))
210+
.build());
211+
forwardedVariables.forEach(param -> invokeArguments.add(param.createReference()));
174212

175-
// TODO(b/512546420): Delegate to the JavaScript function.
176213
// R method(A a, B b, ...) {
177214
// // Call the JavaScript function using an imported helper.
178215
// return invoke(this.jsFuncref, a, b, ...);
179216
// }
180217
adaptorType.addMember(
181218
Method.builder()
182219
.setMethodDescriptor(forwardingMethodDescriptor)
183-
.setParameters(
184-
AstUtils.createParameterVariables(
185-
forwardingMethodDescriptor.getParameterTypeDescriptors()))
186-
.setSourcePosition(adaptorType.getSourcePosition())
220+
.setParameters(forwardedVariables)
221+
.addStatements(
222+
AstUtils.createReturnOrExpressionStatement(
223+
sourcePosition,
224+
MethodCall.builderFrom(
225+
LambdaAdaptorTypeDescriptors.getWasmJsFunctionInvokeMethod(
226+
adaptorTypeDescriptor))
227+
.setArguments(invokeArguments)
228+
.build(),
229+
forwardingMethodDescriptor.getReturnTypeDescriptor()))
230+
.setSourcePosition(sourcePosition)
187231
.build());
188232
}
189233
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright 2026 Google Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
goog.module('test.functions');
17+
18+
/**
19+
* @return {function(number): number}
20+
* @public
21+
*/
22+
function getFunction() {
23+
return (a) => a + 32;
24+
}
25+
26+
exports = {getFunction};

transpiler/javatests/com/google/j2cl/integration/java/wasmjsinterop/super-wasm/Main.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,10 @@ public static class Foo {
9393
@JsMethod(namespace = "test")
9494
private static native String appendInJs(String a, String b);
9595

96-
/** Sanity check for JsFunction interfaces to ensure lambdas still work as expected. */
96+
// TODO(b/516900958): Enable the test when a JsFunction can be imported from JS.
97+
// @JsMethod(namespace = "test.functions", name = "getFunction")
98+
// private static native MyJsFunction getFunctionFromJs();
99+
97100
private static void testJsFunction() {
98101
MyJsFunction impl = new MyJsFunctionImpl();
99102
assertEquals(15, impl.foo(5));
@@ -102,6 +105,10 @@ private static void testJsFunction() {
102105
MyJsFunction lambda = a -> a + 20;
103106
assertEquals(25, lambda.foo(5));
104107
assertEquals(1, lambda.myOverlay());
108+
109+
// TODO(b/516900958): Enable the test when a JsFunction can be imported from JS.
110+
// MyJsFunction jsFunction = getFunctionFromJs();
111+
// assertEquals(42, jsFunction.foo(10));
105112
}
106113

107114
@JsFunction

transpiler/javatests/com/google/j2cl/readable/java/deprecated/output_wasm/contents.wat.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,8 @@
402402
(local $this (ref null $deprecated.DeprecatedExample.DeprecatedJsFunction.$JsFunctionAdaptor))
403403
(local.set $this (ref.cast (ref $deprecated.DeprecatedExample.DeprecatedJsFunction.$JsFunctionAdaptor) (local.get $this.untyped)))
404404
(block
405+
;;@ transpiler/javatests/com/google/j2cl/readable/java/deprecated/readable-j2wasm.js/deprecated/DeprecatedExample.java:48:12
406+
(call $$invoke__javaemul_internal_WasmExtern__javaemul_internal_WasmExtern__void@deprecated.DeprecatedExample.DeprecatedJsFunction.$JsFunctionAdaptor (struct.get $deprecated.DeprecatedExample.DeprecatedJsFunction.$JsFunctionAdaptor $jsFuncref@deprecated.DeprecatedExample.DeprecatedJsFunction.$JsFunctionAdaptor (local.get $this))(extern.externalize (local.get $arg0)))
405407
)
406408
)
407409
(elem declare func $m_doAThing__java_lang_Object__void@deprecated.DeprecatedExample.DeprecatedJsFunction.$JsFunctionAdaptor)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
2+
3+
;;; void $JsFunctionAdaptor.$invoke(WasmExtern arg0, WasmExtern arg1)
4+
(func $$invoke__javaemul_internal_WasmExtern__javaemul_internal_WasmExtern__void@deprecated.DeprecatedExample.DeprecatedJsFunction.$JsFunctionAdaptor (import "imports" "j2wasm.JsInteropRuntime.invokeJsFunction")
5+
(param $arg0 (ref null extern))
6+
(param $arg1 (ref null extern))
7+
)

transpiler/javatests/com/google/j2cl/readable/java/importglobaljstypes/output_wasm/wasm.imports.js.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const j2wasm_ConsoleUtils = goog.require('j2wasm.ConsoleUtils');
44
const j2wasm_DoubleUtils = goog.require('j2wasm.DoubleUtils');
55
const j2wasm_EqualityUtils = goog.require('j2wasm.EqualityUtils');
66
const j2wasm_ExceptionUtils = goog.require('j2wasm.ExceptionUtils');
7+
const j2wasm_JsInteropRuntime = goog.require('j2wasm.JsInteropRuntime');
78
const j2wasm_StringUtils = goog.require('j2wasm.StringUtils');
89

910
/** @return {!Object<string, *>} Wasm import object */
@@ -102,6 +103,7 @@ function getImports() {
102103
'j2wasm.ExceptionUtils.getJavaThrowable': j2wasm_ExceptionUtils.getJavaThrowable,
103104
'j2wasm.ExceptionUtils.isError': j2wasm_ExceptionUtils.isError,
104105
'j2wasm.ExceptionUtils.setJavaThrowable': j2wasm_ExceptionUtils.setJavaThrowable,
106+
'j2wasm.JsInteropRuntime.invokeJsFunction': j2wasm_JsInteropRuntime.invokeJsFunction,
105107
'j2wasm.StringUtils.compareToIgnoreCase': j2wasm_StringUtils.compareToIgnoreCase,
106108
'j2wasm.StringUtils.equalsIgnoreCase': j2wasm_StringUtils.equalsIgnoreCase,
107109
'j2wasm.StringUtils.generateClassName': j2wasm_StringUtils.generateClassName,

transpiler/javatests/com/google/j2cl/readable/java/jsdoctypemappings/output_wasm/contents.wat.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,6 +1090,8 @@
10901090
(local $this (ref null $jsdoctypemappings.JsDocTypeMappings.NativeFunction.$JsFunctionAdaptor))
10911091
(local.set $this (ref.cast (ref $jsdoctypemappings.JsDocTypeMappings.NativeFunction.$JsFunctionAdaptor) (local.get $this.untyped)))
10921092
(block
1093+
;;@ transpiler/javatests/com/google/j2cl/readable/java/jsdoctypemappings/readable-j2wasm.js/jsdoctypemappings/JsDocTypeMappings.java:26:19
1094+
(call $$invoke__javaemul_internal_WasmExtern__void@jsdoctypemappings.JsDocTypeMappings.NativeFunction.$JsFunctionAdaptor (struct.get $jsdoctypemappings.JsDocTypeMappings.NativeFunction.$JsFunctionAdaptor $jsFuncref@jsdoctypemappings.JsDocTypeMappings.NativeFunction.$JsFunctionAdaptor (local.get $this)))
10931095
)
10941096
)
10951097
(elem declare func $m_f__void@jsdoctypemappings.JsDocTypeMappings.NativeFunction.$JsFunctionAdaptor)

transpiler/javatests/com/google/j2cl/readable/java/jsdoctypemappings/output_wasm/imports.wat.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11

22

3+
;;; void $JsFunctionAdaptor.$invoke(WasmExtern arg0)
4+
(func $$invoke__javaemul_internal_WasmExtern__void@jsdoctypemappings.JsDocTypeMappings.NativeFunction.$JsFunctionAdaptor (import "imports" "j2wasm.JsInteropRuntime.invokeJsFunction")
5+
(param $arg0 (ref null extern))
6+
)
7+
38
;;; NativeType()
49
(func $m_<init>__@jsdoctypemappings.JsDocTypeMappings.NativeType (import "imports" "Array.constructor")
510
(result (ref null extern))

0 commit comments

Comments
 (0)