blob: 9de2690ba88caeb87b46790a48fa8efca29da0b9 [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);
final nullability =
callbackParameterType.extensionTypeErasure.nullability;
// Null-check if we can tell the nullability. If we can't, the cast
// closure handles the cast.
if (nullability == Nullability.nonNullable) {
expression = NullCheck(expression);
}
} else {
expression = _util.convertAndCast(
callbackParameterType, invokeOneArg(_util.dartifyRawTarget, v));
}
callbackArguments.add(expression);
}
final callExpr = FunctionInvocation(FunctionAccessKind.FunctionType,
VariableGet(callbackVariable), Arguments(callbackArguments),
functionType: null);
final temp = VariableDeclaration(null,
initializer: callExpr,
type: callExpr.getStaticType(_staticTypeContext),
isSynthesized: true);
final jsified = jsifyValue(temp, _util, _staticTypeContext.typeEnvironment);
return ReturnStatement(Let(temp, jsified));
}
/// 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, then a "cast closure" if needed, followed by all of the arguments
/// to the Dart callback as JS objects. Depending on [boxExternRef], 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. [node] is the conversion function that was called to
/// convert the callback.
///
/// Returns the created trampoline [Procedure].
Procedure _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. The third argument is a cast closure if
// needed.
final callbackVariable = VariableDeclaration('callback',
type: _util.nonNullableObjectType, isSynthesized: true);
final argumentsLength = VariableDeclaration('argumentsLength',
type: _util.coreTypes.doubleNonNullableRawType, isSynthesized: true);
final castClosure = VariableDeclaration('castClosure',
type: _util.nonNullableObjectType, isSynthesized: true);
// Initialize variable declarations.
List<VariableDeclaration> positionalParameters = [];
List<Expression> castClosureArguments = [];
final positionalParametersLength = function.positionalParameters.length;
for (int i = 0; i < positionalParametersLength; i++) {
final parameter = VariableDeclaration('x${i + 1}',
type: _util.nullableWasmExternRefType, isSynthesized: true);
positionalParameters.add(parameter);
if (_needCastClosure(function.positionalParameters[i])) {
castClosureArguments.add(_createJSValue(VariableGet(parameter)));
}
}
// 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> body = [];
if (castClosureArguments.isNotEmpty) {
// Call the cast closure, but only if the arity is okay. In the case where
// the arity is not sufficient, we end up coercing `undefined` to `null`,
// which may result in a type error in the cast closure rather than an
// arity error later.
body.add(IfStatement(
_util.variableGreaterThanOrEqualToConstant(
argumentsLength, IntConstant(function.requiredParameterCount)),
ExpressionStatement(FunctionInvocation(
FunctionAccessKind.FunctionType,
VariableGet(castClosure),
Arguments(castClosureArguments),
functionType: null)),
null));
}
// If more arguments were passed than there are parameters, ignore the extra
// arguments.
body.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--) {
body.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.
body.add(ExpressionStatement(Throw(StringConcatenation([
StringLiteral('Too few arguments passed. '
'Expected ${function.requiredParameterCount} or more, got '),
invokeMethod(argumentsLength, _util.numToIntTarget),
StringLiteral(' instead.')
]))));
Statement functionTrampolineBody = Block(body);
// 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();
return _methodCollector.addInteropProcedure(
functionTrampolineName,
functionTrampolineName,
FunctionNode(functionTrampolineBody,
positionalParameters: [
callbackVariable,
argumentsLength,
if (castClosureArguments.isNotEmpty) castClosure,
...positionalParameters
],
returnType: _util.nullableWasmExternRefType)
..fileOffset = node.fileOffset,
node.fileUri,
AnnotationType.weakExport,
isExternal: false);
}
/// Create a [Procedure] that will wrap a Dart callback in a JS wrapper.
///
/// [node] is the conversion function that is called by the user (either
/// `allowInterop`, `Function.toJS`, or `Function.toJSCaptureThis`). [type] is
/// the static type of the callback. [boxExternRef] determines if the
/// trampoline should box the arguments and return value or convert every
/// value. [needsCastClosure] determines if a cast closure is needed in order
/// to validate the types of some arguments. [captureThis] determines if
/// `this` needs to be passed into the trampoline from the JS wrapper.
///
/// The procedure will call a JS method that will create a wrapper, cache the
/// callback, and call the trampoline function with the callback, the JS
/// function's arguments' length, the cast closure if needed, and the JS
/// function's arguments as arguments.
///
/// Returns the created JS wrapper [Procedure] which will call out to JS
/// and the trampoline [Procedure] which will be invoked by the JS code.
(Procedure, Procedure) _getJSWrapperFunction(
Procedure node, FunctionType type,
{required bool boxExternRef,
required bool needsCastClosure,
required bool captureThis}) {
final functionTrampoline =
_createFunctionTrampoline(node, type, boxExternRef: boxExternRef);
List<String> jsParameters = [];
var jsParametersLength = type.positionalParameters.length;
if (captureThis) jsParametersLength--;
for (int i = 0; i < jsParametersLength; i++) {
jsParameters.add('x$i');
}
String jsWrapperParams = jsParameters.join(',');
// We could avoid incrementing the arguments length in the case of
// `captureThis` and have the function trampoline account for the extra
// argument, but there's no benefit in doing that.
String argumentsLength =
captureThis ? 'arguments.length + 1' : 'arguments.length';
String dartArguments = 'f,$argumentsLength';
String jsMethodParams = 'f';
if (needsCastClosure) {
dartArguments = '$dartArguments,castClosure';
jsMethodParams = '($jsMethodParams,castClosure)';
}
if (captureThis) dartArguments = '$dartArguments,this';
if (jsParameters.isNotEmpty) {
dartArguments = '$dartArguments,$jsWrapperParams';
}
// Create Dart procedure stub.
final jsMethodName = functionTrampoline.name.text;
Procedure dartProcedure = _methodCollector.addInteropProcedure(
'|$jsMethodName',
'dart2wasm.$jsMethodName',
FunctionNode(null,
positionalParameters: [
VariableDeclaration('dartFunction',
type: _util.nonNullableWasmExternRefType,
isSynthesized: true),
if (needsCastClosure)
VariableDeclaration('castClosure',
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,
"$jsMethodParams => finalizeWrapper(f, function($jsWrapperParams) {"
" return dartInstance.exports.${functionTrampoline.name.text}($dartArguments) "
"})");
return (dartProcedure, functionTrampoline);
}
/// 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, exportedFunction) = _getJSWrapperFunction(
staticInvocation.target, type,
boxExternRef: false, needsCastClosure: false, captureThis: 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),
BlockExpression(
Block([
// This ensures TFA will retain the function which the
// JS code will call. The backend in return will export
// the function due to `@pragma('wasm:weak-export', ...)`
ExpressionStatement(StaticInvocation(
_util.exportWasmFunctionTarget,
Arguments([
ConstantExpression(
StaticTearOffConstant(exportedFunction))
])))
]),
StaticInvocation(
jsWrapperFunction,
Arguments([
StaticInvocation(_util.jsObjectFromDartObjectTarget,
Arguments([VariableGet(v)]))
]))),
], types: [
type
])),
type));
}
Expression _createJSValue(Expression value) =>
StaticInvocation(_util.jsValueBoxTarget, Arguments([value]));
/// Whether a closure is needed to capture [type] so that the arguments to the
/// callback can be casted to that [type].
///
/// This includes the case where the parameters have type parameters for
/// types. The casts can't be done in the trampoline as the type parameters
/// aren't in scope.
bool _needCastClosure(DartType type) {
if (type is TypeParameterType || type is StructuralParameterType) {
assert(_util.isJSValueType(type));
return true;
}
return false;
}
/// Creates a cast closure given the callback's [functionType].
///
/// The cast closure accepts the boxed parameters which need to be casted in
/// this closure, and then casts them to the captured types.
///
/// Returns the cast closure if needed. Otherwise, returns `null`.
FunctionExpression? _createCastClosure(FunctionType functionType) {
final positionalParameters = functionType.positionalParameters;
List<VariableDeclaration> castClosureParameters = [];
List<Statement> casts = [];
for (int i = 0; i < positionalParameters.length; i++) {
final type = positionalParameters[i];
if (_needCastClosure(type)) {
final parameter = VariableDeclaration('x${i + 1}',
type: _util.nullableJSValueType, isSynthesized: true);
castClosureParameters.add(parameter);
casts.add(
ExpressionStatement(AsExpression(VariableGet(parameter), type)));
}
}
return castClosureParameters.isEmpty
? null
: FunctionExpression(FunctionNode(Block(casts),
positionalParameters: castClosureParameters,
returnType: VoidType()));
}
/// Given an invocation of `Function.toJS`, returns an [Expression]
/// representing:
///
/// JSValue(jsWrapperFunction(<Function>))
///
/// or if a cast closure is needed:
///
/// JSValue(jsWrapperFunction(<Function>, <CastClosure>))
///
/// If [captureThis] is true, this is assumed to be an invocation of
/// `Function.toJSCaptureThis`.
Expression functionToJS(StaticInvocation staticInvocation,
{bool captureThis = false}) {
final argument = staticInvocation.arguments.positional.single;
final type = argument.getStaticType(_staticTypeContext) as FunctionType;
final castClosure = _createCastClosure(type);
final (jsWrapperFunction, exportedFunction) = _getJSWrapperFunction(
staticInvocation.target, type,
boxExternRef: true,
needsCastClosure: castClosure != null,
captureThis: captureThis);
return _createJSValue(BlockExpression(
Block([
// This ensures TFA will retain the function which the
// JS code will call. The backend in return will export
// the function due to `@pragma('wasm:weak-export', ...)`
ExpressionStatement(StaticInvocation(
_util.exportWasmFunctionTarget,
Arguments([
ConstantExpression(StaticTearOffConstant(exportedFunction))
])))
]),
StaticInvocation(
jsWrapperFunction,
Arguments([
StaticInvocation(
_util.jsObjectFromDartObjectTarget, Arguments([argument])),
if (castClosure != null)
StaticInvocation(_util.jsObjectFromDartObjectTarget,
Arguments([castClosure]))
]))));
}
}