blob: 28f2ac46161a42846646ac2242d23f7627f0b268 [file] [log] [blame]
// Copyright (c) 2026, 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.
// This is a special situation where we're allowed to import dart:_vmservice
// from outside the core libraries to access the native entrypoints.
//
// See VmTarget in package:vm for the exception for this library to access
// dart:_vmservice.
// ignore: uri_does_not_exist
import 'dart:_vmservice' as vm_service_natives;
import 'dart:async';
import 'dart:convert';
import 'dart:isolate';
import 'dart:typed_data';
import 'package:dart_runtime_service/dart_runtime_service.dart';
import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;
import 'package:logging/logging.dart';
import 'vm_isolate_manager.dart';
/// Allows for sending messages to the native VM service implementation.
class NativeBindings {
factory NativeBindings() => _instance;
NativeBindings._();
static final NativeBindings _instance = NativeBindings._();
static final jsonUtf8Decoder = json.fuse(utf8);
final _logger = Logger('$NativeBindings');
/// Sends a general RPC to the VM for processing.
///
/// The RPC is not executed in the scope of any particular isolate.
Future<RpcResponse> sendToVM({
required String method,
required Map<String, Object?> params,
}) {
final receivePort = RawReceivePort(null, 'VM Message');
final completer = Completer<RpcResponse>();
receivePort.handler = (Object value) {
receivePort.close();
try {
completer.complete(_toResponse(value: value));
} on json_rpc.RpcException catch (e) {
completer.completeError(e);
}
};
vm_service_natives.sendRootServiceMessage(
_toRequest(responsePort: receivePort, method: method, params: params),
);
return completer.future;
}
/// Sends an RPC to a specific isolate for processing.
///
/// The RPC is not executed in the scope of any particular isolate.
Future<RpcResponse> sendToIsolate({
required VmRunningIsolate isolate,
required String method,
required Map<String, Object?> params,
}) {
final receivePort = RawReceivePort(
null,
'Isolate Message (${isolate.name})',
);
// Keep track of receive port associated with the request so we can close
// it if isolate exits before sending a response.
isolate.outstandingRequestPorts.add(receivePort);
final completer = Completer<RpcResponse>();
receivePort.handler = (Object value) {
receivePort.close();
isolate.outstandingRequestPorts.remove(receivePort);
try {
completer.complete(_toResponse(value: value));
} on json_rpc.RpcException catch (e) {
completer.completeError(e);
}
};
if (!vm_service_natives.sendIsolateServiceMessage(
isolate.sendPort,
_toRequest(responsePort: receivePort, method: method, params: params),
)) {
receivePort.close();
isolate.outstandingRequestPorts.remove(receivePort);
_logger.warning('Could not send message to $isolate.');
completer.completeError(RpcException.internalError.toException());
}
return completer.future;
}
/// Notifies the VM to start sending events for [streamId].
///
/// This only needs to be called once the first client has subscribed to the
/// stream. Subsequent subscriptions to the stream are handled by the
/// [EventStreamManager].
///
/// If [includePrivates] is true, private event properties starting with '_'
/// will be included in events. This is false by default to reduce the size of
/// events for clients that don't rely on private properties.
bool streamListen({required String streamId, bool includePrivates = false}) =>
// TODO(bkonyi): handle case where some clients want privates included
// and others don't.
vm_service_natives.vmListenStream(streamId, includePrivates);
/// Notifies the VM to stop sending events for [streamId].
///
/// This only needs to be called once the last client subscribed to the
/// stream has cancelled its subscription or disconnected. While clients have
/// active subscriptions to this stream, stream cancellation requests are
/// handled by the [EventStreamManager].
void streamCancel({required String streamId}) =>
vm_service_natives.vmCancelStream(streamId);
/// Notifies the VM that the VM service server has finished initializing.
void onStart() => vm_service_natives.onStart();
/// Notifies the VM that the VM service server has finished exiting.
void onExit() => vm_service_natives.onExit();
/// Notifies the VM that the VM service server address has been updated.
///
/// If [address] is null, the VM will assume the server is not running.
void onServerAddressChange(String? address) =>
vm_service_natives.onServerAddressChange(address);
RpcResponse _toResponse({required Object value}) {
const kResult = 'result';
const kError = 'error';
const kCode = 'code';
const kMessage = 'message';
const kData = 'data';
final Object? converted;
if (value case final String string) {
converted = json.decode(string);
} else if (value case [final Uint8List utf8String]) {
converted = jsonUtf8Decoder.decode(utf8String);
} else {
RpcException.internalError.throwException(
data: {
'details': 'Unknown response type (${value.runtimeType}: $value)',
},
);
}
if (converted case {kResult: final Map<String, Object?> result}) {
return result;
} else if (converted case {
kError: {kCode: final int code, kMessage: final String message},
}) {
// 'data' is not always present, so it can't be included in the object
// destructuring pattern.
final data = (converted[kError]! as Map<String, Object?>)[kData];
throw json_rpc.RpcException(code, message, data: data);
} else {
RpcException.internalError.throwException();
}
}
// Calls toString on all non-String elements of [list]. We do this so all
// elements in the list are strings, making consumption by C++ simpler.
// This has a side effect that boolean literal values like true become 'true'
// and thus indistinguishable from the string literal 'true'.
static void _convertAllToStringInPlace(List<Object?> list) {
for (var i = 0; i < list.length; i++) {
list[i] = list[i].toString();
}
}
List<Object?> _toRequest({
required RawReceivePort responsePort,
required String method,
required Map<String, Object?> params,
}) {
final parametersAreObjects = _methodNeedsObjectParameters(method);
final keys = params.keys.toList(growable: false);
final values = params.values.cast<Object?>().toList(growable: false);
if (!parametersAreObjects) {
_convertAllToStringInPlace(values);
}
// This is the request ID that will be inserted into the JSON response in
// service.cc. package:json_rpc_2 already handles these IDs, so we just
// pass in a placeholder for now until we can update Service::InvokeMethod
// to not expect it.
// TODO(bkonyi): remove request ID from service message.
const kPlaceholderRequestId = -1;
// Keep in sync with Service::InvokeMethod in service.cc.
return List<Object?>.filled(7, null)
..[0] =
0 // Make room for OOB message type.
..[1] = responsePort.sendPort
..[2] = kPlaceholderRequestId
..[3] = method
..[4] = parametersAreObjects
..[5] = keys
..[6] = values;
}
// We currently support two ways of passing parameters from Dart code to C
// code. The original way always converts the parameters to strings before
// passing them over. Our goal is to convert all C handlers to take the
// parameters as Dart objects but until the conversion is complete, we
// maintain the list of supported methods below.
bool _methodNeedsObjectParameters(String method) {
switch (method) {
case '_listDevFS':
case '_listDevFSFiles':
case '_createDevFS':
case '_deleteDevFS':
case '_writeDevFSFile':
case '_writeDevFSFiles':
case '_readDevFSFile':
case '_spawnUri':
case '_reloadKernel':
case '_reloadSources':
case 'reloadSources':
return true;
default:
return false;
}
}
}