[vm] Support serialization of FFI callbacks in IL serialization

TEST=tests/ffi/function_callbacks_test.dart
TEST=Manual run of vm-kernel-precomp-linux-debug-x64-try with --test_il_serialization enabled.

Issue: https://github.com/dart-lang/sdk/issues/43299
Change-Id: Ia57021d9091e8a80de3645cb4723ebdbb5a3d33d
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/256371
Commit-Queue: Alexander Markov <alexmarkov@google.com>
Reviewed-by: Daco Harkes <dacoharkes@google.com>
This commit is contained in:
Alexander Markov
2022-08-26 17:37:21 +00:00
committed by Commit Bot
parent 2da3806823
commit a7f7c0c5ac
22 changed files with 143 additions and 84 deletions
+2 -1
View File
@@ -114,7 +114,8 @@ DEFINE_NATIVE_ENTRY(Ffi_nativeCallbackFunction, 1, 2) {
// _pointerFromFunction and will not leak out into user code.
arguments->SetReturn(
Function::Handle(zone, compiler::ffi::NativeCallbackFunction(
native_signature, func, exceptional_return)));
native_signature, func, exceptional_return,
/*register_function=*/false)));
// Because we have already set the return value.
return Object::sentinel().ptr();
+2 -2
View File
@@ -1387,7 +1387,7 @@ class FfiTrampolineDataSerializationCluster : public SerializationCluster {
WriteFromTo(data);
if (s->kind() == Snapshot::kFullAOT) {
s->WriteUnsigned(data->untag()->callback_id_);
s->Write<int32_t>(data->untag()->callback_id_);
} else {
// FFI callbacks can only be written to AOT snapshots.
ASSERT(data->untag()->callback_target() == Object::null());
@@ -1420,7 +1420,7 @@ class FfiTrampolineDataDeserializationCluster : public DeserializationCluster {
FfiTrampolineData::InstanceSize());
d.ReadFromTo(data);
data->untag()->callback_id_ =
d_->kind() == Snapshot::kFullAOT ? d.ReadUnsigned() : 0;
d_->kind() == Snapshot::kFullAOT ? d.Read<int32_t>() : -1;
}
}
};
+17
View File
@@ -328,6 +328,23 @@ class CanonicalInstanceTraits {
static ObjectPtr NewKey(const CanonicalInstanceKey& obj);
};
struct CanonicalFfiCallbackFunctionTraits {
static uint32_t Hash(const Object& key) { return Function::Cast(key).Hash(); }
static const char* Name() { return "CanonicalFfiCallbackFunctionTraits"; }
static bool IsMatch(const Object& x, const Object& y) {
const auto& f1 = Function::Cast(x);
const auto& f2 = Function::Cast(y);
return (f1.FfiCallbackTarget() == f2.FfiCallbackTarget() &&
f1.FfiCSignature() == f2.FfiCSignature() &&
f1.FfiCallbackExceptionalReturn() ==
f2.FfiCallbackExceptionalReturn());
}
static bool ReportStats() { return false; }
};
using FfiCallbackFunctionSet =
UnorderedHashSet<CanonicalFfiCallbackFunctionTraits>;
} // namespace dart
#endif // RUNTIME_VM_CANONICAL_TABLES_H_
+2 -4
View File
@@ -2151,10 +2151,8 @@ class NativeEntryInstr : public FunctionEntryInstr {
GraphEntryInstr* graph_entry,
intptr_t block_id,
intptr_t try_index,
intptr_t deopt_id,
intptr_t callback_id)
intptr_t deopt_id)
: FunctionEntryInstr(graph_entry, block_id, try_index, deopt_id),
callback_id_(callback_id),
marshaller_(marshaller) {}
DECLARE_INSTRUCTION(NativeEntry)
@@ -2162,7 +2160,6 @@ class NativeEntryInstr : public FunctionEntryInstr {
PRINT_TO_SUPPORT
#define FIELD_LIST(F) \
F(const intptr_t, callback_id_) \
F(const compiler::ffi::CallbackMarshaller&, marshaller_)
DECLARE_INSTRUCTION_SERIALIZABLE_FIELDS(NativeEntryInstr,
@@ -9667,6 +9664,7 @@ class CheckNullInstr : public TemplateDefinition<1, Throws, Pure> {
function_name_(function_name),
exception_type_(exception_type) {
ASSERT(function_name.IsNotTemporaryScopedHandle());
ASSERT(function_name.IsSymbol());
SetInputAt(0, value);
}
+4 -2
View File
@@ -1636,6 +1636,8 @@ void NativeEntryInstr::EmitNativeCode(FlowGraphCompiler* compiler) {
reinterpret_cast<uword>(DLRT_GetThreadForNativeCallback)));
}
const intptr_t callback_id = marshaller_.dart_signature().FfiCallbackId();
// Load the thread object. If we were called by a trampoline, the thread is
// already loaded.
if (!NativeCallbackTrampolines::Enabled()) {
@@ -1644,7 +1646,7 @@ void NativeEntryInstr::EmitNativeCode(FlowGraphCompiler* compiler) {
__ EnterFrame(1 << FP, 0);
__ ReserveAlignedFrameSpace(0);
__ LoadImmediate(R0, callback_id_);
__ LoadImmediate(R0, callback_id);
__ blx(R1);
__ mov(THR, compiler::Operand(R0));
@@ -1689,7 +1691,7 @@ void NativeEntryInstr::EmitNativeCode(FlowGraphCompiler* compiler) {
compiler::target::GrowableObjectArray::data_offset());
__ LoadFieldFromOffset(CODE_REG, R0,
compiler::target::Array::data_offset() +
callback_id_ * compiler::target::kWordSize);
callback_id * compiler::target::kWordSize);
// Put the code object in the reserved slot.
__ StoreToOffset(CODE_REG, FPREG,
+4 -2
View File
@@ -1486,13 +1486,15 @@ void NativeEntryInstr::EmitNativeCode(FlowGraphCompiler* compiler) {
R1, reinterpret_cast<int64_t>(DLRT_GetThreadForNativeCallback));
}
const intptr_t callback_id = marshaller_.dart_signature().FfiCallbackId();
if (!NativeCallbackTrampolines::Enabled()) {
// Create another frame to align the frame before continuing in "native"
// code.
__ EnterFrame(0);
__ ReserveAlignedFrameSpace(0);
__ LoadImmediate(R0, callback_id_);
__ LoadImmediate(R0, callback_id);
__ blr(R1);
__ mov(THR, R0);
@@ -1549,7 +1551,7 @@ void NativeEntryInstr::EmitNativeCode(FlowGraphCompiler* compiler) {
__ LoadCompressedFieldFromOffset(
CODE_REG, R0,
compiler::target::Array::data_offset() +
callback_id_ * compiler::target::kCompressedWordSize);
callback_id * compiler::target::kCompressedWordSize);
// Put the code object in the reserved slot.
__ StoreToOffset(CODE_REG, FPREG,
+4 -2
View File
@@ -1174,6 +1174,8 @@ void NativeEntryInstr::EmitNativeCode(FlowGraphCompiler* compiler) {
__ pushl(ESI);
__ pushl(EDI);
const intptr_t callback_id = marshaller_.dart_signature().FfiCallbackId();
// Load the thread object.
//
// Create another frame to align the frame before continuing in "native" code.
@@ -1183,7 +1185,7 @@ void NativeEntryInstr::EmitNativeCode(FlowGraphCompiler* compiler) {
__ EnterFrame(0);
__ ReserveAlignedFrameSpace(compiler::target::kWordSize);
__ movl(compiler::Address(SPREG, 0), compiler::Immediate(callback_id_));
__ movl(compiler::Address(SPREG, 0), compiler::Immediate(callback_id));
__ movl(EAX, compiler::Immediate(reinterpret_cast<intptr_t>(
DLRT_GetThreadForNativeCallback)));
__ call(EAX);
@@ -1227,7 +1229,7 @@ void NativeEntryInstr::EmitNativeCode(FlowGraphCompiler* compiler) {
EAX, compiler::target::GrowableObjectArray::data_offset()));
__ movl(CODE_REG, compiler::FieldAddress(
EAX, compiler::target::Array::data_offset() +
callback_id_ * compiler::target::kWordSize));
callback_id * compiler::target::kWordSize));
// Put the code object in the reserved slot.
__ movl(compiler::Address(FPREG,
+4 -2
View File
@@ -1704,13 +1704,15 @@ void NativeEntryInstr::EmitNativeCode(FlowGraphCompiler* compiler) {
A1, reinterpret_cast<int64_t>(DLRT_GetThreadForNativeCallback));
}
const intptr_t callback_id = marshaller_.dart_signature().FfiCallbackId();
if (!NativeCallbackTrampolines::Enabled()) {
// Create another frame to align the frame before continuing in "native"
// code.
__ EnterFrame(0);
__ ReserveAlignedFrameSpace(0);
__ LoadImmediate(A0, callback_id_);
__ LoadImmediate(A0, callback_id);
__ jalr(A1);
__ mv(THR, A0);
@@ -1760,7 +1762,7 @@ void NativeEntryInstr::EmitNativeCode(FlowGraphCompiler* compiler) {
__ LoadCompressedFieldFromOffset(
CODE_REG, A0,
compiler::target::Array::data_offset() +
callback_id_ * compiler::target::kCompressedWordSize);
callback_id * compiler::target::kCompressedWordSize);
// Put the code object in the reserved slot.
__ StoreToOffset(CODE_REG, FPREG,
+30 -15
View File
@@ -242,15 +242,16 @@ void BranchInstr::ReadExtra(FlowGraphDeserializer* d) {
template <>
void FlowGraphSerializer::WriteTrait<const compiler::ffi::CallbackMarshaller&>::
Write(FlowGraphSerializer* s, const compiler::ffi::CallbackMarshaller& x) {
UNIMPLEMENTED();
s->Write<const Function&>(x.dart_signature());
}
template <>
const compiler::ffi::CallbackMarshaller& FlowGraphDeserializer::ReadTrait<
const compiler::ffi::CallbackMarshaller&>::Read(FlowGraphDeserializer* d) {
UNIMPLEMENTED();
const Function& dart_signature = d->Read<const Function&>();
const char* error = nullptr;
return *compiler::ffi::CallbackMarshaller::FromFunction(
d->zone(), Function::null_function(), nullptr);
d->zone(), dart_signature, &error);
}
template <>
@@ -830,14 +831,18 @@ void FlowGraphSerializer::WriteTrait<const Function&>::Write(
return;
}
case UntaggedFunction::kFfiTrampoline: {
if (x.FfiCallbackTarget() != Object::null()) {
UNIMPLEMENTED();
}
s->Write<const String&>(String::Handle(zone, x.name()));
s->Write<const FunctionType&>(FunctionType::Handle(zone, x.signature()));
s->Write<const Function&>(Function::Handle(zone, x.FfiCallbackTarget()));
s->Write<const FunctionType&>(
FunctionType::Handle(zone, x.FfiCSignature()));
s->Write<bool>(x.FfiIsLeaf());
if (x.FfiCallbackTarget() != Object::null()) {
s->Write<const Instance&>(
Instance::Handle(zone, x.FfiCallbackExceptionalReturn()));
} else {
s->Write<const String&>(String::Handle(zone, x.name()));
s->Write<const FunctionType&>(
FunctionType::Handle(zone, x.signature()));
s->Write<bool>(x.FfiIsLeaf());
}
return;
}
default:
@@ -914,13 +919,22 @@ const Function& FlowGraphDeserializer::ReadTrait<const Function&>::Read(
target.GetDynamicInvocationForwarder(name));
}
case UntaggedFunction::kFfiTrampoline: {
const String& name = d->Read<const String&>();
const FunctionType& signature = d->Read<const FunctionType&>();
const Function& callback_target = d->Read<const Function&>();
const FunctionType& c_signature = d->Read<const FunctionType&>();
const bool is_leaf = d->Read<bool>();
return Function::ZoneHandle(
zone, compiler::ffi::TrampolineFunction(name, signature, c_signature,
is_leaf));
if (!callback_target.IsNull()) {
const Instance& exceptional_return = d->Read<const Instance&>();
return Function::ZoneHandle(
zone, compiler::ffi::NativeCallbackFunction(
c_signature, callback_target, exceptional_return,
/*register_function=*/true));
} else {
const String& name = d->Read<const String&>();
const FunctionType& signature = d->Read<const FunctionType&>();
const bool is_leaf = d->Read<bool>();
return Function::ZoneHandle(
zone, compiler::ffi::TrampolineFunction(name, signature,
c_signature, is_leaf));
}
}
default:
UNIMPLEMENTED();
@@ -1981,6 +1995,7 @@ InstancePtr FlowGraphDeserializer::MaybeCanonicalize(
V(Array, Object::null_array()) \
V(Field, Field::Handle(d->zone())) \
V(FunctionType, Object::null_function_type()) \
V(Instance, Object::null_instance()) \
V(String, Object::null_string()) \
V(TypeArguments, Object::null_type_arguments()) \
V(TypeParameters, TypeParameters::Handle(d->zone()))
@@ -88,6 +88,7 @@ class NativeCallingConvention;
V(Environment*) \
V(const Field&) \
V(const ICData*) \
V(const Instance&) \
V(int8_t) \
V(int16_t) \
V(int32_t) \
+4 -2
View File
@@ -1365,6 +1365,8 @@ void NativeEntryInstr::EmitNativeCode(FlowGraphCompiler* compiler) {
DLRT_GetThreadForNativeCallback)));
}
const intptr_t callback_id = marshaller_.dart_signature().FfiCallbackId();
// Create another frame to align the frame before continuing in "native" code.
// If we were called by a trampoline, it has already loaded the thread.
if (!NativeCallbackTrampolines::Enabled()) {
@@ -1372,7 +1374,7 @@ void NativeEntryInstr::EmitNativeCode(FlowGraphCompiler* compiler) {
__ ReserveAlignedFrameSpace(0);
COMPILE_ASSERT(RAX != CallingConventions::kArg1Reg);
__ movq(CallingConventions::kArg1Reg, compiler::Immediate(callback_id_));
__ movq(CallingConventions::kArg1Reg, compiler::Immediate(callback_id));
__ CallCFunction(RAX);
__ movq(THR, RAX);
@@ -1415,7 +1417,7 @@ void NativeEntryInstr::EmitNativeCode(FlowGraphCompiler* compiler) {
CODE_REG,
compiler::FieldAddress(
RAX, compiler::target::Array::data_offset() +
callback_id_ * compiler::target::kCompressedWordSize));
callback_id * compiler::target::kCompressedWordSize));
// Put the code object in the reserved slot.
__ movq(compiler::Address(FPREG,
+27 -12
View File
@@ -4,7 +4,9 @@
#include "vm/compiler/ffi/callback.h"
#include "vm/canonical_tables.h"
#include "vm/class_finalizer.h"
#include "vm/object_store.h"
#include "vm/symbols.h"
namespace dart {
@@ -15,34 +17,35 @@ namespace ffi {
FunctionPtr NativeCallbackFunction(const FunctionType& c_signature,
const Function& dart_target,
const Instance& exceptional_return) {
const Instance& exceptional_return,
bool register_function) {
Thread* const thread = Thread::Current();
const int32_t callback_id = thread->AllocateFfiCallbackId();
Zone* const zone = thread->zone();
Function& function = Function::Handle(zone);
ASSERT(c_signature.IsCanonical());
ASSERT(exceptional_return.IsSmi() || exceptional_return.IsCanonical());
// Create a new Function named '<target>_FfiCallback' and stick it in the
// 'dart:ffi' library. Note that these functions will never be invoked by
// Dart, so they may have duplicate names.
Zone* const zone = thread->zone();
const auto& name = String::Handle(
zone, Symbols::FromConcat(thread, Symbols::FfiCallback(),
String::Handle(zone, dart_target.name())));
const Library& lib = Library::Handle(zone, Library::FfiLibrary());
const Class& owner_class = Class::Handle(zone, lib.toplevel_class());
auto& signature = FunctionType::Handle(zone, FunctionType::New());
const Function& function = Function::Handle(
zone, Function::New(signature, name, UntaggedFunction::kFfiTrampoline,
/*is_static=*/true,
/*is_const=*/false,
/*is_abstract=*/false,
/*is_external=*/false,
/*is_native=*/false, owner_class,
TokenPosition::kNoSource));
function =
Function::New(signature, name, UntaggedFunction::kFfiTrampoline,
/*is_static=*/true,
/*is_const=*/false,
/*is_abstract=*/false,
/*is_external=*/false,
/*is_native=*/false, owner_class, TokenPosition::kNoSource);
function.set_is_debuggable(false);
// Set callback-specific fields which the flow-graph builder needs to generate
// the body.
function.SetFfiCSignature(c_signature);
function.SetFfiCallbackId(callback_id);
function.SetFfiCallbackTarget(dart_target);
// We need to load the exceptional return value as a constant in the generated
@@ -71,6 +74,18 @@ FunctionPtr NativeCallbackFunction(const FunctionType& c_signature,
signature ^= ClassFinalizer::FinalizeType(signature);
function.SetSignature(signature);
if (register_function) {
ObjectStore* object_store = thread->isolate_group()->object_store();
if (object_store->ffi_callback_functions() == Array::null()) {
FfiCallbackFunctionSet set(
HashTables::New<FfiCallbackFunctionSet>(/*initial_capacity=*/4));
object_store->set_ffi_callback_functions(set.Release());
}
FfiCallbackFunctionSet set(object_store->ffi_callback_functions());
function ^= set.InsertOrGet(function);
object_store->set_ffi_callback_functions(set.Release());
}
return function.ptr();
}
+2 -1
View File
@@ -21,7 +21,8 @@ namespace ffi {
FunctionPtr NativeCallbackFunction(const FunctionType& c_signature,
const Function& dart_target,
const Instance& exceptional_return);
const Instance& exceptional_return,
bool register_function);
} // namespace ffi
@@ -6146,19 +6146,12 @@ Fragment StreamingFlowGraphBuilder::BuildFfiNativeCallbackFunction() {
compiler::ffi::NativeFunctionTypeFromFunctionType(zone_, native_sig, &error);
ReportIfNotNull(error);
const Function& result =
Function::ZoneHandle(Z, compiler::ffi::NativeCallbackFunction(
native_sig, target, exceptional_return));
const Function& result = Function::ZoneHandle(
Z,
compiler::ffi::NativeCallbackFunction(
native_sig, target, exceptional_return, /*register_function=*/true));
code += Constant(result);
auto& ffi_callback_functions = GrowableObjectArray::Handle(Z);
ffi_callback_functions ^= IG->object_store()->ffi_callback_functions();
if (ffi_callback_functions.IsNull()) {
ffi_callback_functions ^= GrowableObjectArray::New();
IG->object_store()->set_ffi_callback_functions(ffi_callback_functions);
}
ffi_callback_functions.Add(result);
return code;
}
+6 -6
View File
@@ -4745,9 +4745,9 @@ FlowGraph* FlowGraphBuilder::BuildGraphOfFfiCallback(const Function& function) {
graph_entry_ =
new (Z) GraphEntryInstr(*parsed_function_, Compiler::kNoOSRDeoptId);
auto* const native_entry = new (Z) NativeEntryInstr(
marshaller, graph_entry_, AllocateBlockId(), CurrentTryIndex(),
GetNextDeoptId(), function.FfiCallbackId());
auto* const native_entry =
new (Z) NativeEntryInstr(marshaller, graph_entry_, AllocateBlockId(),
CurrentTryIndex(), GetNextDeoptId());
graph_entry_->set_normal_entry(native_entry);
@@ -4793,9 +4793,9 @@ FlowGraph* FlowGraphBuilder::BuildGraphOfFfiCallback(const Function& function) {
body += Drop();
body += IntConstant(0);
} else if (!marshaller.IsHandle(compiler::ffi::kResultIndex)) {
body +=
CheckNullOptimized(String::ZoneHandle(Z, String::New("return_value")),
CheckNullInstr::kArgumentError);
body += CheckNullOptimized(
String::ZoneHandle(Z, Symbols::New(H.thread(), "return_value")),
CheckNullInstr::kArgumentError);
}
if (marshaller.IsCompound(compiler::ffi::kResultIndex)) {
+15 -9
View File
@@ -7656,16 +7656,22 @@ bool Function::FfiCSignatureReturnsStruct() const {
int32_t Function::FfiCallbackId() const {
ASSERT(IsFfiTrampoline());
if (FfiCallbackTarget() == Object::null()) {
return -1;
}
const Object& obj = Object::Handle(data());
ASSERT(!obj.IsNull());
return FfiTrampolineData::Cast(obj).callback_id();
}
void Function::SetFfiCallbackId(int32_t value) const {
ASSERT(IsFfiTrampoline());
const Object& obj = Object::Handle(data());
ASSERT(!obj.IsNull());
FfiTrampolineData::Cast(obj).set_callback_id(value);
const FfiTrampolineData& trampoline_data = FfiTrampolineData::Cast(obj);
int32_t callback_id = trampoline_data.callback_id();
#if defined(DART_PRECOMPILED_RUNTIME)
ASSERT(callback_id >= 0);
#else
if (callback_id < 0) {
callback_id = Thread::Current()->AllocateFfiCallbackId();
trampoline_data.set_callback_id(callback_id);
}
#endif
return callback_id;
}
bool Function::FfiIsLeaf() const {
@@ -10610,7 +10616,7 @@ FfiTrampolineDataPtr FfiTrampolineData::New() {
FfiTrampolineData::kClassId, FfiTrampolineData::InstanceSize(),
Heap::kOld, FfiTrampolineData::ContainsCompressedPointers());
FfiTrampolineDataPtr data = static_cast<FfiTrampolineDataPtr>(raw);
data->untag()->callback_id_ = 0;
data->untag()->callback_id_ = -1;
data->untag()->is_leaf_ = false;
return data;
}
-3
View File
@@ -2745,9 +2745,6 @@ class Function : public Object {
// -1 for Dart -> native calls.
int32_t FfiCallbackId() const;
// Can only be called on FFI trampolines.
void SetFfiCallbackId(int32_t value) const;
// Can only be called on FFI trampolines.
bool FfiIsLeaf() const;
+1 -1
View File
@@ -272,7 +272,7 @@ class ObjectPointerVisitor;
RW(GrowableObjectArray, instructions_tables) \
RW(Array, obfuscation_map) \
RW(Array, loading_unit_uris) \
RW(GrowableObjectArray, ffi_callback_functions) \
RW(Array, ffi_callback_functions) \
RW(Class, ffi_pointer_class) \
RW(Class, ffi_native_type_class) \
// Please remember the last entry must be referred in the 'to' function below.
+9 -6
View File
@@ -6,6 +6,7 @@
#include "vm/program_visitor.h"
#include "vm/canonical_tables.h"
#include "vm/closure_functions_cache.h"
#include "vm/code_patcher.h"
#include "vm/deopt_instructions.h"
@@ -256,14 +257,16 @@ void ProgramVisitor::WalkProgram(Zone* zone,
// TODO(dartbug.com/43049): Use a more general solution and remove manual
// tracking through object_store->ffi_callback_functions.
auto& function = Function::Handle(zone);
const auto& ffi_callback_entries = GrowableObjectArray::Handle(
zone, object_store->ffi_callback_functions());
if (!ffi_callback_entries.IsNull()) {
for (intptr_t i = 0; i < ffi_callback_entries.Length(); i++) {
function ^= ffi_callback_entries.At(i);
if (object_store->ffi_callback_functions() != Array::null()) {
auto& function = Function::Handle(zone);
FfiCallbackFunctionSet set(object_store->ffi_callback_functions());
FfiCallbackFunctionSet::Iterator it(&set);
while (it.MoveNext()) {
const intptr_t entry = it.Current();
function ^= set.GetKey(entry);
walker.AddToWorklist(function);
}
set.Release();
}
}
+3 -3
View File
@@ -1431,9 +1431,9 @@ class UntaggedFfiTrampolineData : public UntaggedObject {
// calls. The callback id is also used to for verifying that callbacks are
// called on the correct isolate. See DLRT_VerifyCallbackIsolate for details.
//
// Will be 0 for non-callbacks. Check 'callback_target_' to determine if this
// is a callback or not.
uint32_t callback_id_;
// Callback id is -1 for non-callbacks or when id is not allocated yet.
// Check 'callback_target_' to determine if this is a callback or not.
int32_t callback_id_;
// Whether this is a leaf call - i.e. one that doesn't call back into Dart.
bool is_leaf_;
+1
View File
@@ -12,6 +12,7 @@
// VMOptions=--use-slow-path --write-protect-code --no-dual-map-code
// VMOptions=--use-slow-path --write-protect-code --no-dual-map-code --stacktrace-every=100
// VMOptions=--dwarf_stack_traces --no-retain_function_objects --no-retain_code_objects
// VMOptions=--test_il_serialization
// SharedObjects=ffi_test_functions
import 'dart:ffi';
+1
View File
@@ -14,6 +14,7 @@
// VMOptions=--use-slow-path --write-protect-code --no-dual-map-code
// VMOptions=--use-slow-path --write-protect-code --no-dual-map-code --stacktrace-every=100
// VMOptions=--dwarf_stack_traces --no-retain_function_objects --no-retain_code_objects
// VMOptions=--test_il_serialization
// SharedObjects=ffi_test_functions
import 'dart:ffi';