Skip to content

Commit

Permalink
feat: adding rid message channel (#27)
Browse files Browse the repository at this point in the history
* rid-build: adding message channel

- this message channel supports sending message from rust to dart and
  is built into the rid framework along with the following macros
  leveragint it
- `rid:log_info`, `rid:log_debug`, `rid::log_warn` to send messages to be logged in Dart
- `rid::error`, `rid::severe` to report errors to dart

* tests: adding message channel integration tests

* test: field_access no longer checking in Cargo.lock

* rid: upgrading allo_isolate dep

* rid-build: removing unused preamble property from yaml config

* rid-build: including init isolate wrappers with expanded code

* rid-build: re-exporting message channel types
  • Loading branch information
thlorenz authored Sep 15, 2021
1 parent 8836786 commit 852d5d6
Show file tree
Hide file tree
Showing 26 changed files with 561 additions and 651 deletions.
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ exclude = [
"examples/dart/wip",
"tests/dart/field_access",
"tests/dart/apps",
"tests/dart/export"
"tests/dart/export",
"tests/dart/framework"
]

[dependencies]
Expand Down
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ test_rust:
test_integration:
cd $(ROOT)/tests/dart/field_access && $(MAKE) test-all && \
cd $(ROOT)/tests/dart/export && $(MAKE) test-all && \
cd $(ROOT)/tests/dart/apps && $(MAKE) test-all
cd $(ROOT)/tests/dart/apps && $(MAKE) test-all && \
cd $(ROOT)/tests/dart/framework && $(MAKE) test-all

.PHONY: test_rust test_integration test
15 changes: 9 additions & 6 deletions rid-build/dart/_isolate_binding.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:collection';
import 'dart:ffi' as dart_ffi;

bool _initializedIsolate = false;
final _initializedIsolates = HashSet<String>();

// -----------------
// Binding to `allo-isolate` crate
Expand Down Expand Up @@ -34,6 +35,7 @@ typedef _dart_rid_init_isolate = void Function(

void initIsolate(
dart_ffi.DynamicLibrary dl,
String initFunctionName,
int port,
bool isDebugMode,
) {
Expand All @@ -42,16 +44,17 @@ void initIsolate(
dl.lookupFunction<_store_dart_post_cobject_C,
_store_dart_post_cobject_Dart>('store_dart_post_cobject');

final _rid_init_isolate_ptr = dl
.lookup<dart_ffi.NativeFunction<_c_rid_init_isolate>>('rid_init_isolate');
final _rid_init_isolate_ptr =
dl.lookup<dart_ffi.NativeFunction<_c_rid_init_isolate>>(initFunctionName);
final _dart_rid_init_isolate _rid_init_isolate =
_rid_init_isolate_ptr.asFunction<_dart_rid_init_isolate>();

if (!_initializedIsolate || isDebugMode) {
_initializedIsolate = true;
final initializedIsolate = _initializedIsolates.contains(initFunctionName);
if (!initializedIsolate || isDebugMode) {
_initializedIsolates.add(initFunctionName);
_store_dart_post_cobject(dart_ffi.NativeApi.postCObject);
_rid_init_isolate(port);
} else if (_initializedIsolate) {
} else if (initializedIsolate) {
throw Exception(
"The isolate can only be initialized once when not run in debug mode");
}
Expand Down
125 changes: 125 additions & 0 deletions rid-build/dart/_message_channel.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import 'dart:async';
import 'dart:ffi';
import 'dart:isolate';

import '_isolate_binding.dart' show initIsolate;

const String _MSG_SEPARATOR = '^';

enum RidMsgType { Severe, Error, LogWarn, LogInfo, LogDebug }

RidMsgType _ridMsgTypeFromString(String s) {
switch (s.toLowerCase()) {
case "err_severe":
return RidMsgType.Severe;
case "err_error":
return RidMsgType.Error;
case "log_warn":
return RidMsgType.LogWarn;
case "log_info":
return RidMsgType.LogInfo;
case "log_debug":
return RidMsgType.LogDebug;
default:
throw ArgumentError.value(s);
}
}

final _REMOVE_QUOTE_RX = RegExp(r'(^"|"$)');

class RidMsg {
final RidMsgType type;
late final String message;
late final String? details;

RidMsg._(this.type, String message, String? details) {
this.message = message.replaceAll(_REMOVE_QUOTE_RX, '');
this.details = details?.replaceAll(_REMOVE_QUOTE_RX, '');
}

@override
String toString() {
final detailsString = details == null ? '' : ', details: "$details"';
return 'RidMsg{ type: $type, message: "$message"$detailsString }';
}

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RidMsg &&
runtimeType == other.runtimeType &&
type == other.type &&
message == other.message &&
details == other.details;

@override
int get hashCode => type.hashCode ^ message.hashCode ^ details.hashCode;
}

class RidMsgChannel {
final _zone = Zone.current;
final StreamController<RidMsg> _sink;
final DynamicLibrary _dl;
late final RawReceivePort _receivePort;
late final _zonedAdd;

RidMsgChannel._(this._dl, bool isDebugMode)
: _sink = StreamController.broadcast() {
_receivePort =
RawReceivePort(_onReceivedMsg, 'rid::messaging_channel::port');
initIsolate(this._dl, 'rid_init_msg_isolate',
_receivePort.sendPort.nativePort, isDebugMode);
_zonedAdd = _zone.registerUnaryCallback(_add);
}

void _onReceivedMsg(String reply) {
_zone.runUnary(_zonedAdd, reply);
}

void _add(String reply) {
if (!_sink.isClosed) {
_sink.add(_decode(reply));
}
}

RidMsg _decode(String data) {
int sepIdx = data.indexOf(_MSG_SEPARATOR);
final type = data.substring(0, sepIdx);
final msgType = _ridMsgTypeFromString(type);

final msg = data.substring(sepIdx + 1);
sepIdx = msg.indexOf(_MSG_SEPARATOR);
if (sepIdx < 0) {
// No details
return RidMsg._(msgType, msg, null);
} else {
final message = msg.substring(0, sepIdx);
final details = msg.substring(sepIdx + 1);
return RidMsg._(msgType, message, details);
}
}

Stream<RidMsg> get stream => _sink.stream;

int get nativePort {
return _receivePort.sendPort.nativePort;
}

Future<void> dispose() {
_receivePort.close();
return _sink.close();
}

static bool _initialized = false;
static RidMsgChannel instance(
DynamicLibrary dl,
bool isDebugMode,
) {
if (_initialized && !isDebugMode) {
throw Exception(
"The message channel can only be initialized once unless running in debug mode");
}
_initialized = true;
return RidMsgChannel._(dl, isDebugMode);
}
}
3 changes: 2 additions & 1 deletion rid-build/dart/_reply_channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ class ReplyChannel<TReply extends IReply> {
ReplyChannel._(this._dl, this._decode, bool isDebugMode)
: _sink = StreamController.broadcast() {
_receivePort = RawReceivePort(_onReceivedReply, 'rid::reply_channel::port');
initIsolate(this._dl, _receivePort.sendPort.nativePort, isDebugMode);
initIsolate(this._dl, 'rid_init_reply_isolate',
_receivePort.sendPort.nativePort, isDebugMode);
_zonedAdd = _zone.registerUnaryCallback(_add);
}

Expand Down
28 changes: 28 additions & 0 deletions rid-build/dart/_rid_rid.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//
// Exporting Native Library to call Rust functions directly
//
final dart_ffi.DynamicLibrary _dl = _open();

//
// Expose a rid instance which initializes and provides access to various features facilitating Dart/Rust interaction
//
class Rid {
final RidMsgChannel _messageChannel;
Rid._(dart_ffi.DynamicLibrary dl, bool isDebugMode)
: _messageChannel = RidMsgChannel.instance(dl, isDebugMode);

RidMsgChannel get messageChannel => _messageChannel;
}

final rid = Rid._(_dl, _isDebugMode);

// Dart evaluates code lazily and won't initialize some parts in time for Rust to
// properly use it. Therefore when rid_ffi is accessed we enforce initialization of everything
// it might need like the message channel by forcing evaluation of the Rid constructor.
ffigen_bind.NativeLibrary _initRidFFI() {
// ignore: unnecessary_null_comparison
if (rid == null) {}
return ffigen_bind.NativeLibrary(_dl);
}

final rid_ffi = _initRidFFI();
41 changes: 22 additions & 19 deletions rid-build/src/dart_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ use rid_common::{
};

use crate::{
build_target::BuildTarget, constants::FFI_NATIVE_LIBRARY_NAME,
parsed_bindings::ParsedBindings, Project,
build_target::BuildTarget, parsed_bindings::ParsedBindings, Project,
};
const PACKAGE_FFI: &str = "package_ffi";
static RID_WIDGETS: &str = include_str!("../dart/_rid_widgets.dart");
Expand All @@ -15,6 +14,7 @@ static RID_UTILS_DART: &str = include_str!("../dart/_rid_utils_dart.dart");
static STORE_STUB_DART: &str = include_str!("../dart/_store_stub.dart");
static REPLY_CHANNEL_STUB_DART: &str =
include_str!("../dart/_reply_channel_stub.dart");
static RID_CLASS_INSTANTIATION: &str = include_str!("../dart/_rid_rid.dart");

fn dart_string_from_pointer() -> String {
format!(
Expand Down Expand Up @@ -52,27 +52,26 @@ extension Rid_ExtOnString on String {{
)
}

fn load_dynamic_library(constructor: &str) -> String {
format!(
r###"final {dart_ffi}.DynamicLibrary _dl = _open();
final {rid_ffi} = {ffigen_bind}.{constructor}(_dl);"###,
dart_ffi = DART_FFI,
rid_ffi = RID_FFI,
ffigen_bind = FFI_GEN_BIND,
constructor = constructor,
)
}

fn dart_ffi_reexports() -> String {
format!("export 'dart:ffi' show Pointer;\n")
}

fn message_channel_reexports(message_channel: &str) -> String {
format!(
"export '{message_channel}' show RidMsgChannel, RidMsg, RidMsgType;\n",
message_channel = message_channel
)
}

/// Generates Dart code from the provided cbindgen artifact, taking config into account.
pub(crate) struct DartGenerator<'a> {
/// Relative path to the bindings generated by Dart ffigen from where we will put the code
/// generated here.
pub(crate) ffigen_binding: &'a str,

/// Relative path to the message_channel Dart implementation
pub(crate) message_channel: &'a str,

/// Relative path to the reply_channel Dart implementation
pub(crate) reply_channel: &'a str,

Expand Down Expand Up @@ -103,7 +102,6 @@ impl<'a> DartGenerator<'a> {
pub(crate) fn generate(&self) -> String {
let extensions = &self.code_sections.dart_code;
let _enums = &self.code_sections.enums;
let dynamic_library_constructor = FFI_NATIVE_LIBRARY_NAME;
let flutter_widget_overrides = match self.project {
Project::Dart => "",
Project::Flutter(_) => RID_WIDGETS,
Expand Down Expand Up @@ -131,6 +129,9 @@ impl<'a> DartGenerator<'a> {
{dart_ffi_exports}
// Forwarding Dart Types for raw Rust structs
{struct_exports}
// Forwarding MessageChannel Types
{message_channel_exports}
//
// Open Dynamic Library
//
Expand All @@ -146,16 +147,16 @@ impl<'a> DartGenerator<'a> {
{extensions}
{string_from_pointer_extension}
{string_pointer_from_string_extension}
//
// Exporting Native Library to call Rust functions directly
//
{native_export}
{rid_class_instantiation}
{store_stub}
{reply_channel_stub}
"###,
imports = self.dart_imports(),
dart_ffi_exports = dart_ffi_reexports(),
message_channel_exports =
message_channel_reexports(self.message_channel),
struct_exports = self.dart_rust_type_reexports(),
open_dl = self.dart_open_dl(),
flutter_widget_overrides = flutter_widget_overrides,
Expand All @@ -164,7 +165,7 @@ impl<'a> DartGenerator<'a> {
string_from_pointer_extension = dart_string_from_pointer(),
string_pointer_from_string_extension =
dart_string_pointer_from_string(),
native_export = load_dynamic_library(dynamic_library_constructor),
rid_class_instantiation = RID_CLASS_INSTANTIATION,
store_stub = store_stub,
reply_channel_stub = reply_channel_stub
)
Expand All @@ -187,13 +188,15 @@ import 'dart:io' as dart_io;
import 'dart:collection' as {dart_collection};
import 'package:ffi/ffi.dart' as {pack_ffi};
import '{ffigen_binding}' as {ffigen_bind};
import '{message_channel}';
import '{reply_channel}';
{project_specific_imports}
"###,
dart_ffi = DART_FFI,
dart_async = DART_ASYNC,
dart_collection = DART_COLLECTION,
ffigen_binding = self.ffigen_binding,
message_channel = self.message_channel,
reply_channel = self.reply_channel,
pack_ffi = PACKAGE_FFI,
ffigen_bind = FFI_GEN_BIND,
Expand Down
7 changes: 4 additions & 3 deletions rid-build/src/ffigen/yaml_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ const IGNORE_FOR_FILE: [&str; 5] = [
pub struct YamlConfig {
llvm_paths: Vec<&'static str>,
output: String,
preamble: String,
structs_to_prefix_raw: Vec<String>,
header_entry_point: String,
}
Expand All @@ -35,8 +34,10 @@ impl YamlConfig {
);
Self {
llvm_paths: host_props.llvm_paths.clone(),
output: p.path_to_generated_ffigen(project_root).to_string_lossy().to_string(),
preamble: "// ignore_for_file: non_constant_identifier_names, unused_import, unused_field, unused_element, camel_case_types".to_string(),
output: p
.path_to_generated_ffigen(project_root)
.to_string_lossy()
.to_string(),
structs_to_prefix_raw: structs_to_prefix_raw.into(),
header_entry_point: c_bindings[0].to_string_lossy().to_string(),
}
Expand Down
Loading

0 comments on commit 852d5d6

Please sign in to comment.