blob: 377721f8359b18b0e82a4663ae46872704f0e57a [file] [log] [blame]
// Copyright (c) 2019, 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.
library vm.transformations.ffi_use_sites;
import 'package:front_end/src/api_unstable/vm.dart'
show
messageFfiExceptionalReturnNull,
messageFfiExpectedConstant,
templateFfiDartTypeMismatch,
templateFfiEmptyStruct,
templateFfiExpectedExceptionalReturn,
templateFfiExpectedNoExceptionalReturn,
templateFfiExtendsOrImplementsSealedClass,
templateFfiNotStatic,
templateFfiTypeInvalid,
templateFfiTypeMismatch;
import 'package:kernel/ast.dart';
import 'package:kernel/class_hierarchy.dart' show ClassHierarchy;
import 'package:kernel/core_types.dart';
import 'package:kernel/library_index.dart' show LibraryIndex;
import 'package:kernel/reference_from_index.dart';
import 'package:kernel/target/targets.dart' show DiagnosticReporter;
import 'package:kernel/type_environment.dart';
import 'ffi.dart'
show
FfiTransformerData,
NativeType,
FfiTransformer,
nativeTypeSizes,
WORD_SIZE,
UNKNOWN,
wordSize;
/// Checks and replaces calls to dart:ffi struct fields and methods.
void transformLibraries(
Component component,
CoreTypes coreTypes,
ClassHierarchy hierarchy,
List<Library> libraries,
DiagnosticReporter diagnosticReporter,
FfiTransformerData ffiTransformerData,
ReferenceFromIndex referenceFromIndex) {
final index = new LibraryIndex(
component, ["dart:ffi", "dart:_internal", "dart:typed_data"]);
if (!index.containsLibrary("dart:ffi")) {
// TODO: This check doesn't make sense: "dart:ffi" is always loaded/created
// for the VM target.
// If dart:ffi is not loaded, do not do the transformation.
return;
}
if (index.tryGetClass('dart:ffi', 'NativeFunction') == null) {
// If dart:ffi is not loaded (for real): do not do the transformation.
return;
}
final transformer = new _FfiUseSiteTransformer(
index,
coreTypes,
hierarchy,
diagnosticReporter,
referenceFromIndex,
ffiTransformerData.replacedGetters,
ffiTransformerData.replacedSetters,
ffiTransformerData.emptyStructs);
libraries.forEach(transformer.visitLibrary);
}
/// Checks and replaces calls to dart:ffi struct fields and methods.
class _FfiUseSiteTransformer extends FfiTransformer {
final Map<Field, Procedure> replacedGetters;
final Map<Field, Procedure> replacedSetters;
final Set<Class> emptyStructs;
StaticTypeContext _staticTypeContext;
bool get isFfiLibrary => currentLibrary == ffiLibrary;
// Used to create private top-level fields with unique names for each
// callback.
int callbackCount = 0;
_FfiUseSiteTransformer(
LibraryIndex index,
CoreTypes coreTypes,
ClassHierarchy hierarchy,
DiagnosticReporter diagnosticReporter,
ReferenceFromIndex referenceFromIndex,
this.replacedGetters,
this.replacedSetters,
this.emptyStructs)
: super(index, coreTypes, hierarchy, diagnosticReporter,
referenceFromIndex) {}
@override
TreeNode visitLibrary(Library node) {
callbackCount = 0;
return super.visitLibrary(node);
}
@override
visitClass(Class node) {
try {
_ensureNotExtendsOrImplementsSealedClass(node);
return super.visitClass(node);
} on _FfiStaticTypeError {
// It's OK to swallow the exception because the diagnostics issued will
// cause compilation to fail. By continuing, we can report more
// diagnostics before compilation ends.
return super.visitClass(node);
}
}
@override
visitField(Field node) {
_staticTypeContext = new StaticTypeContext(node, env);
var result = super.visitField(node);
_staticTypeContext = null;
return result;
}
@override
visitConstructor(Constructor node) {
_staticTypeContext = new StaticTypeContext(node, env);
var result = super.visitConstructor(node);
_staticTypeContext = null;
return result;
}
@override
visitProcedure(Procedure node) {
if (isFfiLibrary && node.isExtensionMember) {
if (node == allocationTearoff ||
node == asFunctionTearoff ||
node == lookupFunctionTearoff) {
// Skip static checks and transformation for the tearoffs.
return node;
}
}
_staticTypeContext = new StaticTypeContext(node, env);
final result = super.visitProcedure(node);
_staticTypeContext = null;
return result;
}
@override
visitPropertyGet(PropertyGet node) {
super.visitPropertyGet(node);
final Procedure replacedWith = replacedGetters[node.interfaceTarget];
if (replacedWith != null) {
node = PropertyGet(node.receiver, replacedWith.name, replacedWith);
}
return node;
}
@override
visitPropertySet(PropertySet node) {
super.visitPropertySet(node);
final Procedure replacedWith = replacedSetters[node.interfaceTarget];
if (replacedWith != null) {
node = PropertySet(
node.receiver, replacedWith.name, node.value, replacedWith);
}
return node;
}
@override
visitStaticInvocation(StaticInvocation node) {
super.visitStaticInvocation(node);
final Member target = node.target;
try {
if (target == structPointerRef || target == structPointerElemAt) {
final DartType nativeType = node.arguments.types[0];
_ensureNativeTypeValid(nativeType, node, allowStructItself: false);
return _replaceRef(node);
} else if (target == structArrayElemAt) {
final DartType nativeType = node.arguments.types[0];
_ensureNativeTypeValid(nativeType, node, allowStructItself: false);
return _replaceRefArray(node);
} else if (target == sizeOfMethod) {
final DartType nativeType = node.arguments.types[0];
// TODO(http://dartbug.com/38721): Change this to an error after
// package:ffi is no longer using sizeOf generically.
if (!isFfiLibrary) {
_ensureNativeTypeValid(nativeType, node);
}
if (nativeType is InterfaceType) {
Expression inlineSizeOf = _inlineSizeOf(nativeType);
if (inlineSizeOf != null) {
return inlineSizeOf;
}
}
} else if (target == lookupFunctionMethod) {
final DartType nativeType = InterfaceType(
nativeFunctionClass, Nullability.legacy, [node.arguments.types[0]]);
final DartType dartType = node.arguments.types[1];
_ensureNativeTypeValid(nativeType, node);
_ensureNativeTypeToDartType(nativeType, dartType, node);
_ensureNoEmptyStructs(dartType, node);
return _replaceLookupFunction(node);
} else if (target == asFunctionMethod) {
final DartType dartType = node.arguments.types[1];
final DartType nativeType = InterfaceType(
nativeFunctionClass, Nullability.legacy, [node.arguments.types[0]]);
_ensureNativeTypeValid(nativeType, node);
_ensureNativeTypeToDartType(nativeType, dartType, node);
_ensureNoEmptyStructs(dartType, node);
final DartType nativeSignature =
(nativeType as InterfaceType).typeArguments[0];
// Inline function body to make all type arguments instatiated.
return StaticInvocation(
asFunctionInternal,
Arguments([node.arguments.positional[0]],
types: [dartType, nativeSignature]));
} else if (target == fromFunctionMethod) {
final DartType nativeType = InterfaceType(
nativeFunctionClass, Nullability.legacy, [node.arguments.types[0]]);
final Expression func = node.arguments.positional[0];
final DartType dartType = func.getStaticType(_staticTypeContext);
_ensureIsStaticFunction(func);
_ensureNativeTypeValid(nativeType, node);
_ensureNativeTypeToDartType(nativeType, dartType, node);
_ensureNoEmptyStructs(dartType, node);
// Check `exceptionalReturn`'s type.
final FunctionType funcType = dartType;
final Class expectedReturnClass =
((node.arguments.types[0] as FunctionType).returnType
as InterfaceType)
.classNode;
final NativeType expectedReturn = getType(expectedReturnClass);
if (expectedReturn == NativeType.kVoid ||
expectedReturn == NativeType.kPointer ||
expectedReturn == NativeType.kHandle ||
expectedReturnClass.superclass == structClass) {
if (node.arguments.positional.length > 1) {
diagnosticReporter.report(
templateFfiExpectedNoExceptionalReturn.withArguments(
funcType.returnType, currentLibrary.isNonNullableByDefault),
node.fileOffset,
1,
node.location.file);
return node;
}
node.arguments.positional.add(NullLiteral()..parent = node);
} else {
// The exceptional return value is not optional for other return
// types.
if (node.arguments.positional.length < 2) {
diagnosticReporter.report(
templateFfiExpectedExceptionalReturn.withArguments(
funcType.returnType, currentLibrary.isNonNullableByDefault),
node.fileOffset,
1,
node.location.file);
return node;
}
final Expression exceptionalReturn = node.arguments.positional[1];
// The exceptional return value must be a constant so that it be
// referenced by precompiled trampoline's object pool.
if (exceptionalReturn is! BasicLiteral &&
!(exceptionalReturn is ConstantExpression &&
exceptionalReturn.constant is PrimitiveConstant)) {
diagnosticReporter.report(messageFfiExpectedConstant,
node.fileOffset, 1, node.location.file);
return node;
}
// Moreover it may not be null.
if (exceptionalReturn is NullLiteral ||
(exceptionalReturn is ConstantExpression &&
exceptionalReturn.constant is NullConstant)) {
diagnosticReporter.report(messageFfiExceptionalReturnNull,
node.fileOffset, 1, node.location.file);
return node;
}
final DartType returnType =
exceptionalReturn.getStaticType(_staticTypeContext);
if (!env.isSubtypeOf(returnType, funcType.returnType,
SubtypeCheckMode.ignoringNullabilities)) {
diagnosticReporter.report(
templateFfiDartTypeMismatch.withArguments(returnType,
funcType.returnType, currentLibrary.isNonNullableByDefault),
exceptionalReturn.fileOffset,
1,
exceptionalReturn.location.file);
return node;
}
}
return _replaceFromFunction(node);
} else if (target == allocateMethod) {
final DartType nativeType = node.arguments.types[0];
_ensureNativeTypeValid(nativeType, node);
// TODO(http://dartbug.com/38721): Change this to an error.
if (nativeType is TypeParameterType) {
// Do not rewire generic invocations.
return node;
}
// Inline the body to get rid of a generic invocation of sizeOf.
// TODO(http://dartbug.com/39964): Add `allignmentOf<T>()` call.
Expression sizeInBytes = _inlineSizeOf(nativeType);
if (sizeInBytes != null) {
if (node.arguments.positional.length == 2) {
sizeInBytes = MethodInvocation(
node.arguments.positional[1],
numMultiplication.name,
Arguments([sizeInBytes]),
numMultiplication);
}
return MethodInvocation(
node.arguments.positional[0],
allocatorAllocateMethod.name,
Arguments([sizeInBytes], types: node.arguments.types),
allocatorAllocateMethod);
}
}
} on _FfiStaticTypeError {
// It's OK to swallow the exception because the diagnostics issued will
// cause compilation to fail. By continuing, we can report more
// diagnostics before compilation ends.
}
return node;
}
Expression _inlineSizeOf(InterfaceType nativeType) {
final Class nativeClass = nativeType.classNode;
final NativeType nt = getType(nativeClass);
if (nt == null) {
// User-defined structs.
Field sizeOfField = nativeClass.fields.single;
return StaticGet(sizeOfField);
}
final int size = nativeTypeSizes[nt.index];
if (size == WORD_SIZE) {
return runtimeBranchOnLayout(wordSize);
}
if (size != UNKNOWN) {
return ConstantExpression(
IntConstant(size),
InterfaceType(listClass, Nullability.legacy,
[InterfaceType(intClass, Nullability.legacy)]));
}
// Size unknown.
return null;
}
// We need to replace calls to 'DynamicLibrary.lookupFunction' with explicit
// Kernel, because we cannot have a generic call to 'asFunction' in its body.
//
// Above, in 'visitStaticInvocation', we ensure that the type arguments to
// 'lookupFunction' are constants, so by inlining the call to 'asFunction' at
// the call-site, we ensure that there are no generic calls to 'asFunction'.
Expression _replaceLookupFunction(StaticInvocation node) {
// The generated code looks like:
//
// _asFunctionInternal<DS, NS>(lookup<NativeFunction<NS>>(symbolName))
final DartType nativeSignature = node.arguments.types[0];
final DartType dartSignature = node.arguments.types[1];
final Arguments args = Arguments([
node.arguments.positional[1]
], types: [
InterfaceType(nativeFunctionClass, Nullability.legacy, [nativeSignature])
]);
final Expression lookupResult = MethodInvocation(
node.arguments.positional[0],
Name("lookup"),
args,
libraryLookupMethod);
return StaticInvocation(asFunctionInternal,
Arguments([lookupResult], types: [dartSignature, nativeSignature]));
}
// We need to rewrite calls to 'fromFunction' into two calls, representing the
// compile-time and run-time aspects of creating the closure:
//
// final dynamic _#ffiCallback0 = Pointer.fromFunction<T>(f, e) =>
// _pointerFromFunction<NativeFunction<T>>(
// _nativeCallbackFunction<T>(f, e));
//
// ... _#ffiCallback0 ...
//
// We must implement this as a Kernel rewrite because <T> must be a
// compile-time constant to any invocation of '_nativeCallbackFunction'.
//
// Creating this closure requires a runtime call, so we save the result in a
// synthetic top-level field to avoid recomputing it.
Expression _replaceFromFunction(StaticInvocation node) {
final nativeFunctionType = InterfaceType(
nativeFunctionClass, Nullability.legacy, node.arguments.types);
var name = Name("_#ffiCallback${callbackCount++}", currentLibrary);
var getterReference = currentLibraryIndex?.lookupGetterReference(name);
final Field field = Field.immutable(name,
type: InterfaceType(
pointerClass, Nullability.legacy, [nativeFunctionType]),
initializer: StaticInvocation(
pointerFromFunctionProcedure,
Arguments([
StaticInvocation(nativeCallbackFunctionProcedure, node.arguments)
], types: [
nativeFunctionType
])),
isStatic: true,
isFinal: true,
fileUri: currentLibrary.fileUri,
getterReference: getterReference)
..fileOffset = node.fileOffset;
currentLibrary.addField(field);
return StaticGet(field);
}
Expression _replaceRef(StaticInvocation node) {
final dartType = node.arguments.types[0];
final clazz = (dartType as InterfaceType).classNode;
final constructor = clazz.constructors
.firstWhere((c) => c.name == Name("#fromTypedDataBase"));
Expression pointer = NullCheck(node.arguments.positional[0]);
if (node.arguments.positional.length == 2) {
pointer = MethodInvocation(
pointer,
offsetByMethod.name,
Arguments([
MethodInvocation(
node.arguments.positional[1],
numMultiplication.name,
Arguments([_inlineSizeOf(dartType)]),
numMultiplication)
]),
offsetByMethod);
}
return ConstructorInvocation(constructor, Arguments([pointer]));
}
Expression _replaceRefArray(StaticInvocation node) {
final dartType = node.arguments.types[0];
final clazz = (dartType as InterfaceType).classNode;
final constructor = clazz.constructors
.firstWhere((c) => c.name == Name("#fromTypedDataBase"));
final typedDataBasePrime = typedDataBaseOffset(
PropertyGet(NullCheck(node.arguments.positional[0]),
cArrayTypedDataBaseField.name, cArrayTypedDataBaseField),
MethodInvocation(node.arguments.positional[1], numMultiplication.name,
Arguments([StaticGet(clazz.fields.single)]), numMultiplication),
StaticGet(clazz.fields.single),
dartType,
node.fileOffset);
return ConstructorInvocation(constructor, Arguments([typedDataBasePrime]));
}
@override
visitMethodInvocation(MethodInvocation node) {
super.visitMethodInvocation(node);
final Member target = node.interfaceTarget;
try {
if (target == elementAtMethod) {
final DartType pointerType =
node.receiver.getStaticType(_staticTypeContext);
final DartType nativeType = _pointerTypeGetTypeArg(pointerType);
_ensureNativeTypeValid(nativeType, node);
Expression inlineSizeOf = _inlineSizeOf(nativeType);
if (inlineSizeOf != null) {
// Generates `receiver.offsetBy(inlineSizeOfExpression)`.
return MethodInvocation(
node.receiver,
offsetByMethod.name,
Arguments([
MethodInvocation(
node.arguments.positional.single,
numMultiplication.name,
Arguments([inlineSizeOf]),
numMultiplication)
]),
offsetByMethod);
}
}
} on _FfiStaticTypeError {
// It's OK to swallow the exception because the diagnostics issued will
// cause compilation to fail. By continuing, we can report more
// diagnostics before compilation ends.
}
return node;
}
DartType _pointerTypeGetTypeArg(DartType pointerType) {
return pointerType is InterfaceType ? pointerType.typeArguments[0] : null;
}
void _ensureNativeTypeToDartType(
DartType nativeType, DartType dartType, Expression node,
{bool allowHandle: false}) {
final DartType correspondingDartType = convertNativeTypeToDartType(
nativeType,
allowStructs: true,
allowHandle: allowHandle);
if (dartType == correspondingDartType) return;
if (env.isSubtypeOf(correspondingDartType, dartType,
SubtypeCheckMode.ignoringNullabilities)) {
return;
}
diagnosticReporter.report(
templateFfiTypeMismatch.withArguments(dartType, correspondingDartType,
nativeType, currentLibrary.isNonNullableByDefault),
node.fileOffset,
1,
node.location.file);
throw _FfiStaticTypeError();
}
void _ensureNativeTypeValid(DartType nativeType, Expression node,
{bool allowHandle: false, bool allowStructItself = true}) {
if (!_nativeTypeValid(nativeType,
allowStructs: true,
allowStructItself: allowStructItself,
allowHandle: allowHandle)) {
diagnosticReporter.report(
templateFfiTypeInvalid.withArguments(
nativeType, currentLibrary.isNonNullableByDefault),
node.fileOffset,
1,
node.location.file);
throw _FfiStaticTypeError();
}
}
void _ensureNoEmptyStructs(DartType nativeType, Expression node) {
// Error on structs with no fields.
if (nativeType is InterfaceType) {
final Class nativeClass = nativeType.classNode;
if (hierarchy.isSubclassOf(nativeClass, structClass)) {
if (emptyStructs.contains(nativeClass)) {
diagnosticReporter.report(
templateFfiEmptyStruct.withArguments(nativeClass.name),
node.fileOffset,
1,
node.location.file);
}
}
}
// Recurse when seeing a function type.
if (nativeType is FunctionType) {
nativeType.positionalParameters
.forEach((e) => _ensureNoEmptyStructs(e, node));
_ensureNoEmptyStructs(nativeType.returnType, node);
}
}
/// The Dart type system does not enforce that NativeFunction return and
/// parameter types are only NativeTypes, so we need to check this.
bool _nativeTypeValid(DartType nativeType,
{bool allowStructs: false,
bool allowStructItself = false,
bool allowHandle = false}) {
return convertNativeTypeToDartType(nativeType,
allowStructs: allowStructs,
allowStructItself: allowStructItself,
allowHandle: allowHandle) !=
null;
}
void _ensureIsStaticFunction(Expression node) {
if ((node is StaticGet && node.target is Procedure) ||
(node is ConstantExpression && node.constant is TearOffConstant)) {
return;
}
diagnosticReporter.report(
templateFfiNotStatic.withArguments(fromFunctionMethod.name.text),
node.fileOffset,
1,
node.location.file);
throw _FfiStaticTypeError();
}
Class _extendsOrImplementsSealedClass(Class klass) {
final Class superClass = klass.supertype?.classNode;
// The Opaque and Struct classes can be extended, but subclasses
// cannot be (nor implemented).
if (klass != opaqueClass &&
klass != structClass &&
(hierarchy.isSubtypeOf(klass, opaqueClass) ||
hierarchy.isSubtypeOf(klass, structClass))) {
return superClass != opaqueClass && superClass != structClass
? superClass
: null;
}
if (!nativeTypesClasses.contains(klass) && klass != cArrayClass) {
for (final parent in nativeTypesClasses) {
if (hierarchy.isSubtypeOf(klass, parent)) {
return parent;
}
}
}
return null;
}
void _ensureNotExtendsOrImplementsSealedClass(Class klass) {
final Class extended = _extendsOrImplementsSealedClass(klass);
if (extended != null) {
diagnosticReporter.report(
templateFfiExtendsOrImplementsSealedClass
.withArguments(extended.name),
klass.fileOffset,
1,
klass.location.file);
throw _FfiStaticTypeError();
}
}
}
/// Used internally for abnormal control flow to prevent cascading error
/// messages.
class _FfiStaticTypeError implements Exception {}