blob: 077a9e36e43dc0997f152511175f69bbc6379a38 [file] [log] [blame]
// Copyright (c) 2020, 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 'dart:ffi';
import 'dart:io';
import 'c_types.dart';
import 'structs_by_value_tests_configuration.dart';
import 'utils.dart';
/// The test type determines how to convert the arguments into return values
/// such that the caller knows what to check.
enum TestType {
/// Tested by getting all the individual fields out of the structs and
/// summing their values.
structArguments,
/// Tested by passing assigning the arguments to the struct fields.
structReturn,
/// Tested by returning the struct passed in.
structReturnArgument,
}
extension on FunctionType {
TestType get testType {
if (arguments.containsComposites && returnValue is FundamentalType) {
return TestType.structArguments;
}
if (returnValue is CompositeType && argumentTypes.contains(returnValue)) {
return TestType.structReturnArgument;
}
if (returnValue is StructType) {
if (arguments.length == (returnValue as CompositeType).members.length) {
return TestType.structReturn;
}
}
if (returnValue is UnionType) {
if (arguments.length == 1) {
return TestType.structReturn;
}
}
// No structs, sum the arguments as well.
return TestType.structArguments;
}
}
/// We use the class structure as an algebraic data type in order to keep the
/// relevant parts of the code generation closer together.
extension on CType {
/// The part of the cout expression after `std::cout` and before the `;`.
String coutExpression(String variableName) {
switch (this.runtimeType) {
case FundamentalType:
if (this == uint8 || this == int8) {
return "<< static_cast<int>($variableName)";
}
return "<< $variableName";
case StructType:
final this_ = this as StructType;
return this_.members.coutExpression("$variableName.");
case PointerType:
final this_ = this as PointerType;
final pointerTo = this_.pointerTo;
switch (pointerTo) {
case StructType _:
return pointerTo.members.coutExpression("$variableName->");
}
case UnionType:
final this_ = this as UnionType;
return this_.members.take(1).toList().coutExpression("$variableName.");
case FixedLengthArrayType:
final this_ = this as FixedLengthArrayType;
final indices = [for (var i = 0; i < this_.length; i += 1) i];
String result = '<< "["';
result += indices
.map((i) => this_.elementType.coutExpression("$variableName[$i]"))
.join('<< ", "');
result += '<< "]"';
return result.trimCouts();
}
throw Exception("Not implemented for ${this.runtimeType}");
}
/// A statement recursively outputting all members.
String coutStatement(String variableName) {
final coutExpr = this.coutExpression(variableName);
return 'std::cout << "$variableName = " $coutExpr << "\\n";';
}
}
extension on List<Member> {
/// The part of the cout expression after `std::cout` and before the `;`.
String coutExpression([String namePrefix = ""]) {
String result = '<< "("';
result += this
.map((m) => m.type.coutExpression("$namePrefix${m.name}"))
.join('<< ", "');
result += '<< ")"';
return result.trimCouts();
}
}
extension on CType {
/// A list of statements adding all members recursively to `result`.
///
/// Both valid in Dart and C.
String addToResultStatements(String variableName, bool isDart) {
switch (this.runtimeType) {
case FundamentalType:
final this_ = this as FundamentalType;
final boolToInt = this_.isBool ? ' ? 1 : 0' : '';
return " result += $variableName$boolToInt;\n";
case StructType:
final this_ = this as StructType;
return this_.members.addToResultStatements(isDart, "$variableName.");
case PointerType:
final this_ = this as PointerType;
final pointerTo = this_.pointerTo;
switch (pointerTo) {
case StructType _:
if (isDart) {
return pointerTo.members
.addToResultStatements(isDart, "$variableName.ref.");
}
return pointerTo.members
.addToResultStatements(isDart, "$variableName->");
}
case UnionType:
final this_ = this as UnionType;
final member = this_.members.first;
return member.type
.addToResultStatements("$variableName.${member.name}", isDart);
case FixedLengthArrayType:
final this_ = this as FixedLengthArrayType;
final indices = [for (var i = 0; i < this_.length; i += 1) i];
return indices
.map((i) => this_.elementType
.addToResultStatements("$variableName[$i]", isDart))
.join();
}
throw Exception("Not implemented for ${this.runtimeType}");
}
}
extension on List<Member> {
/// A list of statements adding all members recursively to `result`.
///
/// Both valid in Dart and C.
String addToResultStatements(bool isDart, [String namePrefix = ""]) {
return map(
(m) => m.type.addToResultStatements("$namePrefix${m.name}", isDart))
.join();
}
}
extension on CType {
/// A list of statements recursively assigning all members with [a].
///
/// Both valid in Dart and C.
String assignValueStatements(
ArgumentValueAssigner a,
String variableName,
bool isDart,
) {
switch (this.runtimeType) {
case FundamentalType:
final this_ = this as FundamentalType;
return " $variableName = ${a.nextValue(this_)};\n";
case StructType:
final this_ = this as StructType;
return this_.members.assignValueStatements(a, isDart, "$variableName.");
case UnionType:
final this_ = this as UnionType;
final member = this_.members.first;
return member.type
.assignValueStatements(a, "$variableName.${member.name}", isDart);
case FixedLengthArrayType:
final this_ = this as FixedLengthArrayType;
final indices = [for (var i = 0; i < this_.length; i += 1) i];
return indices
.map((i) => this_.elementType
.assignValueStatements(a, "$variableName[$i]", isDart))
.join();
case PointerType:
final this_ = this as PointerType;
final pointerTo = this_.pointerTo;
switch (pointerTo) {
case StructType _:
if (isDart) {
return pointerTo.members
.assignValueStatements(a, isDart, "$variableName.ref.");
}
return pointerTo.members
.assignValueStatements(a, isDart, "$variableName->");
}
}
throw Exception("Not implemented for ${this.runtimeType}");
}
/// A list of statements recursively coping all members from [source].
///
/// Both valid in Dart and C.
String copyValueStatements(String source, String destination) {
switch (this.runtimeType) {
case FundamentalType:
return "$destination = $source;\n";
case StructType:
final this_ = this as StructType;
return this_.members.copyValueStatements("$source.", "$destination.");
}
throw Exception("Not implemented for ${this.runtimeType}");
}
}
extension on List<Member> {
/// A list of statements recursively assigning all members with [a].
///
/// Both valid in Dart and C.
String assignValueStatements(
ArgumentValueAssigner a,
bool isDart, [
String namePrefix = "",
]) {
return map((m) => m.type.assignValueStatements(
a,
"$namePrefix${m.name}",
isDart,
)).join();
}
/// A list of statements recursively coping all members from [source].
///
/// Both valid in Dart and C.
String copyValueStatements([sourcePrefix = "", destinationPrefix = ""]) {
return map((m) => m.type.copyValueStatements(
"$sourcePrefix${m.name}", "$destinationPrefix${m.name}")).join();
}
}
/// A helper class that assigns values to fundamental types.
///
/// Also keeps track of a sum of all values, to be used for testing the result.
class ArgumentValueAssigner {
int i = 1;
int sum = 0;
String nextValue(FundamentalType type) {
int argumentValue = i;
i++;
if (type.isBool) {
argumentValue = argumentValue % 2;
}
if (type.isSigned && i % 2 == 0) {
argumentValue = -argumentValue;
}
sum += argumentValue;
if (type.isFloatingPoint) {
return argumentValue.toDouble().toString();
} else if (type.isInteger) {
return argumentValue.toString();
} else if (type.isBool) {
return argumentValue == 1 ? 'true' : 'false';
}
throw 'Unknown type $type';
}
String sumValue(FundamentalType type) {
if (type.isFloatingPoint) {
return sum.toDouble().toString();
} else {
return sum.toString();
}
}
}
extension on CType {
/// A list of Dart statements recursively allocating all members.
String dartAllocateStatements(String variableName) {
switch (this.runtimeType) {
case PointerType:
final this_ = this as PointerType;
final pointerTo = this_.pointerTo;
switch (pointerTo) {
case StructType _:
return '''
final ${variableName} = calloc<${pointerTo.dartType}>();
''';
}
return "\n";
case FundamentalType:
return " ${dartType} ${variableName};\n";
case StructType:
case UnionType:
return """
final ${variableName}Pointer = calloc<$dartType>();
final ${dartType} ${variableName} = ${variableName}Pointer.ref;
""";
}
throw Exception("Not implemented for ${this.runtimeType}");
}
/// A list of Dart statements allocating as zero or nullptr.
String dartAllocateZeroStatements(String variableName,
{bool structsAsPointers = false}) {
switch (this.runtimeType) {
case FundamentalType:
final this_ = this as FundamentalType;
if (this_.isInteger) {
return "${dartType} ${variableName} = 0;\n";
}
if (this_.isFloatingPoint) {
return "${dartType} ${variableName} = 0.0;\n";
}
if (this_.isBool) {
return "${dartType} ${variableName} = false;\n";
}
throw 'Unknown type $this_';
case StructType:
case UnionType:
if (structsAsPointers) {
return "Pointer<${dartType}> ${variableName}Pointer = nullptr;\n";
} else {
return "${dartType} ${variableName} = Pointer<${dartType}>.fromAddress(0).ref;\n";
}
case PointerType:
return "${dartType} ${variableName} = nullptr;\n";
}
throw Exception("Not implemented for ${this.runtimeType}");
}
}
extension on List<Member> {
/// A list of Dart statements recursively allocating all members.
String dartAllocateStatements([String namePrefix = ""]) {
return map((m) => m.type.dartAllocateStatements("$namePrefix${m.name}"))
.join();
}
/// A list of Dart statements as zero or nullptr.
String dartAllocateZeroStatements(String namePrefix) {
return map((m) => m.type.dartAllocateZeroStatements("$namePrefix${m.name}"))
.join();
}
}
extension on CType {
/// A list of Dart statements recursively freeing all members.
String dartFreeStatements(String variableName) {
switch (this.runtimeType) {
case FundamentalType:
return "";
case StructType:
case UnionType:
return "calloc.free(${variableName}Pointer);\n";
case PointerType:
return "calloc.free(${variableName});\n";
}
throw Exception("Not implemented for ${this.runtimeType}");
}
}
extension on List<Member> {
/// A list of Dart statements recursively freeing all members.
String dartFreeStatements([String namePrefix = ""]) {
return map((m) => m.type.dartFreeStatements("$namePrefix${m.name}")).join();
}
}
extension on CType {
/// A list of C statements recursively allocating all members.
String cAllocateStatements(String variableName) {
switch (this.runtimeType) {
case FundamentalType:
return "${cType} ${variableName};\n";
case StructType:
case UnionType:
return "${cType} ${variableName} = {};\n";
case PointerType:
final this_ = this as PointerType;
final pointerTo = this_.pointerTo;
switch (pointerTo) {
case StructType _:
return '''
${pointerTo.cType} ${variableName}_value = {};
${cType} ${variableName} = &${variableName}_value;
''';
}
}
throw Exception("Not implemented for ${this.runtimeType}");
}
}
extension on List<Member> {
/// A list of C statements recursively allocating all members.
String cAllocateStatements([String namePrefix = ""]) {
return map((m) => m.type.cAllocateStatements("$namePrefix${m.name}"))
.join();
}
}
extension on CType {
/// A list of Dart statements recursively checking all members.
String dartExpectsStatements(String expected, String actual) {
switch (this.runtimeType) {
case FundamentalType:
final this_ = this as FundamentalType;
if (this_.isInteger) {
return "Expect.equals(${expected}, ${actual});";
}
if (this_.isFloatingPoint) {
return "Expect.approxEquals(${expected}, ${actual});";
}
if (this_.isBool) {
return "Expect.equals(${expected} % 2 != 0, ${actual});";
}
throw 'Unexpected type $this_';
case StructType:
final this_ = this as StructType;
return this_.members.dartExpectsStatements("$expected.", "$actual.");
case FixedLengthArrayType:
final this_ = this as FixedLengthArrayType;
return """
for (int i = 0; i < ${this_.length}; i++){
${this_.elementType.dartExpectsStatements("$expected[i]", "$actual[i]")}
}
""";
}
throw Exception("Not implemented for ${this.runtimeType}");
}
}
extension on List<Member> {
/// A list of Dart statements recursively checking all members.
String dartExpectsStatements(
[String expectedPrefix = "", String actualPrefix = ""]) {
return map((m) => m.type.dartExpectsStatements(
"$expectedPrefix${m.name}", "$actualPrefix${m.name}")).join();
}
}
extension on CType {
/// A list of C statements recursively checking all members.
String cExpectsStatements(String expected, String actual) {
switch (this.runtimeType) {
case FundamentalType:
final this_ = this as FundamentalType;
if (this_.isInteger || this_.isBool) {
return "CHECK_EQ(${expected}, ${actual});";
}
assert(this_.isFloatingPoint);
return "CHECK_APPROX(${expected}, ${actual});";
case StructType:
final this_ = this as StructType;
return this_.members.cExpectsStatements("$expected.", "$actual.");
case FixedLengthArrayType:
final this_ = this as FixedLengthArrayType;
return """
for (intptr_t i = 0; i < ${this_.length}; i++){
${this_.elementType.cExpectsStatements("$expected[i]", "$actual[i]")}
}
""";
}
throw Exception("Not implemented for ${this.runtimeType}");
}
/// A list of C statements recursively checking all members for zero.
String cExpectsZeroStatements(String actual) {
switch (this.runtimeType) {
case FundamentalType:
final this_ = this as FundamentalType;
if (this_.isInteger || this_.isBool) {
return "CHECK_EQ(0, ${actual});";
}
assert(this_.isFloatingPoint);
return "CHECK_APPROX(0.0, ${actual});";
case StructType:
final this_ = this as StructType;
return this_.members.cExpectsZeroStatements("$actual.");
case FixedLengthArrayType:
final this_ = this as FixedLengthArrayType;
return """
for (intptr_t i = 0; i < ${this_.length}; i++){
${this_.elementType.cExpectsZeroStatements("$actual[i]")}
}
""";
}
throw Exception("Not implemented for ${this.runtimeType}");
}
}
extension on List<Member> {
/// A list of C statements recursively checking all members.
String cExpectsStatements(
[String expectedPrefix = "", String actualPrefix = ""]) {
return map((m) => m.type.cExpectsStatements(
"$expectedPrefix${m.name}", "$actualPrefix${m.name}")).join();
}
/// A list of C statements recursively checking all members for zero.
String cExpectsZeroStatements([String actualPrefix = ""]) {
return map((m) => m.type.cExpectsZeroStatements("$actualPrefix${m.name}"))
.join();
}
}
extension on CType {
/// Expression denoting the first FundamentalType field.
///
/// Both valid in Dart and C.
String firstArgumentName(String variableName, bool isDart) {
switch (this.runtimeType) {
case FundamentalType:
return variableName;
case StructType:
case UnionType:
final this_ = this as CompositeType;
return this_.members.firstArgumentName(isDart, "$variableName.");
case PointerType:
final this_ = this as PointerType;
final pointerTo = this_.pointerTo;
switch (pointerTo) {
case StructType _:
if (isDart) {
return pointerTo.members
.firstArgumentName(isDart, "$variableName.ref.");
}
return pointerTo.members
.firstArgumentName(isDart, "$variableName->");
}
case FixedLengthArrayType:
final this_ = this as FixedLengthArrayType;
return this_.elementType.firstArgumentName("$variableName[0]", isDart);
}
throw Exception("Not implemented for ${this.runtimeType}");
}
}
extension on List<Member> {
/// Expression denoting the first FundamentalType field.
///
/// Both valid in Dart and C.
String firstArgumentName(bool isDart, [String prefix = ""]) {
return this[0].type.firstArgumentName("$prefix${this[0].name}", isDart);
}
}
extension CompositeTypeGenerator on CompositeType {
String dartClass() {
final self = this;
final packingAnnotation = (self is StructType) && self.hasPacking
? "@Packed(${self.packing})"
: "";
String dartFields = "";
for (final member in members) {
dartFields += "${member.dartStructField()}\n\n";
}
String toStringBody = members.map((m) {
if (m.type is FixedLengthArrayType) {
int dimensionNumber = 0;
String inlineFor = "";
String read = m.name;
String closing = "";
for (final dimension in (m.type as FixedLengthArrayType).dimensions) {
final i = "i$dimensionNumber";
inlineFor += "[for (var $i = 0; $i < $dimension; $i += 1)";
read += "[$i]";
closing += "]";
dimensionNumber++;
}
return "\$\{$inlineFor $read $closing\}";
}
return "\$\{${m.name}\}";
}).join(", ");
return """
$packingAnnotation
final class $name extends $dartSuperClass {
$dartFields
String toString() => "($toStringBody)";
}
""";
}
String get cDefinition {
final self = this;
final packingPragmaPush = (self is StructType) && self.hasPacking
? "#pragma pack(push, ${self.packing})"
: "";
final packingPragmaPop =
(self is StructType) && self.hasPacking ? "#pragma pack(pop)" : "";
String cFields = "";
for (final member in members) {
cFields += " ${member.cStructField}\n";
}
return """
$packingPragmaPush
$cKeyword $name {
$cFields
};
$packingPragmaPop
""";
}
}
extension on FunctionType {
String dartCallCode({required bool isLeaf, required bool isNative}) {
final a = ArgumentValueAssigner();
final assignValues = arguments.assignValueStatements(a, true);
final argumentFrees = arguments.dartFreeStatements();
final argumentNames = arguments.map((e) => e.name).join(", ");
String expects;
switch (testType) {
case TestType.structArguments:
// Check against sum value.
final expectedResult = a.sumValue(returnValue as FundamentalType);
expects = returnValue.dartExpectsStatements(expectedResult, "result");
break;
case TestType.structReturn:
// Check against input arguments.
expects = arguments.dartExpectsStatements("", "result.");
break;
case TestType.structReturnArgument:
expects = returnValue.dartExpectsStatements(
structReturnArgument.name, "result");
break;
}
var namePostfix = isNative ? "Native" : "";
namePostfix += isLeaf ? "Leaf" : "";
return """
${isNative ? '''
@Native<$dartCType>(symbol: '$cName'${isLeaf ? ", isLeaf:true" : ""})
external ${returnValue.dartType} $dartName$namePostfix(${arguments.map((m) => '${m.type.dartType} ${m.name}').join(', ')});
''' : '''
final $dartName$namePostfix =
ffiTestFunctions.lookupFunction<$dartCType, $dartType>(
"$cName"${isLeaf ? ", isLeaf:true" : ""});
'''}
${reason.makeDartDocComment()}
void $dartTestName$namePostfix() {
${arguments.dartAllocateStatements()}
${assignValues}
final result = $dartName$namePostfix($argumentNames);
print("result = \$result");
$expects
$argumentFrees
}
""";
}
String dartCallbackCode() {
final argumentString =
arguments.map((a) => "${a.type.dartType} ${a.name}").join(", ");
final prints = arguments.map((a) => "\$\{${a.name}\}").join(", ");
bool structsAsPointers = false;
String assignReturnGlobal = "";
String buildReturnValue = "";
String returnValueType = returnValue.dartType;
String result = 'result';
if (returnValueType == 'bool') {
// We can't sum a bool;
returnValueType = 'int';
result = 'result % 2 != 0';
}
switch (testType) {
case TestType.structArguments:
// Sum all input values.
buildReturnValue = """
$returnValueType result = 0;
${arguments.addToResultStatements(true, '${dartName}_')}
""";
assignReturnGlobal = "${dartName}Result = $result;";
break;
case TestType.structReturn:
// Allocate a struct.
buildReturnValue = """
final resultPointer = calloc<${returnValue.dartType}>();
final result = resultPointer.ref;
${arguments.copyValueStatements("${dartName}_", "result.")}
""";
assignReturnGlobal = "${dartName}ResultPointer = resultPointer;";
structsAsPointers = true;
break;
case TestType.structReturnArgument:
buildReturnValue = """
${returnValue.cType} result = ${dartName}_${structReturnArgument.name};
""";
assignReturnGlobal = "${dartName}Result = result;";
break;
}
final globals = arguments.dartAllocateZeroStatements("${dartName}_");
final copyToGlobals = arguments.map((a) {
final type = a.type;
switch (type) {
case PointerType _:
// Copy the pointer, but don't use after callback returned.
return '${dartName}_${a.name} = ${a.name};';
default:
return '${dartName}_${a.name} = ${a.name};';
}
}).join("\n ");
// Simulate assigning values the same way as in C, so that we know what the
// final return value should be.
final a = ArgumentValueAssigner();
arguments.assignValueStatements(a, true);
String afterCallbackExpects = "";
String afterCallbackFrees = "";
switch (testType) {
case TestType.structArguments:
// Check that the input structs are still available.
// Check against sum value.
final expectedResult = a.sumValue(returnValue as FundamentalType);
afterCallbackExpects =
returnValue.dartExpectsStatements(expectedResult, "result");
break;
case TestType.structReturn:
// We're passing allocating structs in [buildReturnValue].
afterCallbackFrees =
returnValue.dartFreeStatements("${dartName}Result");
break;
case TestType.structReturnArgument:
break;
}
return """
typedef ${cName}Type = $dartCType;
// Global variables to be able to test inputs after callback returned.
$globals
// Result variable also global, so we can delete it after the callback.
${returnValue.dartAllocateZeroStatements("${dartName}Result", structsAsPointers: structsAsPointers)}
${returnValue.dartType} ${dartName}CalculateResult() {
$buildReturnValue
$assignReturnGlobal
return $result;
}
${reason.makeDartDocComment()}
${returnValue.dartType} $dartName($argumentString) {
print("$dartName($prints)");
// Possibly throw.
if (${arguments.firstArgumentName(true)} == $throwExceptionValue ||
${arguments.firstArgumentName(true)} == $returnNullValue) {
print("throwing!");
throw Exception("$cName throwing on purpose!");
}
$copyToGlobals
final result = ${dartName}CalculateResult();
print("result = \$result");
return result;
}
${this.arguments.containsPointers ? '' : '''
void ${dartName}AfterCallback() {
$afterCallbackFrees
final result = ${dartName}CalculateResult();
print("after callback result = \$result");
$afterCallbackExpects
$afterCallbackFrees
}
'''}
""";
}
String dartCallbackTestConstructor({required bool isNativeCallable}) {
String exceptionalPrefix = isNativeCallable ? "exceptionalReturn: " : "";
String exceptionalReturn = "";
if (returnValue is FundamentalType) {
final returnValue_ = returnValue as FundamentalType;
if (returnValue_.isFloatingPoint) {
exceptionalReturn = ", ${exceptionalPrefix}0.0";
} else if (returnValue_.isInteger) {
exceptionalReturn = ", ${exceptionalPrefix}0";
} else if (returnValue_.isBool) {
exceptionalReturn = ", ${exceptionalPrefix}false";
} else {
throw 'Unexpected type $returnValue_';
}
}
final T = '${cName}Type';
final constructor = isNativeCallable
? 'NativeCallable<$T>.isolateLocal'
: 'Pointer.fromFunction<$T>';
final afterCallback = this.arguments.containsPointers
? 'noChecks'
: '${dartName}AfterCallback';
return """
CallbackTest.withCheck("$cName",
$constructor($dartName$exceptionalReturn),
$afterCallback),
""";
}
String dartNativeCallableListenerCode({required bool isAsync}) {
final argumentString =
arguments.map((a) => "${a.type.dartType} ${a.name}").join(", ");
final prints = arguments.map((a) => "\$\{${a.name}\}").join(", ");
// The type of the async callback is the same as this FunctionType, but with
// a void return type.
final callbackType =
FunctionType(argumentTypes, void_, reason, varArgsIndex: varArgsIndex);
final a = ArgumentValueAssigner();
arguments.assignValueStatements(a, true);
final expectedResult = a.sumValue(int64);
return """
typedef ${cName}Type = ${callbackType.dartCType};
// Global variable that stores the result.
final ${cName}Result = Completer<double>();
${reason.makeDartDocComment()}
void $dartName($argumentString) {
print("$dartName($prints)");
double result = 0;
${arguments.addToResultStatements(true)}
print("result = \$result");
${cName}Result.complete(result);
}
Future<void> ${dartName}AfterCallback() async {
final result = await ${cName}Result.future;
print("after callback result = \$result");
Expect.approxEquals($expectedResult, result);
}
""";
}
String dartNativeCallableListenerTestConstructor({required bool isAsync}) {
final T = '${cName}Type';
final constructor =
isAsync ? 'NativeCallable<$T>.listener' : 'Pointer.fromFunction<$T>';
final afterCallback = this.arguments.containsPointers
? 'noChecksAsync'
: '${dartName}AfterCallback';
return """
AsyncCallbackTest("$cName",
$constructor($dartName),
$afterCallback),
""";
}
String get cCallCode {
String body = "";
String returnValueType = returnValue.cType;
String returnStatement = 'return result;';
if (returnValueType == 'bool') {
// We can't sum in a bool.
returnValueType = 'uint64_t';
returnStatement = 'return result % 2 != 0;';
}
switch (testType) {
case TestType.structArguments:
body = """
$returnValueType result = 0;
${arguments.addToResultStatements(false)}
""";
break;
case TestType.structReturn:
body = """
$returnValueType result = {};
${arguments.copyValueStatements("", "result.")}
""";
break;
case TestType.structReturnArgument:
body = """
$returnValueType result = ${structReturnArgument.name};
""";
break;
}
final argumentString = [
for (final argument in arguments.take(varArgsIndex ?? arguments.length))
"${argument.type.cType} ${argument.name}",
if (varArgsIndex != null) "...",
].join(", ");
final varArgUnpackArguments = [
for (final argument in arguments.skip(varArgsIndex ?? arguments.length))
" ${argument.type.cType} ${argument.name} = va_arg(var_args, ${argument.type.cType});",
].join("\n");
String varArgsUnpack = '';
if (varArgsIndex != null) {
varArgsUnpack = """
va_list var_args;
va_start(var_args, ${arguments[varArgsIndex! - 1].name});
$varArgUnpackArguments
va_end(var_args);
""";
}
return """
// Used for testing structs and unions by value.
${reason.makeCComment()}
DART_EXPORT ${returnValue.cType} $cName($argumentString) {
$varArgsUnpack
std::cout << "$cName" ${arguments.coutExpression()} << "\\n";
$body
${returnValue.coutStatement("result")}
$returnStatement
}
""";
}
String get cCallbackCode {
final a = ArgumentValueAssigner();
final argumentAllocations = arguments.cAllocateStatements();
final assignValues = arguments.assignValueStatements(a, false);
final argumentString = [
for (final argument in arguments.take(varArgsIndex ?? arguments.length))
"${argument.type.cType} ${argument.name}",
if (varArgsIndex != null) "...",
].join(", ");
final argumentNames = arguments.map((e) => e.name).join(", ");
String expects = "";
String expectsZero = "";
switch (testType) {
case TestType.structArguments:
// Check against sum value.
final returnValue_ = returnValue as FundamentalType;
final expectedResult = a.sumValue(returnValue_);
expects = returnValue.cExpectsStatements(expectedResult, "result");
expectsZero = returnValue.cExpectsZeroStatements("result");
break;
case TestType.structReturn:
// Check against input statements.
expects = arguments.cExpectsStatements("", "result.");
expectsZero = arguments.cExpectsZeroStatements("result.");
break;
case TestType.structReturnArgument:
// Check against input struct fields.
expects =
returnValue.cExpectsStatements(structReturnArgument.name, "result");
expectsZero = returnValue.cExpectsZeroStatements("result");
break;
}
return """
// Used for testing structs and unions by value.
${reason.makeCComment()}
DART_EXPORT intptr_t
Test$cName(
// NOLINTNEXTLINE(whitespace/parens)
${returnValue.cType} (*f)($argumentString)) {
$argumentAllocations
$assignValues
std::cout << "Calling Test$cName(" ${arguments.coutExpression()} << ")\\n";
${returnValue.cType} result = f($argumentNames);
${returnValue.coutStatement("result")}
$expects
// Pass argument that will make the Dart callback throw.
${arguments.firstArgumentName(false)} = $throwExceptionValue;
result = f($argumentNames);
$expectsZero
// Pass argument that will make the Dart callback return null.
${arguments.firstArgumentName(false)} = $returnNullValue;
result = f($argumentNames);
$expectsZero
return 0;
}
""";
}
String get cAsyncCallbackCode {
final a = ArgumentValueAssigner();
final argumentAllocations = arguments.cAllocateStatements();
final assignValues = arguments.assignValueStatements(a, false);
final argumentString = [
for (final argument in arguments.take(varArgsIndex ?? arguments.length))
"${argument.type.cType} ${argument.name}",
if (varArgsIndex != null) "...",
].join(", ");
final argumentNames = arguments.map((e) => e.name).join(", ");
return """
// Used for testing structs and unions by value.
${reason.makeCComment()}
DART_EXPORT void
TestAsync$cName(
// NOLINTNEXTLINE(whitespace/parens)
void (*f)($argumentString)) {
$argumentAllocations
$assignValues
std::cout << "Calling TestAsync$cName(" ${arguments.coutExpression()} << ")\\n";
f($argumentNames);
}
""";
}
}
/// Some value between 0 and 127 (works in every native type).
const throwExceptionValue = 42;
/// Some value between 0 and 127 (works in every native type).
const returnNullValue = 84;
const generatorPath =
'tests/ffi/generator/structs_by_value_tests_generator.dart';
headerDartCompound({required int copyrightYear}) => """
${headerCommon(copyrightYear: copyrightYear, generatorPath: generatorPath)}
import 'dart:ffi';
""";
String compoundsPath() => Platform.script
.resolve("../../ffi/function_structs_by_value_generated_compounds.dart")
.toFilePath();
Future<void> writeDartCompounds() async {
final StringBuffer buffer = StringBuffer();
buffer.write(headerDartCompound(copyrightYear: 2021));
buffer.writeAll(compounds.map((e) => e.dartClass()));
final path = compoundsPath();
await File(path).writeAsString(buffer.toString());
await runProcess(Platform.resolvedExecutable, ["format", path]);
}
headerDartCallTest({
required int copyrightYear,
String vmFlags = '',
}) {
if (vmFlags.length != 0 && !vmFlags.endsWith(' ')) {
vmFlags += ' ';
}
return """
${headerCommon(copyrightYear: copyrightYear, generatorPath: generatorPath)}
//
// SharedObjects=ffi_test_functions
// VMOptions=${vmFlags.trim()}
// VMOptions=$vmFlags--deterministic --optimization-counter-threshold=90
// VMOptions=$vmFlags--use-slow-path
// VMOptions=$vmFlags--use-slow-path --stacktrace-every=100
import 'dart:ffi';
import 'package:expect/expect.dart';
// ignore: unused_import
import 'package:ffi/ffi.dart';
import 'dylib_utils.dart';
// Reuse the compound classes.
import 'function_structs_by_value_generated_compounds.dart';
final ffiTestFunctions = dlopenPlatformSpecific("ffi_test_functions");
""";
}
Future<void> writeDartCallTest(
String nameSuffix,
List<FunctionType> functions, {
required bool isLeaf,
required bool isNative,
required bool isVarArgs,
}) async {
final StringBuffer buffer = StringBuffer();
buffer.write(headerDartCallTest(
copyrightYear: isVarArgs || isNative
? 2023
: isLeaf
? 2021
: 2020,
));
var suffix = isNative ? 'Native' : '';
suffix += isLeaf ? 'Leaf' : '';
final forceDlOpen = !isNative
? ''
: '''
// Force dlopen so @Native lookups in DynamicLibrary.process() succeed.
dlopenGlobalPlatformSpecific('ffi_test_functions');
''';
buffer.write("""
void main() {$forceDlOpen
for (int i = 0; i < 100; ++i) {
${functions.map((e) => "${e.dartTestName}$suffix();").join("\n ")}
}
}
""");
buffer.writeAll(
functions.map((e) => e.dartCallCode(isLeaf: isLeaf, isNative: isNative)));
final path = callTestPath(
isLeaf: isLeaf,
isNative: isNative,
nameSuffix: nameSuffix,
isVarArgs: isVarArgs);
await File(path).writeAsString(buffer.toString());
await runProcess(Platform.resolvedExecutable, ["format", path]);
}
String callTestPath({
required bool isLeaf,
required bool isNative,
String nameSuffix = '',
required bool isVarArgs,
}) {
final baseName = isVarArgs ? 'varargs' : 'structs_by_value';
final suffix =
'$nameSuffix${isNative ? '_native' : ''}${isLeaf ? '_leaf' : ''}';
return Platform.script
.resolve("../../ffi/function_${baseName}_generated${suffix}_test.dart")
.toFilePath();
}
headerDartCallbackTest({
required int copyrightYear,
String vmFlags = '',
}) {
if (vmFlags.length != 0 && !vmFlags.endsWith(' ')) {
vmFlags += ' ';
}
return """
${headerCommon(copyrightYear: copyrightYear, generatorPath: generatorPath)}
//
// SharedObjects=ffi_test_functions
// VMOptions=${vmFlags.trim()}
// VMOptions=$vmFlags--deterministic --optimization-counter-threshold=20
// VMOptions=$vmFlags--use-slow-path
// VMOptions=$vmFlags--use-slow-path --stacktrace-every=100
import 'dart:ffi';
import 'package:expect/expect.dart';
// ignore: unused_import
import 'package:ffi/ffi.dart';
import 'callback_tests_utils.dart';
import 'dylib_utils.dart';
// Reuse the compound classes.
import 'function_structs_by_value_generated_compounds.dart';
final ffiTestFunctions = dlopenPlatformSpecific("ffi_test_functions");
void main() {
testCases.forEach((t) {
print("==== Running " + t.name);
t.run();
});
}
""";
}
Future<void> writeDartCallbackTest(
List<FunctionType> functions, {
required bool isVarArgs,
}) async {
for (bool isNativeCallable in [false, true]) {
final StringBuffer buffer = StringBuffer();
buffer.write(headerDartCallbackTest(
copyrightYear: isVarArgs || isNativeCallable ? 2023 : 2020,
));
buffer.write("""
final testCases = [
${functions.map((e) => e.dartCallbackTestConstructor(isNativeCallable: isNativeCallable)).join("\n")}
];
""");
buffer.writeAll(functions.map((e) => e.dartCallbackCode()));
final path = callbackTestPath(
isVarArgs: isVarArgs, isNativeCallable: isNativeCallable);
await File(path).writeAsString(buffer.toString());
await runProcess(Platform.resolvedExecutable, ["format", path]);
}
}
String callbackTestPath({
required bool isVarArgs,
required bool isNativeCallable,
}) {
final baseName = isVarArgs ? "varargs" : "structs_by_value";
final natCall = isNativeCallable ? "_native_callable" : "";
return Platform.script
.resolve(
"../../ffi/function_callbacks_${baseName}${natCall}_generated_test.dart")
.toFilePath();
}
headerDartAsyncCallbackTest({String vmFlags = ''}) {
if (vmFlags.length != 0 && !vmFlags.endsWith(' ')) {
vmFlags += ' ';
}
return """
${headerCommon(copyrightYear: 2023, generatorPath: generatorPath)}
//
// SharedObjects=ffi_test_functions
// VMOptions=${vmFlags.trim()}
// VMOptions=$vmFlags--deterministic --optimization-counter-threshold=20
// VMOptions=$vmFlags--use-slow-path
// VMOptions=$vmFlags--use-slow-path --stacktrace-every=100
import 'dart:async';
import 'dart:ffi';
import 'package:expect/expect.dart';
// ignore: unused_import
import 'package:ffi/ffi.dart';
import 'async_callback_tests_utils.dart';
import 'dylib_utils.dart';
// Reuse the compound classes.
import 'function_structs_by_value_generated_compounds.dart';
final ffiTestFunctions = dlopenPlatformSpecific("ffi_test_functions");
Future<void> main() async {
for (final t in testCases) {
print("==== Running " + t.name);
await t.run();
}
}
""";
}
Future<void> writeDartNativeCallableListenerTest(
List<FunctionType> functions, {
required bool isAsync,
}) async {
final StringBuffer buffer = StringBuffer();
buffer.write(headerDartAsyncCallbackTest());
if (isAsync) {
functions = functions.where((f) => !f.arguments.containsPointers).toList();
}
final constructors = functions
.map((e) => e.dartNativeCallableListenerTestConstructor(isAsync: isAsync))
.join("\n");
buffer.write("""
final testCases = [
$constructors
];
""");
buffer.writeAll(
functions.map((e) => e.dartNativeCallableListenerCode(isAsync: isAsync)));
final path = nativeCallableListenerTestPath(isAsync: isAsync);
await File(path).writeAsString(buffer.toString());
await runProcess(Platform.resolvedExecutable, ["format", path]);
}
String nativeCallableListenerTestPath({required bool isAsync}) {
return Platform.script
.resolve("../../ffi/native_callables_${isAsync ? 'async' : 'sync'}_"
"structs_by_value_generated_test.dart")
.toFilePath();
}
Future<void> writeC() async {
final StringBuffer buffer = StringBuffer();
buffer.write(headerC(copyrightYear: 2020, generatorPath: generatorPath));
buffer.writeAll(compounds.map((e) => e.cDefinition));
buffer.writeAll(functions.map((e) => e.cCallCode));
buffer.writeAll(functions.map((e) => e.cCallbackCode));
buffer.writeAll(functions.map((e) => e.cAsyncCallbackCode));
buffer.writeAll(functionsVarArgs.map((e) => e.cCallCode));
buffer.writeAll(functionsVarArgs.map((e) => e.cCallbackCode));
buffer.write(footerC);
await File(ccPath).writeAsString(buffer.toString());
await runProcess(clangFormatPath, ["-i", ccPath]);
}
final sdkRoot = Platform.script.resolve("../../../");
final ccPath = sdkRoot
.resolve("runtime/bin/ffi_test/ffi_test_functions_generated.cc")
.toFilePath();
final clangFormatPath = sdkRoot
.resolve(
'buildtools/${buildToolsSubdir[Abi.current()]!}/clang/bin/clang-format')
.toFilePath();
const buildToolsSubdir = {
Abi.linuxArm64: 'linux-arm64',
Abi.linuxX64: 'linux-x64',
Abi.macosArm64: 'mac-arm64',
Abi.macosX64: 'mac-x64',
Abi.windowsX64: 'win-x64',
};
void printUsage() {
print("""
Generates structs by value tests.
""");
}
void main(List<String> arguments) async {
if (arguments.length != 0) {
printUsage();
return;
}
await Future.wait([
writeDartCompounds(),
for (bool isLeaf in [false, true]) ...[
for (bool isNative in [false, true]) ...[
writeDartCallTest(
'_args',
functionsStructArguments,
isLeaf: isLeaf,
isNative: isNative,
isVarArgs: false,
),
writeDartCallTest(
'_ret',
functionsStructReturn,
isLeaf: isLeaf,
isNative: isNative,
isVarArgs: false,
),
writeDartCallTest(
'_ret_arg',
functionsReturnArgument,
isLeaf: isLeaf,
isNative: isNative,
isVarArgs: false,
),
writeDartCallTest(
'',
functionsVarArgs,
isLeaf: isLeaf,
isNative: isNative,
isVarArgs: true,
),
],
],
writeDartCallbackTest(functions, isVarArgs: false),
writeDartCallbackTest(functionsVarArgs, isVarArgs: true),
writeDartNativeCallableListenerTest(functions, isAsync: false),
writeDartNativeCallableListenerTest(functions, isAsync: true),
writeC(),
]);
}