Skip to content

Commit 3e7cda8

Browse files
dcharkescommit-bot@chromium.org
authored andcommitted
[vm/ffi] Support passing structs by value
This CL adds passing structs by value in FFI trampolines. Nested structs and inline arrays are future work. C defines passing empty structs as undefined behavior, so that is not supported in this CL. Suggested review order: 1) commit message 2) ffi/marshaller (decisions for what is done in IL and what in MC) 3) frontend/kernel_to_il (IL construction) 4) backend/il (MC generation from IL) 5) rest in VM Overall architecture is that structs are split up into word-size chunks in IL when this is possible: 1 definition in IL per chunk, 1 Location in IL per chunk, and 1 NativeLocation for the backend per chunk. In some cases it is not possible or less convenient to split into chunks. In these cases TypedDataBase objects are stored into and loaded from directly in machine code. The various cases: - FFI call arguments which are not passed as pointers: pass individual chunks to FFI call which already have the right location. - FFI call arguments which are passed as pointers: Pass in TypedDataBase to FFI call, allocate space on the stack, and make a copy on the stack and pass the copies' address to the callee. - FFI call return value: pass in TypedData to FFI call, and copy result in machine code. - FFI callback arguments which are not passed as pointers: IL definition for each chunk, and populate a new TypedData with those chunks. - FFI callback arguments which are passed as pointer: IL definition for the pointer, and copying of contents in IL. - FFI return value when location is pointer: Copy data to callee result location in IL. - FFI return value when location is not a pointer: Copy data in machine code to the right registers. Some other notes about the implementation: - Due to Store/LoadIndexed loading doubles from float arrays, we use a int32 instead and use the BitCastInstr. - Linux ia32 uses `ret 4` when returning structs by value. This requires special casing in the FFI callback trampolines to either use `ret` or `ret 4` when returning. - The 1 IL definition, 1 Location, and 1 NativeLocation approach does not remove the need for special casing PairLocations in the machine code generation because they are 1 Location belonging to 1 definition. Because of the amount of corner cases in the calling conventions that need to be covered, the tests are generated, rather than hand-written. ABIs tested on CQ: x64 (Linux, MacOS, Windows), ia32 (Linux, Windows), arm (Android softFP, Linux hardFP), arm64 Android. ABIs tested locally through Flutter: ia32 Android (emulator), x64 iOS (simulator), arm64 iOS. ABIs not tested: arm iOS. TEST=runtime/bin/ffi_test/ffi_test_functions_generated.cc TEST=runtime/bin/ffi_test/ffi_test_functions.cc TEST=tests/{ffi,ffi_2}/function_structs_by_value_generated_test.dart TEST=tests/{ffi,ffi_2}/function_callbacks_structs_by_value_generated_tes TEST=tests/{ffi,ffi_2}/function_callbacks_structs_by_value_test.dart TEST=tests/{ffi,ffi_2}/vmspecific_static_checks_test.dart Closes #36730. Change-Id: I474d3a4ee1faadbe767ddadd1b696e24d8dc364c Cq-Include-Trybots: luci.dart.try:dart-sdk-linux-try,dart-sdk-mac-try,dart-sdk-win-try,vm-ffi-android-debug-arm-try,vm-ffi-android-debug-arm64-try,vm-kernel-asan-linux-release-x64-try,vm-kernel-mac-debug-x64-try,vm-kernel-linux-debug-ia32-try,vm-kernel-linux-debug-x64-try,vm-kernel-nnbd-linux-debug-x64-try,vm-kernel-nnbd-linux-debug-ia32-try,vm-kernel-nnbd-mac-release-x64-try,vm-kernel-nnbd-win-debug-x64-try,vm-kernel-precomp-linux-debug-x64-try,vm-kernel-precomp-linux-debug-simarm_x64-try,vm-kernel-precomp-nnbd-linux-debug-x64-try,vm-kernel-precomp-win-release-x64-try,vm-kernel-reload-linux-debug-x64-try,vm-kernel-reload-rollback-linux-debug-x64-try,vm-kernel-win-debug-x64-try,vm-kernel-win-debug-ia32-try,vm-precomp-ffi-qemu-linux-release-arm-try,vm-kernel-precomp-obfuscate-linux-release-x64-try,vm-kernel-msan-linux-release-x64-try,vm-kernel-precomp-msan-linux-release-x64-try,vm-kernel-precomp-android-release-arm_x64-try,analyzer-analysis-server-linux-try Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/140290 Commit-Queue: Daco Harkes <[email protected]> Reviewed-by: Martin Kustermann <[email protected]>
1 parent f6c6ea5 commit 3e7cda8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1936
-269
lines changed

pkg/_fe_analyzer_shared/lib/src/messages/codes_generated.dart

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3789,6 +3789,31 @@ const MessageCode messageFastaUsageShort =
37893789
-o <file> Generate the output into <file>.
37903790
-h Display this message (add -v for information about all options).""");
37913791

3792+
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
3793+
const Template<
3794+
Message Function(String name)> templateFfiEmptyStruct = const Template<
3795+
Message Function(String name)>(
3796+
messageTemplate:
3797+
r"""Struct '#name' is empty. Empty structs are undefined behavior.""",
3798+
withArguments: _withArgumentsFfiEmptyStruct);
3799+
3800+
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
3801+
const Code<Message Function(String name)> codeFfiEmptyStruct =
3802+
const Code<Message Function(String name)>(
3803+
"FfiEmptyStruct",
3804+
templateFfiEmptyStruct,
3805+
);
3806+
3807+
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
3808+
Message _withArgumentsFfiEmptyStruct(String name) {
3809+
if (name.isEmpty) throw 'No name provided';
3810+
name = demangleMixinApplicationName(name);
3811+
return new Message(codeFfiEmptyStruct,
3812+
message:
3813+
"""Struct '${name}' is empty. Empty structs are undefined behavior.""",
3814+
arguments: {'name': name});
3815+
}
3816+
37923817
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
37933818
const Code<Null> codeFfiExceptionalReturnNull = messageFfiExceptionalReturnNull;
37943819

pkg/analyzer/lib/src/generated/ffi_verifier.dart

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,22 @@ class FfiVerifier extends RecursiveAstVisitor<void> {
163163
element.name == 'DynamicLibraryExtension' &&
164164
element.library.name == 'dart.ffi';
165165

166+
bool _isEmptyStruct(ClassElement classElement) {
167+
final fields = classElement.fields;
168+
var structFieldCount = 0;
169+
for (final field in fields) {
170+
final declaredType = field.type;
171+
if (declaredType.isDartCoreInt) {
172+
structFieldCount++;
173+
} else if (declaredType.isDartCoreDouble) {
174+
structFieldCount++;
175+
} else if (_isPointer(declaredType.element)) {
176+
structFieldCount++;
177+
}
178+
}
179+
return structFieldCount == 0;
180+
}
181+
166182
bool _isHandle(Element element) =>
167183
element.name == 'Handle' && element.library.name == 'dart.ffi';
168184

@@ -246,12 +262,12 @@ class FfiVerifier extends RecursiveAstVisitor<void> {
246262
nativeType.optionalParameterTypes.isNotEmpty) {
247263
return false;
248264
}
249-
if (!_isValidFfiNativeType(nativeType.returnType, true)) {
265+
if (!_isValidFfiNativeType(nativeType.returnType, true, false)) {
250266
return false;
251267
}
252268

253-
for (final DartType typeArg in nativeType.typeArguments) {
254-
if (!_isValidFfiNativeType(typeArg, false)) {
269+
for (final DartType typeArg in nativeType.normalParameterTypes) {
270+
if (!_isValidFfiNativeType(typeArg, false, false)) {
255271
return false;
256272
}
257273
}
@@ -261,7 +277,8 @@ class FfiVerifier extends RecursiveAstVisitor<void> {
261277
}
262278

263279
/// Validates that the given [nativeType] is a valid dart:ffi native type.
264-
bool _isValidFfiNativeType(DartType nativeType, bool allowVoid) {
280+
bool _isValidFfiNativeType(
281+
DartType nativeType, bool allowVoid, bool allowEmptyStruct) {
265282
if (nativeType is InterfaceType) {
266283
// Is it a primitive integer/double type (or ffi.Void if we allow it).
267284
final primitiveType = _primitiveNativeType(nativeType);
@@ -274,10 +291,20 @@ class FfiVerifier extends RecursiveAstVisitor<void> {
274291
}
275292
if (_isPointerInterfaceType(nativeType)) {
276293
final nativeArgumentType = nativeType.typeArguments.single;
277-
return _isValidFfiNativeType(nativeArgumentType, true) ||
294+
return _isValidFfiNativeType(nativeArgumentType, true, true) ||
278295
_isStructClass(nativeArgumentType) ||
279296
_isNativeTypeInterfaceType(nativeArgumentType);
280297
}
298+
if (_isStructClass(nativeType)) {
299+
if (!allowEmptyStruct) {
300+
if (_isEmptyStruct(nativeType.element)) {
301+
// TODO(dartbug.com/36780): This results in an error message not
302+
// mentioning empty structs at all.
303+
return false;
304+
}
305+
}
306+
return true;
307+
}
281308
} else if (nativeType is FunctionType) {
282309
return _isValidFfiNativeFunctionType(nativeType);
283310
}
@@ -533,7 +560,8 @@ class FfiVerifier extends RecursiveAstVisitor<void> {
533560
final DartType R = (T as FunctionType).returnType;
534561
if ((FT as FunctionType).returnType.isVoid ||
535562
_isPointer(R.element) ||
536-
_isHandle(R.element)) {
563+
_isHandle(R.element) ||
564+
_isStructClass(R)) {
537565
if (argCount != 1) {
538566
_errorReporter.reportErrorForNode(
539567
FfiCode.INVALID_EXCEPTION_VALUE, node.argumentList.arguments[1]);

pkg/front_end/lib/src/api_unstable/vm.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export '../fasta/fasta_codes.dart'
4747
messageFfiExpectedConstant,
4848
noLength,
4949
templateFfiDartTypeMismatch,
50+
templateFfiEmptyStruct,
5051
templateFfiExpectedExceptionalReturn,
5152
templateFfiExpectedNoExceptionalReturn,
5253
templateFfiExtendsOrImplementsSealedClass,

pkg/front_end/messages.status

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,7 @@ FastaUsageLong/example: Fail
315315
FastaUsageShort/analyzerCode: Fail
316316
FastaUsageShort/example: Fail
317317
FfiDartTypeMismatch/analyzerCode: Fail
318+
FfiEmptyStruct/analyzerCode: Fail
318319
FfiExceptionalReturnNull/analyzerCode: Fail
319320
FfiExpectedConstant/analyzerCode: Fail
320321
FfiExpectedExceptionalReturn/analyzerCode: Fail

pkg/front_end/messages.yaml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1914,7 +1914,7 @@ InternalProblemUnsupportedNullability:
19141914

19151915
IncrementalCompilerIllegalParameter:
19161916
template: "Illegal parameter name '#string' found during expression compilation."
1917-
1917+
19181918
IncrementalCompilerIllegalTypeParameter:
19191919
template: "Illegal type parameter name '#string' found during expression compilation."
19201920

@@ -4233,6 +4233,11 @@ FfiTypeMismatch:
42334233
template: "Expected type '#type' to be '#type2', which is the Dart type corresponding to '#type3'."
42344234
external: test/ffi_test.dart
42354235

4236+
FfiEmptyStruct:
4237+
# Used by dart:ffi
4238+
template: "Struct '#name' is empty. Empty structs are undefined behavior."
4239+
external: test/ffi_test.dart
4240+
42364241
FfiTypeInvalid:
42374242
# Used by dart:ffi
42384243
template: "Expected type '#type' to be a valid and instantiated subtype of 'NativeType'."

pkg/vm/lib/target/vm.dart

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import 'package:kernel/vm/constants_native_effects.dart'
2222

2323
import '../transformations/call_site_annotator.dart' as callSiteAnnotator;
2424
import '../transformations/lowering.dart' as lowering show transformLibraries;
25-
import '../transformations/ffi.dart' as transformFfi show ReplacedMembers;
2625
import '../transformations/ffi_definitions.dart' as transformFfiDefinitions
2726
show transformLibraries;
2827
import '../transformations/ffi_use_sites.dart' as transformFfiUseSites
@@ -155,17 +154,16 @@ class VmTarget extends Target {
155154
this, coreTypes, hierarchy, libraries, referenceFromIndex);
156155
logger?.call("Transformed mixin applications");
157156

158-
transformFfi.ReplacedMembers replacedFields =
159-
transformFfiDefinitions.transformLibraries(
160-
component,
161-
coreTypes,
162-
hierarchy,
163-
libraries,
164-
diagnosticReporter,
165-
referenceFromIndex,
166-
changedStructureNotifier);
157+
final ffiTransformerData = transformFfiDefinitions.transformLibraries(
158+
component,
159+
coreTypes,
160+
hierarchy,
161+
libraries,
162+
diagnosticReporter,
163+
referenceFromIndex,
164+
changedStructureNotifier);
167165
transformFfiUseSites.transformLibraries(component, coreTypes, hierarchy,
168-
libraries, diagnosticReporter, replacedFields, referenceFromIndex);
166+
libraries, diagnosticReporter, ffiTransformerData, referenceFromIndex);
169167
logger?.call("Transformed ffi annotations");
170168

171169
// TODO(kmillikin): Make this run on a per-method basis.

pkg/vm/lib/transformations/ffi.dart

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -310,8 +310,8 @@ class FfiTransformer extends Transformer {
310310
/// [Handle] -> [Object]
311311
/// [NativeFunction]<T1 Function(T2, T3) -> S1 Function(S2, S3)
312312
/// where DartRepresentationOf(Tn) -> Sn
313-
DartType convertNativeTypeToDartType(
314-
DartType nativeType, bool allowStructs, bool allowHandle) {
313+
DartType convertNativeTypeToDartType(DartType nativeType,
314+
{bool allowStructs = false, bool allowHandle = false}) {
315315
if (nativeType is! InterfaceType) {
316316
return null;
317317
}
@@ -352,13 +352,13 @@ class FfiTransformer extends Transformer {
352352
return null;
353353
}
354354
if (fun.typeParameters.length != 0) return null;
355-
// TODO(36730): Structs cannot appear in native function signatures.
356-
final DartType returnType = convertNativeTypeToDartType(
357-
fun.returnType, /*allowStructs=*/ false, /*allowHandle=*/ true);
355+
356+
final DartType returnType = convertNativeTypeToDartType(fun.returnType,
357+
allowStructs: allowStructs, allowHandle: true);
358358
if (returnType == null) return null;
359359
final List<DartType> argumentTypes = fun.positionalParameters
360-
.map((t) => convertNativeTypeToDartType(
361-
t, /*allowStructs=*/ false, /*allowHandle=*/ true))
360+
.map((t) => convertNativeTypeToDartType(t,
361+
allowStructs: allowStructs, allowHandle: true))
362362
.toList();
363363
if (argumentTypes.contains(null)) return null;
364364
return FunctionType(argumentTypes, returnType, Nullability.legacy);
@@ -373,12 +373,12 @@ class FfiTransformer extends Transformer {
373373
}
374374
}
375375

376-
/// Contains replaced members, of which all the call sites need to be replaced.
377-
///
378-
/// [ReplacedMembers] is populated by _FfiDefinitionTransformer and consumed by
379-
/// _FfiUseSiteTransformer.
380-
class ReplacedMembers {
376+
/// Contains all information collected by _FfiDefinitionTransformer that is
377+
/// needed in _FfiUseSiteTransformer.
378+
class FfiTransformerData {
381379
final Map<Field, Procedure> replacedGetters;
382380
final Map<Field, Procedure> replacedSetters;
383-
ReplacedMembers(this.replacedGetters, this.replacedSetters);
381+
final Set<Class> emptyStructs;
382+
FfiTransformerData(
383+
this.replacedGetters, this.replacedSetters, this.emptyStructs);
384384
}

pkg/vm/lib/transformations/ffi_definitions.dart

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ import 'ffi.dart';
5656
///
5757
/// static final int #sizeOf = 24;
5858
/// }
59-
ReplacedMembers transformLibraries(
59+
FfiTransformerData transformLibraries(
6060
Component component,
6161
CoreTypes coreTypes,
6262
ClassHierarchy hierarchy,
@@ -70,17 +70,17 @@ ReplacedMembers transformLibraries(
7070
// TODO: This check doesn't make sense: "dart:ffi" is always loaded/created
7171
// for the VM target.
7272
// If dart:ffi is not loaded, do not do the transformation.
73-
return ReplacedMembers({}, {});
73+
return FfiTransformerData({}, {}, {});
7474
}
7575
if (index.tryGetClass('dart:ffi', 'NativeFunction') == null) {
7676
// If dart:ffi is not loaded (for real): do not do the transformation.
77-
return ReplacedMembers({}, {});
77+
return FfiTransformerData({}, {}, {});
7878
}
7979
final transformer = new _FfiDefinitionTransformer(index, coreTypes, hierarchy,
8080
diagnosticReporter, referenceFromIndex, changedStructureNotifier);
8181
libraries.forEach(transformer.visitLibrary);
82-
return ReplacedMembers(
83-
transformer.replacedGetters, transformer.replacedSetters);
82+
return FfiTransformerData(transformer.replacedGetters,
83+
transformer.replacedSetters, transformer.emptyStructs);
8484
}
8585

8686
/// Checks and elaborates the dart:ffi structs and fields.
@@ -89,6 +89,7 @@ class _FfiDefinitionTransformer extends FfiTransformer {
8989

9090
Map<Field, Procedure> replacedGetters = {};
9191
Map<Field, Procedure> replacedSetters = {};
92+
Set<Class> emptyStructs = {};
9293

9394
ChangedStructureNotifier changedStructureNotifier;
9495

@@ -231,7 +232,9 @@ class _FfiDefinitionTransformer extends FfiTransformer {
231232
Nullability.legacy);
232233
// TODO(dartbug.com/37271): Support structs inside structs.
233234
final DartType shouldBeDartType = convertNativeTypeToDartType(
234-
nativeType, /*allowStructs=*/ false, /*allowHandle=*/ false);
235+
nativeType,
236+
allowStructs: false,
237+
allowHandle: false);
235238
if (shouldBeDartType == null ||
236239
!env.isSubtypeOf(type, shouldBeDartType,
237240
SubtypeCheckMode.ignoringNullabilities)) {
@@ -338,6 +341,9 @@ class _FfiDefinitionTransformer extends FfiTransformer {
338341
}
339342

340343
_annoteStructWithFields(node, classes);
344+
if (classes.isEmpty) {
345+
emptyStructs.add(node);
346+
}
341347

342348
final sizeAndOffsets = <Abi, SizeAndOffsets>{};
343349
for (final Abi abi in Abi.values) {
@@ -565,6 +571,9 @@ class _FfiDefinitionTransformer extends FfiTransformer {
565571
return offset;
566572
}
567573

574+
// Keep consistent with runtime/vm/compiler/ffi/native_type.cc
575+
// NativeCompoundType::FromNativeTypes.
576+
//
568577
// TODO(37271): Support nested structs.
569578
SizeAndOffsets _calculateSizeAndOffsets(List<NativeType> types, Abi abi) {
570579
int offset = 0;

0 commit comments

Comments
 (0)