[vm/isolates] Introduce 'vm:isolate-unsendable' pragma.

Decorate Zone, Future, Completer, Timer and Stream with newly-introduced pragma to make sure that message verification stops earlier, produces shorter retaining path for the user.

BUG=https://github.com/dart-lang/sdk/issues/51722
TEST=send_unsupported_objects_test,isolate_exit_unsendable_test.dart
CoreLibraryReviewExempt: vm-specific pragmas
Change-Id: I499eea542d228ac9cf0797a682664f93f360dc80
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/289027
Commit-Queue: Alexander Aprelev <aam@google.com>
Reviewed-by: Martin Kustermann <kustermann@google.com>
This commit is contained in:
Alexander Aprelev 2023-03-27 20:54:11 +00:00 committed by Commit Queue
parent 5b510210db
commit 67683c3973
25 changed files with 305 additions and 79 deletions

View file

@ -15,6 +15,7 @@ These pragmas are part of the VM's API and are safe for use in external code.
| `vm:always-consider-inlining` | Marks a function which particularly benefits from inlining and specialization in context of the caller (for example, when concrete types of arguments are known). Inliner will not give up after one failed inlining attempt and will continue trying to inline this function. |
| `vm:platform-const` | Marks a static getter or a static field with an initializer where the getter body or field initializer evaluates to a constant value if the target operating system is known. |
| `weak-tearoff-reference` | [Declaring a static weak reference intrinsic method.](compiler/pragmas_recognized_by_compiler.md#declaring-a-static-weak-reference-intrinsic-method) |
| `vm:isolate-unsendable` | Marks a class, instances of which won't be allowed to be passed through ports or sent between isolates. |
## Unsafe pragmas for general use

View file

@ -250,18 +250,13 @@ static ObjectPtr ValidateMessageObject(Zone* zone,
MESSAGE_SNAPSHOT_ILLEGAL(SuspendState);
default:
if (cid >= kNumPredefinedCids) {
klass = class_table->At(cid);
if (klass.num_native_fields() != 0) {
illegal_object = raw;
exception_message = "is a NativeWrapper";
break;
}
if (klass.implements_finalizable()) {
illegal_object = raw;
exception_message = "is a Finalizable";
break;
}
klass = class_table->At(cid);
if (klass.is_isolate_unsendable()) {
illegal_object = raw;
exception_message =
"is unsendable object (see restrictions listed at"
"`SendPort.send()` documentation for more information)";
break;
}
}
raw->untag()->VisitPointers(&visitor);

View file

@ -0,0 +1,47 @@
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
//
import 'dart:isolate';
import 'package:async_helper/async_helper.dart';
import 'package:expect/expect.dart';
@pragma('vm:isolate-unsendable')
class Locked {}
class ExtendsLocked extends Locked {}
class ImplementsLocked implements Locked {}
main() async {
asyncStart();
final rpExit = ReceivePort();
final rpError = RawReceivePort((e) {
Expect.fail('Spawned isolated failed with $e');
});
final rp = RawReceivePort((e) {
Expect.fail('Received unexpected $e, no objects should have arrived');
});
await Isolate.spawn((sendPort) {
for (final pairFunctionName in [
[Locked.new, "Locked"],
[ExtendsLocked.new, "ExtendsLocked"],
[ImplementsLocked.new, "ImplementsLocked"]
]) {
Expect.throws(() {
Isolate.exit(sendPort, (pairFunctionName[0] as Function)());
}, (e) {
return e is ArgumentError &&
e
.toString()
.contains(RegExp("unsendable object .+${pairFunctionName[1]}"));
});
}
}, rp.sendPort, onError: rpError.sendPort, onExit: rpExit.sendPort);
await rpExit.first;
rpError.close();
rp.close();
asyncEnd();
}

View file

@ -39,26 +39,65 @@ class Fu {
}
}
void checkForRetainingPath(Object? e, List<String> list) {
Expect.isTrue(e is ArgumentError);
@pragma("vm:isolate-unsendable")
class Locked {}
class ExtendsLocked extends Locked {}
class ImplementsLocked implements Locked {}
bool checkForRetainingPath(Object? e, List<String> list) {
if (e is! ArgumentError) {
return false;
}
final msg = e.toString();
list.forEach((s) {
Expect.contains(s, msg);
});
return list.every((s) => msg.contains(s));
}
main() async {
asyncStart();
final rp = ReceivePort();
try {
rp.sendPort.send(Fu.unsendable('fu'));
} catch (e) {
checkForRetainingPath(e, <String>[
'NativeWrapper',
'Baz',
'Fu',
]);
for (final pair in [
[
() => Fu.unsendable('fu'),
["NativeClass", "Baz", "Fu"]
],
[
() => Future.value(123),
["Future"]
],
[
Locked.new,
["Locked"]
],
[
ExtendsLocked.new,
["ExtendsLocked"]
],
[
ImplementsLocked.new,
["ImplementsLocked"]
]
]) {
Expect.throws(() => rp.sendPort.send((pair[0] as Function)()),
(e) => checkForRetainingPath(e, pair[1] as List<String>));
}
try {
await Isolate.spawn((_) {}, Locked());
Expect.fail('spawn should have failed');
} catch (e) {
Expect.isTrue(e is ArgumentError && e.toString().contains("Locked"));
}
runZoned(() {
Expect.throws(() {
final z = Zone.current;
rp.sendPort.send(Zone.current);
}, (e) => checkForRetainingPath(e, <String>['Zone']));
}, zoneValues: {0: 1});
rp.close();
asyncEnd();
}

View file

@ -0,0 +1,46 @@
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// @dart = 2.9
import 'dart:isolate';
import 'package:async_helper/async_helper.dart';
import 'package:expect/expect.dart';
@pragma('vm:isolate-unsendable')
class Locked {}
class ExtendsLocked extends Locked {}
class ImplementsLocked implements Locked {}
main() async {
asyncStart();
final rpExit = ReceivePort();
final rpError = RawReceivePort((e) {
Expect.fail('Spawned isolated failed with $e');
});
final rp = RawReceivePort((e) {
Expect.fail('Received unexpected $e, no objects should have arrived');
});
await Isolate.spawn((sendPort) {
for (final fun in [
() => Locked(),
() => ExtendsLocked(),
() => ImplementsLocked()
]) {
Expect.throws(() {
Isolate.exit(sendPort, fun());
}, (e) {
return e is ArgumentError && e.toString().contains("unsendable object");
});
}
}, rp.sendPort, onError: rpError.sendPort, onExit: rpExit.sendPort);
await rpExit.first;
rpError.close();
rp.close();
asyncEnd();
}

View file

@ -41,26 +41,65 @@ class Fu {
}
}
void checkForRetainingPath(Object e, List<String> list) {
Expect.isTrue(e is ArgumentError);
@pragma("vm:isolate-unsendable")
class Locked {}
class ExtendsLocked extends Locked {}
class ImplementsLocked implements Locked {}
bool checkForRetainingPath(Object e, List<String> list) {
if (e is! ArgumentError) {
return false;
}
final msg = e.toString();
list.forEach((s) {
Expect.contains(s, msg);
});
return list.every((s) => msg.contains(s));
}
main() async {
asyncStart();
final rp = ReceivePort();
try {
rp.sendPort.send(Fu.unsendable('fu'));
} catch (e) {
checkForRetainingPath(e, <String>[
'NativeWrapper',
'Baz',
'Fu',
]);
for (final pair in [
[
() => Fu.unsendable('fu'),
["NativeClass", "Baz", "Fu"]
],
[
() => Future.value(123),
["Future"]
],
[
() => Locked(),
["Locked"]
],
[
() => ExtendsLocked(),
["ExtendsLocked"]
],
[
() => ImplementsLocked(),
["ImplementsLocked"]
]
]) {
Expect.throws(() => rp.sendPort.send((pair[0] as Function)()),
(e) => checkForRetainingPath(e, pair[1] as List<String>));
}
try {
await Isolate.spawn((_) {}, Locked());
Expect.fail('spawn should have failed');
} catch (e) {
Expect.isTrue(e is ArgumentError && e.toString().contains("Locked"));
}
runZoned(() {
Expect.throws(() {
final z = Zone.current;
rp.sendPort.send(Zone.current);
}, (e) => checkForRetainingPath(e, <String>['Zone']));
}, zoneValues: {0: 1});
rp.close();
asyncEnd();
}

View file

@ -1088,6 +1088,7 @@ void ClassFinalizer::FinalizeTypesInClass(const Class& cls) {
bool implements_finalizable =
cls.Name() == Symbols::Finalizable().ptr() &&
Library::UrlOf(cls.library()) == Symbols::DartFfi().ptr();
bool is_isolate_unsendable = cls.is_isolate_unsendable();
// Finalize super class.
Class& super_class = Class::Handle(zone, cls.SuperClass());
@ -1106,6 +1107,8 @@ void ClassFinalizer::FinalizeTypesInClass(const Class& cls) {
cls.set_super_type(super_type);
implements_finalizable |=
Class::ImplementsFinalizable(super_type.type_class());
is_isolate_unsendable |=
Class::IsIsolateUnsendable(super_type.type_class());
}
// Finalize interface types (but not necessarily interface classes).
const auto& interface_types = Array::Handle(zone, cls.interfaces());
@ -1120,9 +1123,12 @@ void ClassFinalizer::FinalizeTypesInClass(const Class& cls) {
interface_types.SetAt(i, interface_type);
implements_finalizable |=
Class::ImplementsFinalizable(interface_type.type_class());
is_isolate_unsendable |=
Class::IsIsolateUnsendable(interface_type.type_class());
}
cls.set_implements_finalizable(implements_finalizable);
cls.set_is_type_finalized();
cls.set_is_isolate_unsendable(is_isolate_unsendable);
RegisterClassInHierarchy(thread->zone(), cls);
#endif // defined(DART_PRECOMPILED_RUNTIME)

View file

@ -192,7 +192,7 @@ KernelLoader::KernelLoader(Program* program,
patch_classes_(Array::ZoneHandle(zone_)),
active_class_(),
library_kernel_offset_(-1), // Set to the correct value in LoadLibrary
correction_offset_(-1), // Set to the correct value in LoadLibrary
correction_offset_(-1), // Set to the correct value in LoadLibrary
loading_native_wrappers_library_(false),
library_kernel_data_(ExternalTypedData::ZoneHandle(zone_)),
kernel_program_info_(KernelProgramInfo::ZoneHandle(zone_)),
@ -1045,6 +1045,7 @@ void KernelLoader::FinishTopLevelClassLoading(
bool has_pragma_annotation;
ReadVMAnnotations(library, annotation_count, /*native_name=*/nullptr,
/*is_invisible_function=*/nullptr,
/*is_isolate_unsendable=*/nullptr,
&has_pragma_annotation);
field_helper.SetJustRead(FieldHelper::kAnnotations);
@ -1356,8 +1357,13 @@ void KernelLoader::LoadClass(const Library& library,
class_helper.ReadUntilExcluding(ClassHelper::kAnnotations);
intptr_t annotation_count = helper_.ReadListLength();
bool has_pragma_annotation = false;
bool is_isolate_unsendable = false;
ReadVMAnnotations(library, annotation_count, /*native_name=*/nullptr,
/*is_invisible_function=*/nullptr, &has_pragma_annotation);
/*is_invisible_function=*/nullptr, &is_isolate_unsendable,
&has_pragma_annotation);
if (is_isolate_unsendable) {
out_class->set_is_isolate_unsendable(true);
}
if (has_pragma_annotation) {
out_class->set_has_pragma(true);
}
@ -1437,6 +1443,7 @@ void KernelLoader::FinishClassLoading(const Class& klass,
bool has_pragma_annotation;
ReadVMAnnotations(library, annotation_count, /*native_name=*/nullptr,
/*is_invisible_function=*/nullptr,
/*is_isolate_unsendable=*/nullptr,
&has_pragma_annotation);
field_helper.SetJustRead(FieldHelper::kAnnotations);
@ -1563,7 +1570,8 @@ void KernelLoader::FinishClassLoading(const Class& klass,
bool has_pragma_annotation;
bool is_invisible_function;
ReadVMAnnotations(library, annotation_count, /*native_name=*/nullptr,
&is_invisible_function, &has_pragma_annotation);
&is_invisible_function, /*isolate_unsendable=*/nullptr,
&has_pragma_annotation);
constructor_helper.SetJustRead(ConstructorHelper::kAnnotations);
constructor_helper.ReadUntilExcluding(ConstructorHelper::kFunction);
@ -1705,7 +1713,8 @@ void KernelLoader::FinishLoading(const Class& klass) {
class_index, &class_helper);
}
// Read annotations on a procedure to identify potential VM-specific directives.
// Read annotations on a procedure or a class to identify potential VM-specific
// directives.
//
// Output parameters:
//
@ -1720,6 +1729,7 @@ void KernelLoader::ReadVMAnnotations(const Library& library,
intptr_t annotation_count,
String* native_name,
bool* is_invisible_function,
bool* is_isolate_unsendable,
bool* has_pragma_annotation) {
if (is_invisible_function != nullptr) {
*is_invisible_function = false;
@ -1757,6 +1767,12 @@ void KernelLoader::ReadVMAnnotations(const Library& library,
constant_reader.GetStringConstant(options_index, native_name);
}
}
if (is_isolate_unsendable != nullptr) {
if (constant_reader.IsStringConstant(name_index,
"vm:isolate-unsendable")) {
*is_isolate_unsendable = true;
}
}
}
} else {
helper_.SkipExpression();
@ -1796,7 +1812,8 @@ void KernelLoader::LoadProcedure(const Library& library,
bool is_invisible_function;
const intptr_t annotation_count = helper_.ReadListLength();
ReadVMAnnotations(library, annotation_count, &native_name,
&is_invisible_function, &has_pragma_annotation);
&is_invisible_function, /*isolate_unsendable=*/nullptr,
&has_pragma_annotation);
is_external = is_external && native_name.IsNull();
procedure_helper.SetJustRead(ProcedureHelper::kAnnotations);
const Object& script_class =

View file

@ -228,6 +228,7 @@ class KernelLoader : public ValueObject {
intptr_t annotation_count,
String* native_name,
bool* is_invisible_function,
bool* is_isolate_unsendable,
bool* has_pragma_annotation);
KernelLoader(const Script& script,

View file

@ -3149,6 +3149,11 @@ void Class::set_has_pragma(bool value) const {
set_state_bits(HasPragmaBit::update(value, state_bits()));
}
void Class::set_is_isolate_unsendable(bool value) const {
ASSERT(IsolateGroup::Current()->program_lock()->IsCurrentThreadWriter());
set_state_bits(IsIsolateUnsendableBit::update(value, state_bits()));
}
void Class::set_implements_finalizable(bool value) const {
ASSERT(IsolateGroup::Current()->program_lock()->IsCurrentThreadWriter());
set_state_bits(ImplementsFinalizableBit::update(value, state_bits()));
@ -4992,6 +4997,7 @@ ClassPtr Class::NewNativeWrapper(const Library& library,
cls.set_is_declaration_loaded();
cls.set_is_type_finalized();
cls.set_is_synthesized_class();
cls.set_is_isolate_unsendable(true);
NOT_IN_PRECOMPILED(cls.set_implementor_cid(kDynamicCid));
library.AddClass(cls);
return cls.ptr();

View file

@ -1683,6 +1683,9 @@ class Class : public Object {
ASSERT(Class::Handle(clazz).is_type_finalized());
return ImplementsFinalizableBit::decode(clazz->untag()->state_bits_);
}
static bool IsIsolateUnsendable(ClassPtr clazz) {
return IsIsolateUnsendableBit::decode(clazz->untag()->state_bits_);
}
#if !defined(DART_PRECOMPILED_RUNTIME)
CodePtr allocation_stub() const { return untag()->allocation_stub(); }
@ -1924,6 +1927,12 @@ class Class : public Object {
kBaseClassBit,
kInterfaceClassBit,
kFinalBit,
// Whether instances of the class cannot be sent across ports.
//
// Will be true iff
// - class is marked with `@pramga('vm:isolate-unsendable')
// - super class / super interface classes are marked as unsendable.
kIsIsolateUnsendableBit,
};
class ConstBit : public BitField<uint32_t, bool, kConstBit, 1> {};
class ImplementedBit : public BitField<uint32_t, bool, kImplementedBit, 1> {};
@ -1954,6 +1963,8 @@ class Class : public Object {
class InterfaceClassBit
: public BitField<uint32_t, bool, kInterfaceClassBit, 1> {};
class FinalBit : public BitField<uint32_t, bool, kFinalBit, 1> {};
class IsIsolateUnsendableBit
: public BitField<uint32_t, bool, kIsIsolateUnsendableBit, 1> {};
void set_name(const String& value) const;
void set_user_name(const String& value) const;
@ -1993,7 +2004,12 @@ class Class : public Object {
void set_num_type_arguments_unsafe(intptr_t value) const;
bool has_pragma() const { return HasPragmaBit::decode(state_bits()); }
void set_has_pragma(bool has_pragma) const;
void set_has_pragma(bool value) const;
void set_is_isolate_unsendable(bool value) const;
bool is_isolate_unsendable() const {
return IsIsolateUnsendableBit::decode(state_bits());
}
bool implements_finalizable() const {
ASSERT(is_type_finalized());

View file

@ -826,29 +826,17 @@ class ObjectCopyBase {
DART_FORCE_INLINE
bool CanCopyObject(uword tags, ObjectPtr object) {
const auto cid = UntaggedObject::ClassIdTag::decode(tags);
if (Class::IsIsolateUnsendable(class_table_->At(cid))) {
exception_msg_ = OS::SCreate(
zone_,
"Illegal argument in isolate message: object is unsendable - %s ("
"see restrictions listed at `SendPort.send()` documentation "
"for more information)",
Class::Handle(class_table_->At(cid)).ToCString());
exception_unexpected_object_ = object;
return false;
}
if (cid > kNumPredefinedCids) {
const bool has_native_fields =
Class::NumNativeFieldsOf(class_table_->At(cid)) != 0;
if (has_native_fields) {
exception_msg_ =
OS::SCreate(zone_,
"Illegal argument in isolate message: (object extends "
"NativeWrapper - %s)",
Class::Handle(class_table_->At(cid)).ToCString());
exception_unexpected_object_ = object;
return false;
}
const bool implements_finalizable =
Class::ImplementsFinalizable(class_table_->At(cid));
if (implements_finalizable) {
exception_msg_ = OS::SCreate(
zone_,
"Illegal argument in isolate message: (object implements "
"Finalizable - %s)",
Class::Handle(class_table_->At(cid)).ToCString());
exception_unexpected_object_ = object;
return false;
}
return true;
}
#define HANDLE_ILLEGAL_CASE(Type) \
@ -1030,12 +1018,9 @@ class RetainingPath {
}
// These we are not expected to drill into as they can't be on
// retaining path, they are illegal to send.
if (cid >= kNumPredefinedCids) {
klass = class_table->At(cid);
if ((klass.num_native_fields() != 0) ||
(klass.implements_finalizable())) {
break;
}
klass = class_table->At(cid);
if (klass.is_isolate_unsendable()) {
break;
}
} else {
ASSERT(traversal_rules_ ==

View file

@ -517,6 +517,7 @@ class ObjectPointerVisitor;
V(vm_ffi_native_assets, "vm:ffi:native-assets") \
V(vm_ffi_struct_fields, "vm:ffi:struct-fields") \
V(vm_invisible, "vm:invisible") \
V(vm_isolate_unsendable, "vm:isolate-unsendable") \
V(vm_never_inline, "vm:never-inline") \
V(vm_non_nullable_result_type, "vm:non-nullable-result-type") \
V(vm_notify_debugger_on_exception, "vm:notify-debugger-on-exception") \

View file

@ -10,6 +10,7 @@ import 'dart:typed_data';
@patch
@pragma("vm:entry-point")
@vmIsolateUnsendable
abstract interface class Finalizable {}
@patch

View file

@ -116,7 +116,8 @@ import "dart:_internal"
printToConsole,
Since,
typeAcceptsNull,
unsafeCast;
unsafeCast,
vmIsolateUnsendable;
part 'async_error.dart';
part 'broadcast_stream_controller.dart';

View file

@ -224,6 +224,7 @@ abstract class FutureOr<T> {
/// called. That situation should generally be avoided if possible, unless
/// it's very clearly documented.
@pragma("wasm:entry-point")
@vmIsolateUnsendable
abstract interface class Future<T> {
/// A `Future<Null>` completed with `null`.
///
@ -1163,6 +1164,7 @@ class TimeoutException implements Exception {
/// }
/// }
/// ```
@vmIsolateUnsendable
abstract interface class Completer<T> {
/// Creates a new completer.
///

View file

@ -133,6 +133,7 @@ typedef void _TimerCallback();
/// A broadcast stream inheriting from [Stream] must override [isBroadcast]
/// to return `true` if it wants to signal that it behaves like a broadcast
/// stream.
@vmIsolateUnsendable
abstract mixin class Stream<T> {
const Stream();

View file

@ -33,6 +33,7 @@ part of dart.async;
///
/// See also:
/// * [Stopwatch] for measuring elapsed time.
@vmIsolateUnsendable
abstract interface class Timer {
/// Creates a new timer.
///

View file

@ -570,6 +570,7 @@ abstract final class ZoneDelegate {
/// Similarly, zones provide [bindCallbackGuarded] (and the corresponding
/// [bindUnaryCallbackGuarded] and [bindBinaryCallbackGuarded]), when the
/// callback should be invoked through [Zone.runGuarded].
@vmIsolateUnsendable
abstract final class Zone {
// Private constructor so that it is not possible instantiate a Zone class.
Zone._();

View file

@ -1018,3 +1018,19 @@ class DoubleLinkedQueueEntry<E> {
/// The next entry, or `null` if there is none.
DoubleLinkedQueueEntry<E>? nextEntry() => _nextLink;
}
/// Annotation on a class preventing instances from being sent between isolates.
///
/// Applies to class, mixin or enum declarations, and is inherited by subclasses
/// along extends, with and implements relations.
///
/// An instance of a class with this annotation will be prevented from being
/// part of isolate communication. It cannot be sent through a SendPort,
/// not even if both ends are in the same isolate, and it cannot be part of
/// the initial message of isolate spawn operations.
///
/// The annotation is intended for classes which have a dynamic link
/// to the current isolate, for example being tied to the event loop
/// through scheduled events or timers, which would put the object into
/// an inconsistent state if simply being copied.
const vmIsolateUnsendable = pragma("vm:isolate-unsendable");

View file

@ -800,6 +800,10 @@ abstract class SendPort implements Capability {
/// - [UserTag]
/// - `MirrorReference`
///
/// Instances of classes that either themselves are marked with
/// `@pragma('vm:isolate-unsendable')`, extend or implement such classes
/// cannot be sent through the ports.
///
/// Apart from those exceptions any object can be sent. Objects that are
/// identified as immutable (e.g. strings) will be shared whereas all other
/// objects will be copied.

View file

@ -62,7 +62,7 @@ Future<void> testSendAndExitFinalizable() async {
receivePort.sendPort,
);
final result = await receivePort.first;
Expect.contains("Invalid argument: is a Finalizable", result);
Expect.contains("Invalid argument: is unsendable", result);
}
Future<void> testSendAndExitFinalizer() async {
@ -79,5 +79,5 @@ Future<void> testSendAndExitFinalizer() async {
receivePort.sendPort,
);
final result = await receivePort.first;
Expect.contains("Invalid argument: is a Finalizable", result);
Expect.contains("Invalid argument: is unsendable", result);
}

View file

@ -64,7 +64,7 @@ Future<void> testSendAndExitFinalizable() async {
receivePort.sendPort,
);
final result = await receivePort.first;
Expect.contains("Invalid argument: is a Finalizable", result);
Expect.contains("Invalid argument: is unsendable", result);
}
Future<void> testSendAndExitFinalizer() async {
@ -81,5 +81,5 @@ Future<void> testSendAndExitFinalizer() async {
receivePort.sendPort,
);
final result = await receivePort.first;
Expect.contains("Invalid argument: is a Finalizable", result);
Expect.contains("Invalid argument: is unsendable", result);
}

View file

@ -27,7 +27,7 @@ main() {
asyncStart();
Future<Isolate?>.value(snd).catchError((e) {
Expect.isTrue(e is ArgumentError);
Expect.isTrue("$e".contains("NativeWrapper"));
Expect.isTrue("$e".contains("Test"));
port.close();
asyncEnd();
});

View file

@ -29,7 +29,7 @@ main() {
asyncStart();
snd.catchError((e) {
Expect.isTrue(e is ArgumentError);
Expect.isTrue("$e".contains("NativeWrapper"));
Expect.isTrue("$e".contains("Test"));
port.close();
asyncEnd();
});