blob: 427aa0fa2649d605dafd24ed53fc7c0130cd5e59 [file] [log] [blame]
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:kernel/ast.dart';
import 'package:kernel/type_environment.dart';
import 'method_collector.dart';
import 'util.dart';
/// Specializes Dart callbacks so they can be called from JS.
class CallbackSpecializer {
final StatefulStaticTypeContext _staticTypeContext;
final MethodCollector _methodCollector;
final CoreTypesUtil _util;
CallbackSpecializer(
this._staticTypeContext, this._util, this._methodCollector);
Statement _generateDispatchCase(
FunctionType function,
VariableDeclaration callbackVariable,
List<VariableDeclaration> positionalParameters,
int requiredParameterCount,
{required bool boxExternRef}) {
List<Expression> callbackArguments = [];
for (int i = 0; i < requiredParameterCount; i++) {
DartType callbackParameterType = function.positionalParameters[i];
Expression expression;
VariableGet v = VariableGet(positionalParameters[i]);
if (_util.isJSValueType(callbackParameterType) && boxExternRef) {
expression = _createJSValue(v);
if (!callbackParameterType.extensionTypeErasure.isPotentiallyNullable) {
expression = NullCheck(expression);
}
} else {
expression = _util.convertAndCast(
callbackParameterType, invokeOneArg(_util.dartifyRawTarget, v));
}
callbackArguments.add(expression);
}
return ReturnStatement(StaticInvocation(
_util.jsifyTarget(function.returnType),
Arguments([
FunctionInvocation(FunctionAccessKind.FunctionType,
VariableGet(callbackVariable), Arguments(callbackArguments),
functionType: null),
])));
}
/// Creates a callback trampoline for the given [function].
///
/// This callback trampoline expects a Dart callback as its first argument,
/// then an integer value(double type) indicating the number of arguments
/// passed, followed by all of the arguments to the Dart callback as JS
/// objects. The trampoline will `dartifyRaw` or box all incoming JS objects
/// and then cast them to their appropriate types, dispatch, and then
/// `jsifyRaw` or box any returned value.
///
/// Returns a [String] function name representing the name of the wrapping
/// function.
String _createFunctionTrampoline(Procedure node, FunctionType function,
{required bool boxExternRef}) {
// Create arguments for each positional parameter in the function. These
// arguments will be JS objects. The generated wrapper will cast each
// argument to the correct type. The first argument to this function will
// be the Dart callback, which will be cast to the supplied [FunctionType]
// before being invoked. The second argument will be a `double` indicating
// the number of arguments passed.
int parameterId = 1;
final callbackVariable = VariableDeclaration('callback',
type: _util.nonNullableObjectType, isSynthesized: true);
final argumentsLength = VariableDeclaration('argumentsLength',
type: _util.coreTypes.doubleNonNullableRawType, isSynthesized: true);
// Initialize variable declarations.
List<VariableDeclaration> positionalParameters = [];
final positionalParametersLength = function.positionalParameters.length;
for (int j = 0; j < positionalParametersLength; j++) {
positionalParameters.add(VariableDeclaration('x${parameterId++}',
type: _util.nullableWasmExternRefType, isSynthesized: true));
}
// Build the body of a function trampoline. To support default arguments, we
// find the last defined argument in JS, that is the last argument which was
// explicitly passed by the user, and then we dispatch to a Dart function
// with the right number of arguments.
List<Statement> dispatchCases = [];
// If more arguments were passed than there are parameters, ignore the extra
// arguments.
dispatchCases.add(IfStatement(
_util.variableGreaterThanOrEqualToConstant(
argumentsLength, IntConstant(positionalParametersLength)),
_generateDispatchCase(function, callbackVariable, positionalParameters,
positionalParametersLength,
boxExternRef: boxExternRef),
null));
// TODO(srujzs): Consider using a switch instead.
for (int i = positionalParametersLength - 1;
i >= function.requiredParameterCount;
i--) {
dispatchCases.add(IfStatement(
_util.variableCheckConstant(
argumentsLength, DoubleConstant(i.toDouble())),
_generateDispatchCase(
function, callbackVariable, positionalParameters, i,
boxExternRef: boxExternRef),
null));
}
// Throw since we have too few arguments. Alternatively, we can continue
// checking lengths and try to call the callback, which will then throw, but
// that's unnecessary extra code. Note that we can't exclude this and assume
// the last dispatch case will catch this. Since arguments that are not
// passed are `undefined` and `undefined` gets converted to `null`, they may
// be treated as valid `null` arguments to the Dart function even though
// they were never passed.
dispatchCases.add(ExpressionStatement(Throw(StringConcatenation([
StringLiteral('Too few arguments passed. '
'Expected ${function.requiredParameterCount} or more, got '),
invokeMethod(argumentsLength, _util.numToIntTarget),
StringLiteral(' instead.')
]))));
Statement functionTrampolineBody = Block(dispatchCases);
// Create a new procedure for the callback trampoline. This procedure will
// be exported from Wasm to JS so it can be called from JS. The argument
// returned from the supplied callback will be converted with `jsifyRaw` to
// a native JS value before being returned to JS.
final functionTrampolineName = _methodCollector.generateMethodName();
_methodCollector.addInteropProcedure(
functionTrampolineName,
functionTrampolineName,
FunctionNode(functionTrampolineBody,
positionalParameters: [
callbackVariable,
argumentsLength,
...positionalParameters
],
returnType: _util.nullableWasmExternRefType)
..fileOffset = node.fileOffset,
node.fileUri,
AnnotationType.export,
isExternal: false);
return functionTrampolineName;
}
/// Returns a JS method that wraps a Dart callback in a JS wrapper.
Procedure _getJSWrapperFunction(Procedure node, FunctionType type,
{required bool boxExternRef}) {
final functionTrampolineName =
_createFunctionTrampoline(node, type, boxExternRef: boxExternRef);
List<String> jsParameters = [];
for (int i = 0; i < type.positionalParameters.length; i++) {
jsParameters.add('x$i');
}
String jsParametersString = jsParameters.join(',');
String dartArguments = 'f,arguments.length';
if (jsParameters.isNotEmpty) {
dartArguments = '$dartArguments,$jsParametersString';
}
// Create Dart procedure stub.
final jsMethodName = functionTrampolineName;
Procedure dartProcedure = _methodCollector.addInteropProcedure(
'|$jsMethodName',
'dart2wasm.$jsMethodName',
FunctionNode(null,
positionalParameters: [
VariableDeclaration('dartFunction',
type: _util.nonNullableWasmExternRefType, isSynthesized: true)
],
returnType: _util.nonNullableWasmExternRefType),
node.fileUri,
AnnotationType.import,
isExternal: true);
// Create JS method.
// Note: We have to use a regular function for the inner closure in some
// cases because we need access to `arguments`.
_methodCollector.addMethod(
dartProcedure,
jsMethodName,
"f => finalizeWrapper(f, function($jsParametersString) {"
" return dartInstance.exports.$functionTrampolineName($dartArguments) "
"})");
return dartProcedure;
}
/// Lowers an invocation of `allowInterop<type>(foo)` to:
///
/// let #var = foo in
/// _isDartFunctionWrapped<type>(#var) ?
/// #var :
/// _wrapDartFunction<type>(#var, jsWrapperFunction(#var));
///
/// The use of two functions here is necessary because we do not allow
/// `WasmExternRef` to be an argument or return type for a tear off.
///
/// Note: _wrapDartFunction tracks wrapped Dart functions in a map. When
/// these Dart functions flow to JS, they are replaced by their wrappers. If
/// the wrapper should ever flow back into Dart then it will be replaced by
/// the original Dart function.
// TODO(srujzs): It looks like there's no more code that references this
// function anymore in dart2wasm. Should we delete this lowering and related
// code?
Expression allowInterop(StaticInvocation staticInvocation) {
final argument = staticInvocation.arguments.positional.single;
final type = argument.getStaticType(_staticTypeContext) as FunctionType;
final jsWrapperFunction = _getJSWrapperFunction(
staticInvocation.target, type,
boxExternRef: false);
final v = VariableDeclaration('#var',
initializer: argument, type: type, isSynthesized: true);
return Let(
v,
ConditionalExpression(
StaticInvocation(_util.isDartFunctionWrappedTarget,
Arguments([VariableGet(v)], types: [type])),
VariableGet(v),
StaticInvocation(
_util.wrapDartFunctionTarget,
Arguments([
VariableGet(v),
StaticInvocation(
jsWrapperFunction,
Arguments([
StaticInvocation(_util.jsObjectFromDartObjectTarget,
Arguments([VariableGet(v)]))
])),
], types: [
type
])),
type));
}
Expression _createJSValue(Expression value) =>
StaticInvocation(_util.jsValueBoxTarget, Arguments([value]));
/// Lowers an invocation of `<Function>.toJS` to:
///
/// JSValue(jsWrapperFunction(<Function>))
Expression functionToJS(StaticInvocation staticInvocation) {
final argument = staticInvocation.arguments.positional.single;
final type = argument.getStaticType(_staticTypeContext) as FunctionType;
final jsWrapperFunction = _getJSWrapperFunction(
staticInvocation.target, type,
boxExternRef: true);
return _createJSValue(StaticInvocation(
jsWrapperFunction,
Arguments([
StaticInvocation(
_util.jsObjectFromDartObjectTarget, Arguments([argument]))
])));
}
}