@mobrowser/native 2.17.0 → 2.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/package.json +6 -2
- package/rust/mobrowser/Cargo.toml +12 -0
- package/rust/mobrowser/src/ffi.rs +104 -0
- package/rust/mobrowser/src/lib.rs +103 -0
- package/rust/mobrowser/src/oneshot.rs +65 -0
- package/rust/mobrowser/src/responder.rs +63 -0
- package/rust/mobrowser/src/runtime.rs +66 -0
- package/rust/mobrowser/src/tests.rs +223 -0
- package/rust/mobrowser/src/transport.rs +145 -0
- package/rust/mobrowser-build/Cargo.toml +10 -0
- package/rust/mobrowser-build/src/lib.rs +314 -0
package/README.md
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
This package contains the configuration files for MōBrowser native integration
|
|
1
|
+
This package contains the configuration files for MōBrowser native integration: the CMake toolchain
|
|
2
|
+
and headers for C++ modules, and the `mobrowser` and `mobrowser-build` crates for Rust modules.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mobrowser/native",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.18.0",
|
|
4
4
|
"description": "Configuration files for MōBrowser native integration",
|
|
5
5
|
"mobrowserSbomRelationship": "BUILD_DEPENDENCY_OF",
|
|
6
6
|
"main": "cmake/MoBrowser.cmake",
|
|
@@ -11,7 +11,11 @@
|
|
|
11
11
|
"cmake/ProtobufCompat.cmake",
|
|
12
12
|
"cmake/ProtobufDownload.cmake",
|
|
13
13
|
"include",
|
|
14
|
-
"internal"
|
|
14
|
+
"internal",
|
|
15
|
+
"rust/mobrowser/Cargo.toml",
|
|
16
|
+
"rust/mobrowser/src",
|
|
17
|
+
"rust/mobrowser-build/Cargo.toml",
|
|
18
|
+
"rust/mobrowser-build/src"
|
|
15
19
|
],
|
|
16
20
|
"keywords": [
|
|
17
21
|
"mobrowser",
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "mobrowser"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2024"
|
|
5
|
+
rust-version = "1.85"
|
|
6
|
+
description = "MōBrowser native module runtime"
|
|
7
|
+
publish = false
|
|
8
|
+
|
|
9
|
+
[dependencies]
|
|
10
|
+
prost = "0.14"
|
|
11
|
+
prost-types = "0.14"
|
|
12
|
+
tokio = { version = "1", default-features = false, features = ["rt-multi-thread"] }
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Copyright (c) 2000-2026 TeamDev. All rights reserved.
|
|
2
|
+
// TeamDev PROPRIETARY and CONFIDENTIAL.
|
|
3
|
+
// Use is subject to license terms.
|
|
4
|
+
|
|
5
|
+
//! The C ABI between MōBrowser and the native module. Used by [`crate::main!`].
|
|
6
|
+
|
|
7
|
+
use std::any::Any;
|
|
8
|
+
use std::borrow::Cow;
|
|
9
|
+
use std::cell::RefCell;
|
|
10
|
+
use std::ffi::{c_char, CStr};
|
|
11
|
+
use std::panic::{catch_unwind, AssertUnwindSafe};
|
|
12
|
+
use std::sync::Once;
|
|
13
|
+
|
|
14
|
+
use crate::responder::Responder;
|
|
15
|
+
use crate::transport;
|
|
16
|
+
|
|
17
|
+
pub type ReplyCallback = unsafe extern "C" fn(id: u64, data: *const c_char, size: usize);
|
|
18
|
+
pub type RejectCallback = unsafe extern "C" fn(id: u64, error: *const c_char);
|
|
19
|
+
pub type HostInvokeCallback = unsafe extern "C" fn(
|
|
20
|
+
id: u64,
|
|
21
|
+
service: *const c_char,
|
|
22
|
+
method: *const c_char,
|
|
23
|
+
data: *const c_char,
|
|
24
|
+
size: usize,
|
|
25
|
+
reply: ReplyCallback,
|
|
26
|
+
reject: RejectCallback,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
thread_local! {
|
|
30
|
+
static LAST_PANIC: RefCell<String> = const { RefCell::new(String::new()) };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// Records each panic's message for the rejection of the call it aborts,
|
|
34
|
+
/// keeping the previous hook, which reports it.
|
|
35
|
+
fn install_panic_hook() {
|
|
36
|
+
static ONCE: Once = Once::new();
|
|
37
|
+
ONCE.call_once(|| {
|
|
38
|
+
let previous = std::panic::take_hook();
|
|
39
|
+
std::panic::set_hook(Box::new(move |info| {
|
|
40
|
+
let message = panic_message(info.payload());
|
|
41
|
+
LAST_PANIC.with(|last| message.clone_into(&mut last.borrow_mut()));
|
|
42
|
+
previous(info);
|
|
43
|
+
}));
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
pub(crate) fn panic_message(payload: &(dyn Any + Send)) -> &str {
|
|
48
|
+
payload
|
|
49
|
+
.downcast_ref::<&str>()
|
|
50
|
+
.copied()
|
|
51
|
+
.or_else(|| payload.downcast_ref::<String>().map(String::as_str))
|
|
52
|
+
.unwrap_or("unknown panic")
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
pub(crate) fn last_panic_message() -> String {
|
|
56
|
+
LAST_PANIC.with(|last| last.borrow().clone())
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
pub fn app_main(launch: fn()) {
|
|
60
|
+
install_panic_hook();
|
|
61
|
+
if let Err(panic) = catch_unwind(launch) {
|
|
62
|
+
transport::set_launch_error(format!("panic: {}", panic_message(&*panic)));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// # Safety
|
|
67
|
+
///
|
|
68
|
+
/// `service` and `method` must be valid C strings and `data` must point to
|
|
69
|
+
/// `size` readable bytes, all for the duration of the call.
|
|
70
|
+
pub unsafe fn invoke(
|
|
71
|
+
id: u64,
|
|
72
|
+
service: *const c_char,
|
|
73
|
+
method: *const c_char,
|
|
74
|
+
data: *const c_char,
|
|
75
|
+
size: usize,
|
|
76
|
+
reply: ReplyCallback,
|
|
77
|
+
reject: RejectCallback,
|
|
78
|
+
) {
|
|
79
|
+
install_panic_hook();
|
|
80
|
+
let responder = Responder::new(id, reply, reject);
|
|
81
|
+
let service = unsafe { c_str(service) };
|
|
82
|
+
let method = unsafe { c_str(method) };
|
|
83
|
+
let data = if data.is_null() || size == 0 {
|
|
84
|
+
&[]
|
|
85
|
+
} else {
|
|
86
|
+
unsafe { std::slice::from_raw_parts(data.cast::<u8>(), size) }
|
|
87
|
+
};
|
|
88
|
+
// A panicking handler drops the responder while unwinding, which rejects
|
|
89
|
+
// the call with the panic message.
|
|
90
|
+
let _ =
|
|
91
|
+
catch_unwind(AssertUnwindSafe(|| transport::invoke(&service, &method, data, responder)));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
pub fn set_host_invoke(host_invoke: Option<HostInvokeCallback>) {
|
|
95
|
+
install_panic_hook();
|
|
96
|
+
transport::set_host_invoke(host_invoke);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
unsafe fn c_str<'a>(s: *const c_char) -> Cow<'a, str> {
|
|
100
|
+
if s.is_null() {
|
|
101
|
+
return Cow::Borrowed("");
|
|
102
|
+
}
|
|
103
|
+
unsafe { CStr::from_ptr(s) }.to_string_lossy()
|
|
104
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Copyright (c) 2000-2026 TeamDev. All rights reserved.
|
|
2
|
+
// TeamDev PROPRIETARY and CONFIDENTIAL.
|
|
3
|
+
// Use is subject to license terms.
|
|
4
|
+
|
|
5
|
+
//! Runtime for MōBrowser native modules written in Rust.
|
|
6
|
+
//!
|
|
7
|
+
//! The module is a `cdylib` named `mobrowser_client` that MōBrowser loads on
|
|
8
|
+
//! startup. Declare the entry point with [`main!`], register services in it with
|
|
9
|
+
//! [`register_service`], and include the code generated by `mobrowser-build`
|
|
10
|
+
//! with [`include_proto!`]:
|
|
11
|
+
//!
|
|
12
|
+
//! ```ignore
|
|
13
|
+
//! mod proto {
|
|
14
|
+
//! mobrowser::include_proto!();
|
|
15
|
+
//! }
|
|
16
|
+
//!
|
|
17
|
+
//! mobrowser::main!(launch);
|
|
18
|
+
//!
|
|
19
|
+
//! fn launch() {
|
|
20
|
+
//! mobrowser::register_service(proto::GreetServiceServer::new(Greeter));
|
|
21
|
+
//! }
|
|
22
|
+
//! ```
|
|
23
|
+
//!
|
|
24
|
+
//! Service methods are `async fn`s that run on the Tokio [`runtime`].
|
|
25
|
+
|
|
26
|
+
mod oneshot;
|
|
27
|
+
mod responder;
|
|
28
|
+
mod transport;
|
|
29
|
+
|
|
30
|
+
pub mod runtime;
|
|
31
|
+
|
|
32
|
+
#[doc(hidden)]
|
|
33
|
+
pub mod ffi;
|
|
34
|
+
|
|
35
|
+
pub use responder::Responder;
|
|
36
|
+
pub use transport::{register_service, request};
|
|
37
|
+
|
|
38
|
+
pub use prost;
|
|
39
|
+
pub use prost_types;
|
|
40
|
+
pub use tokio;
|
|
41
|
+
|
|
42
|
+
/// A native service the main process can call.
|
|
43
|
+
///
|
|
44
|
+
/// Implemented by the `<Name>Server` wrappers that `mobrowser-build` generates;
|
|
45
|
+
/// there is no need to implement it by hand.
|
|
46
|
+
pub trait Service: Send + Sync + 'static {
|
|
47
|
+
/// The fully qualified Protobuf name of the service, `package.Service`.
|
|
48
|
+
fn full_name(&self) -> &'static str;
|
|
49
|
+
|
|
50
|
+
/// Decodes `data` as the request of `method` and dispatches it. The
|
|
51
|
+
/// `responder` is completed or rejected exactly once.
|
|
52
|
+
fn call(&self, method: &str, data: &[u8], responder: Responder);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// Includes the Rust code that `mobrowser-build` generated from the project's
|
|
56
|
+
/// `.proto` files. Messages and services of a `.proto` without a `package` end
|
|
57
|
+
/// up at the top level; the others in a module per package.
|
|
58
|
+
#[macro_export]
|
|
59
|
+
macro_rules! include_proto {
|
|
60
|
+
() => {
|
|
61
|
+
include!(concat!(env!("OUT_DIR"), "/mobrowser_proto.rs"));
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// Exports the entry points MōBrowser looks up in the native module and makes
|
|
66
|
+
/// `$launch` the function it calls once, on startup.
|
|
67
|
+
///
|
|
68
|
+
/// Must be invoked exactly once, in the root of the `mobrowser_client` crate.
|
|
69
|
+
#[macro_export]
|
|
70
|
+
macro_rules! main {
|
|
71
|
+
($launch:path) => {
|
|
72
|
+
#[unsafe(no_mangle)]
|
|
73
|
+
pub extern "C" fn AppMain() {
|
|
74
|
+
$crate::ffi::app_main($launch);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// # Safety
|
|
78
|
+
///
|
|
79
|
+
/// Called by MōBrowser with valid C strings and a buffer of `size` bytes.
|
|
80
|
+
#[unsafe(no_mangle)]
|
|
81
|
+
pub unsafe extern "C" fn invoke(
|
|
82
|
+
id: u64,
|
|
83
|
+
service: *const ::std::ffi::c_char,
|
|
84
|
+
method: *const ::std::ffi::c_char,
|
|
85
|
+
data: *const ::std::ffi::c_char,
|
|
86
|
+
size: usize,
|
|
87
|
+
reply: $crate::ffi::ReplyCallback,
|
|
88
|
+
reject: $crate::ffi::RejectCallback,
|
|
89
|
+
) {
|
|
90
|
+
unsafe { $crate::ffi::invoke(id, service, method, data, size, reply, reject) }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
#[unsafe(no_mangle)]
|
|
94
|
+
pub extern "C" fn set_host_invoke(
|
|
95
|
+
host_invoke: ::std::option::Option<$crate::ffi::HostInvokeCallback>,
|
|
96
|
+
) {
|
|
97
|
+
$crate::ffi::set_host_invoke(host_invoke);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
#[cfg(test)]
|
|
103
|
+
mod tests;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Copyright (c) 2000-2026 TeamDev. All rights reserved.
|
|
2
|
+
// TeamDev PROPRIETARY and CONFIDENTIAL.
|
|
3
|
+
// Use is subject to license terms.
|
|
4
|
+
|
|
5
|
+
//! A minimal, runtime-agnostic one-shot channel.
|
|
6
|
+
|
|
7
|
+
use std::future::Future;
|
|
8
|
+
use std::pin::Pin;
|
|
9
|
+
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
|
|
10
|
+
use std::task::{Context, Poll, Waker};
|
|
11
|
+
|
|
12
|
+
struct Shared<T> {
|
|
13
|
+
value: Option<T>,
|
|
14
|
+
closed: bool,
|
|
15
|
+
waker: Option<Waker>,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
pub(crate) struct Sender<T>(Arc<Mutex<Shared<T>>>);
|
|
19
|
+
|
|
20
|
+
/// Resolves to the sent value, or to `None` if the sender was dropped without
|
|
21
|
+
/// sending.
|
|
22
|
+
pub(crate) struct Receiver<T>(Arc<Mutex<Shared<T>>>);
|
|
23
|
+
|
|
24
|
+
pub(crate) fn channel<T>() -> (Sender<T>, Receiver<T>) {
|
|
25
|
+
let shared = Arc::new(Mutex::new(Shared { value: None, closed: false, waker: None }));
|
|
26
|
+
(Sender(shared.clone()), Receiver(shared))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
fn lock<T>(shared: &Mutex<Shared<T>>) -> MutexGuard<'_, Shared<T>> {
|
|
30
|
+
shared.lock().unwrap_or_else(PoisonError::into_inner)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
impl<T> Sender<T> {
|
|
34
|
+
pub(crate) fn send(self, value: T) {
|
|
35
|
+
lock(&self.0).value = Some(value);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
impl<T> Drop for Sender<T> {
|
|
40
|
+
fn drop(&mut self) {
|
|
41
|
+
let mut shared = lock(&self.0);
|
|
42
|
+
shared.closed = true;
|
|
43
|
+
let waker = shared.waker.take();
|
|
44
|
+
drop(shared);
|
|
45
|
+
if let Some(waker) = waker {
|
|
46
|
+
waker.wake();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
impl<T> Future for Receiver<T> {
|
|
52
|
+
type Output = Option<T>;
|
|
53
|
+
|
|
54
|
+
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<T>> {
|
|
55
|
+
let mut shared = lock(&self.0);
|
|
56
|
+
if let Some(value) = shared.value.take() {
|
|
57
|
+
Poll::Ready(Some(value))
|
|
58
|
+
} else if shared.closed {
|
|
59
|
+
Poll::Ready(None)
|
|
60
|
+
} else {
|
|
61
|
+
shared.waker = Some(cx.waker().clone());
|
|
62
|
+
Poll::Pending
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Copyright (c) 2000-2026 TeamDev. All rights reserved.
|
|
2
|
+
// TeamDev PROPRIETARY and CONFIDENTIAL.
|
|
3
|
+
// Use is subject to license terms.
|
|
4
|
+
|
|
5
|
+
use std::ffi::{c_char, CString};
|
|
6
|
+
|
|
7
|
+
use crate::ffi::{self, RejectCallback, ReplyCallback};
|
|
8
|
+
|
|
9
|
+
const CANCELED: &str = "RPC canceled: callback destroyed without completion.";
|
|
10
|
+
|
|
11
|
+
/// The untyped, one-shot reply channel of a single call from the main process.
|
|
12
|
+
///
|
|
13
|
+
/// Dropping it without replying rejects the call.
|
|
14
|
+
pub struct Responder {
|
|
15
|
+
id: u64,
|
|
16
|
+
handlers: Option<(ReplyCallback, RejectCallback)>,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
impl Responder {
|
|
20
|
+
pub(crate) fn new(id: u64, reply: ReplyCallback, reject: RejectCallback) -> Self {
|
|
21
|
+
Self { id, handlers: Some((reply, reject)) }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// Completes the call with a serialized response.
|
|
25
|
+
pub fn reply(mut self, data: &[u8]) {
|
|
26
|
+
if let Some((reply, _)) = self.handlers.take() {
|
|
27
|
+
// The host copies the bytes before returning.
|
|
28
|
+
let ptr = if data.is_empty() { b"".as_ptr() } else { data.as_ptr() };
|
|
29
|
+
unsafe { reply(self.id, ptr.cast::<c_char>(), data.len()) };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// Fails the call with an error message.
|
|
34
|
+
pub fn reject(mut self, error: impl AsRef<str>) {
|
|
35
|
+
self.reject_with(error.as_ref());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fn reject_with(&mut self, error: &str) {
|
|
39
|
+
if let Some((_, reject)) = self.handlers.take() {
|
|
40
|
+
let error = to_cstring(error);
|
|
41
|
+
unsafe { reject(self.id, error.as_ptr()) };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
impl Drop for Responder {
|
|
47
|
+
fn drop(&mut self) {
|
|
48
|
+
if self.handlers.is_none() {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if std::thread::panicking() {
|
|
52
|
+
let message = format!("panic: {}", ffi::last_panic_message());
|
|
53
|
+
self.reject_with(&message);
|
|
54
|
+
} else {
|
|
55
|
+
self.reject_with(CANCELED);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// Converts `s` to a C string, replacing interior NULs that would truncate it.
|
|
61
|
+
pub(crate) fn to_cstring(s: &str) -> CString {
|
|
62
|
+
CString::new(s.replace('\0', "\u{FFFD}")).expect("NULs were replaced")
|
|
63
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Copyright (c) 2000-2026 TeamDev. All rights reserved.
|
|
2
|
+
// TeamDev PROPRIETARY and CONFIDENTIAL.
|
|
3
|
+
// Use is subject to license terms.
|
|
4
|
+
|
|
5
|
+
//! The Tokio runtime that async service methods run on.
|
|
6
|
+
|
|
7
|
+
use std::future::Future;
|
|
8
|
+
use std::panic::{catch_unwind, AssertUnwindSafe};
|
|
9
|
+
use std::sync::OnceLock;
|
|
10
|
+
use std::task::Poll;
|
|
11
|
+
|
|
12
|
+
use tokio::runtime::{Builder, Handle};
|
|
13
|
+
|
|
14
|
+
use crate::ffi::panic_message;
|
|
15
|
+
use crate::Responder;
|
|
16
|
+
|
|
17
|
+
static HANDLE: OnceLock<Handle> = OnceLock::new();
|
|
18
|
+
|
|
19
|
+
/// Makes async service methods run on the runtime of `handle` instead of the
|
|
20
|
+
/// one MōBrowser creates. Call it in the entry point, before registering
|
|
21
|
+
/// services. Fails with `handle` if a runtime is already in use.
|
|
22
|
+
pub fn set_handle(handle: Handle) -> Result<(), Handle> {
|
|
23
|
+
HANDLE.set(handle)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// The runtime async service methods run on: the one passed to [`set_handle`],
|
|
27
|
+
/// or a multi-threaded runtime created on first use.
|
|
28
|
+
pub fn handle() -> &'static Handle {
|
|
29
|
+
HANDLE.get_or_init(|| {
|
|
30
|
+
let runtime = Builder::new_multi_thread()
|
|
31
|
+
.enable_all()
|
|
32
|
+
.thread_name("mobrowser-tokio")
|
|
33
|
+
.build()
|
|
34
|
+
.expect("failed to start the Tokio runtime");
|
|
35
|
+
let handle = runtime.handle().clone();
|
|
36
|
+
// Keep the runtime until the process exits.
|
|
37
|
+
std::mem::forget(runtime);
|
|
38
|
+
handle
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Runs `future` on the runtime and completes the call with its result. A panic
|
|
43
|
+
/// in `future` rejects the call with `panic: <message>`.
|
|
44
|
+
#[doc(hidden)]
|
|
45
|
+
pub fn spawn_call<T, F>(future: F, responder: Responder)
|
|
46
|
+
where
|
|
47
|
+
T: prost::Message,
|
|
48
|
+
F: Future<Output = Result<T, String>> + Send + 'static,
|
|
49
|
+
{
|
|
50
|
+
let mut future = Box::pin(future);
|
|
51
|
+
handle().spawn(async move {
|
|
52
|
+
let result = std::future::poll_fn(|cx| {
|
|
53
|
+
match catch_unwind(AssertUnwindSafe(|| future.as_mut().poll(cx))) {
|
|
54
|
+
Ok(Poll::Pending) => Poll::Pending,
|
|
55
|
+
Ok(Poll::Ready(result)) => Poll::Ready(Ok(result)),
|
|
56
|
+
Err(panic) => Poll::Ready(Err(panic)),
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
.await;
|
|
60
|
+
match result {
|
|
61
|
+
Ok(Ok(response)) => responder.reply(&response.encode_to_vec()),
|
|
62
|
+
Ok(Err(error)) => responder.reject(error),
|
|
63
|
+
Err(panic) => responder.reject(format!("panic: {}", panic_message(&*panic))),
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// Copyright (c) 2000-2026 TeamDev. All rights reserved.
|
|
2
|
+
// TeamDev PROPRIETARY and CONFIDENTIAL.
|
|
3
|
+
// Use is subject to license terms.
|
|
4
|
+
|
|
5
|
+
use std::ffi::{c_char, CStr};
|
|
6
|
+
use std::future::Future;
|
|
7
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
8
|
+
use std::sync::{Arc, Mutex};
|
|
9
|
+
use std::task::{Context, Poll, Wake, Waker};
|
|
10
|
+
use std::thread::Thread;
|
|
11
|
+
|
|
12
|
+
use crate::{ffi, register_service, request, Responder, Service};
|
|
13
|
+
|
|
14
|
+
#[derive(Debug, PartialEq)]
|
|
15
|
+
enum Outcome {
|
|
16
|
+
Reply(Vec<u8>),
|
|
17
|
+
Reject(String),
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static OUTCOMES: Mutex<Vec<(u64, Outcome)>> = Mutex::new(Vec::new());
|
|
21
|
+
static NEXT_ID: AtomicU64 = AtomicU64::new(1);
|
|
22
|
+
|
|
23
|
+
unsafe extern "C" fn record_reply(id: u64, data: *const c_char, size: usize) {
|
|
24
|
+
let data = unsafe { std::slice::from_raw_parts(data.cast::<u8>(), size) }.to_vec();
|
|
25
|
+
OUTCOMES.lock().unwrap().push((id, Outcome::Reply(data)));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
unsafe extern "C" fn record_reject(id: u64, error: *const c_char) {
|
|
29
|
+
let error = unsafe { CStr::from_ptr(error) }.to_string_lossy().into_owned();
|
|
30
|
+
OUTCOMES.lock().unwrap().push((id, Outcome::Reject(error)));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
fn outcomes(id: u64) -> Vec<Outcome> {
|
|
34
|
+
let mut all = OUTCOMES.lock().unwrap();
|
|
35
|
+
let (mine, rest) = all.drain(..).partition(|(i, _)| *i == id);
|
|
36
|
+
*all = rest;
|
|
37
|
+
mine.into_iter().map(|(_, outcome)| outcome).collect()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// Echoes the request of "Echo", drops the responder of "Drop", and panics in
|
|
41
|
+
/// "Panic", without the runtime.
|
|
42
|
+
struct TestService;
|
|
43
|
+
|
|
44
|
+
impl Service for TestService {
|
|
45
|
+
fn full_name(&self) -> &'static str {
|
|
46
|
+
"test.TestService"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fn call(&self, method: &str, data: &[u8], responder: Responder) {
|
|
50
|
+
match method {
|
|
51
|
+
"Echo" => responder.reply(data),
|
|
52
|
+
"Drop" => drop(responder),
|
|
53
|
+
"Panic" => panic!("boom"),
|
|
54
|
+
_ => responder.reject("Method not found."),
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fn call(service: &str, method: &str, data: &[u8]) -> Vec<Outcome> {
|
|
60
|
+
let id = start_call(service, method, data);
|
|
61
|
+
outcomes(id)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// Invokes `method` and returns the id of the call without waiting for it.
|
|
65
|
+
fn start_call(service: &str, method: &str, data: &[u8]) -> u64 {
|
|
66
|
+
register_service(TestService);
|
|
67
|
+
register_service(AsyncTestService);
|
|
68
|
+
let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
|
|
69
|
+
let service = crate::responder::to_cstring(service);
|
|
70
|
+
let method = crate::responder::to_cstring(method);
|
|
71
|
+
unsafe {
|
|
72
|
+
ffi::invoke(
|
|
73
|
+
id,
|
|
74
|
+
service.as_ptr(),
|
|
75
|
+
method.as_ptr(),
|
|
76
|
+
data.as_ptr().cast(),
|
|
77
|
+
data.len(),
|
|
78
|
+
record_reply,
|
|
79
|
+
record_reject,
|
|
80
|
+
)
|
|
81
|
+
};
|
|
82
|
+
id
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
fn encode(s: &str) -> Vec<u8> {
|
|
86
|
+
prost::Message::encode_to_vec(&s.to_owned())
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#[test]
|
|
90
|
+
fn completes_once() {
|
|
91
|
+
assert_eq!(call("test.TestService", "Echo", &encode("hi")), [Outcome::Reply(encode("hi"))]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
#[test]
|
|
95
|
+
fn dropped_responder_rejects() {
|
|
96
|
+
assert_eq!(
|
|
97
|
+
call("test.TestService", "Drop", &[]),
|
|
98
|
+
[Outcome::Reject("RPC canceled: callback destroyed without completion.".into())]
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
#[test]
|
|
103
|
+
fn panic_rejects() {
|
|
104
|
+
assert_eq!(call("test.TestService", "Panic", &[]), [Outcome::Reject("panic: boom".into())]);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
#[test]
|
|
108
|
+
fn unknown_service_rejects() {
|
|
109
|
+
assert_eq!(call("test.Missing", "Echo", &[]), [Outcome::Reject("Service not found.".into())]);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
#[test]
|
|
113
|
+
fn unknown_method_rejects() {
|
|
114
|
+
assert_eq!(
|
|
115
|
+
call("test.TestService", "Missing", &[]),
|
|
116
|
+
[Outcome::Reject("Method not found.".into())]
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// Replies to "Echo" with the request and rejects everything else.
|
|
121
|
+
unsafe extern "C" fn fake_host(
|
|
122
|
+
id: u64,
|
|
123
|
+
_service: *const c_char,
|
|
124
|
+
method: *const c_char,
|
|
125
|
+
data: *const c_char,
|
|
126
|
+
size: usize,
|
|
127
|
+
reply: ffi::ReplyCallback,
|
|
128
|
+
reject: ffi::RejectCallback,
|
|
129
|
+
) {
|
|
130
|
+
if unsafe { CStr::from_ptr(method) } == c"Echo" {
|
|
131
|
+
unsafe { reply(id, data, size) };
|
|
132
|
+
} else {
|
|
133
|
+
unsafe { reject(id, c"no such method".as_ptr()) };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// Runs `future` to completion on the current thread.
|
|
138
|
+
fn block_on<F: Future>(future: F) -> F::Output {
|
|
139
|
+
struct ThreadWaker(Thread);
|
|
140
|
+
impl Wake for ThreadWaker {
|
|
141
|
+
fn wake(self: Arc<Self>) {
|
|
142
|
+
self.0.unpark();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
let waker = Waker::from(Arc::new(ThreadWaker(std::thread::current())));
|
|
146
|
+
let mut cx = Context::from_waker(&waker);
|
|
147
|
+
let mut future = std::pin::pin!(future);
|
|
148
|
+
loop {
|
|
149
|
+
match future.as_mut().poll(&mut cx) {
|
|
150
|
+
Poll::Ready(output) => return output,
|
|
151
|
+
Poll::Pending => std::thread::park(),
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// One test, since the host is global.
|
|
157
|
+
#[test]
|
|
158
|
+
fn host_requests() {
|
|
159
|
+
let call = |method, text: &str| block_on(request::<_, String>("s", method, &text.to_owned()));
|
|
160
|
+
|
|
161
|
+
ffi::set_host_invoke(None);
|
|
162
|
+
assert_eq!(call("Echo", "a"), Err("Native RPC host is not available.".into()));
|
|
163
|
+
|
|
164
|
+
ffi::set_host_invoke(Some(fake_host));
|
|
165
|
+
assert_eq!(call("Echo", "b"), Ok("b".into()));
|
|
166
|
+
assert_eq!(call("Other", "c"), Err("no such method".into()));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/// Echoes the request of "Echo" after yielding, fails "Fail", and panics in
|
|
170
|
+
/// "Panic", like a generated async server.
|
|
171
|
+
struct AsyncTestService;
|
|
172
|
+
|
|
173
|
+
impl Service for AsyncTestService {
|
|
174
|
+
fn full_name(&self) -> &'static str {
|
|
175
|
+
"test.AsyncTestService"
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
fn call(&self, method: &str, data: &[u8], responder: Responder) {
|
|
179
|
+
let method = method.to_owned();
|
|
180
|
+
let request: String = prost::Message::decode(data).unwrap_or_default();
|
|
181
|
+
crate::runtime::spawn_call(
|
|
182
|
+
async move {
|
|
183
|
+
tokio::task::yield_now().await;
|
|
184
|
+
match method.as_str() {
|
|
185
|
+
"Echo" => Ok(request),
|
|
186
|
+
"Panic" => panic!("async boom"),
|
|
187
|
+
_ => Err("async failure".to_owned()),
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
responder,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/// Waits for the outcome of an async call.
|
|
196
|
+
fn wait_outcomes(id: u64) -> Vec<Outcome> {
|
|
197
|
+
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
|
|
198
|
+
loop {
|
|
199
|
+
let found = outcomes(id);
|
|
200
|
+
if !found.is_empty() || std::time::Instant::now() > deadline {
|
|
201
|
+
return found;
|
|
202
|
+
}
|
|
203
|
+
std::thread::sleep(std::time::Duration::from_millis(1));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
#[test]
|
|
208
|
+
fn async_completes() {
|
|
209
|
+
let id = start_call("test.AsyncTestService", "Echo", &encode("hi"));
|
|
210
|
+
assert_eq!(wait_outcomes(id), [Outcome::Reply(encode("hi"))]);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
#[test]
|
|
214
|
+
fn async_error_rejects() {
|
|
215
|
+
let id = start_call("test.AsyncTestService", "Fail", &[]);
|
|
216
|
+
assert_eq!(wait_outcomes(id), [Outcome::Reject("async failure".into())]);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
#[test]
|
|
220
|
+
fn async_panic_rejects() {
|
|
221
|
+
let id = start_call("test.AsyncTestService", "Panic", &[]);
|
|
222
|
+
assert_eq!(wait_outcomes(id), [Outcome::Reject("panic: async boom".into())]);
|
|
223
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Copyright (c) 2000-2026 TeamDev. All rights reserved.
|
|
2
|
+
// TeamDev PROPRIETARY and CONFIDENTIAL.
|
|
3
|
+
// Use is subject to license terms.
|
|
4
|
+
|
|
5
|
+
use std::collections::HashMap;
|
|
6
|
+
use std::ffi::c_char;
|
|
7
|
+
use std::future::Future;
|
|
8
|
+
use std::panic::{catch_unwind, AssertUnwindSafe};
|
|
9
|
+
use std::sync::{Arc, LazyLock, Mutex, MutexGuard, OnceLock, PoisonError, RwLock};
|
|
10
|
+
|
|
11
|
+
use crate::ffi::HostInvokeCallback;
|
|
12
|
+
use crate::oneshot;
|
|
13
|
+
use crate::responder::{to_cstring, Responder};
|
|
14
|
+
use crate::Service;
|
|
15
|
+
|
|
16
|
+
type ServiceMap = HashMap<&'static str, Arc<dyn Service>>;
|
|
17
|
+
|
|
18
|
+
static SERVICES: LazyLock<RwLock<ServiceMap>> = LazyLock::new(Default::default);
|
|
19
|
+
|
|
20
|
+
/// Why the entry point failed, if it did; calls to services it didn't register
|
|
21
|
+
/// are rejected with it.
|
|
22
|
+
static LAUNCH_ERROR: OnceLock<String> = OnceLock::new();
|
|
23
|
+
|
|
24
|
+
type PendingRequest = Box<dyn FnOnce(Result<Vec<u8>, String>) + Send>;
|
|
25
|
+
|
|
26
|
+
#[derive(Default)]
|
|
27
|
+
struct Host {
|
|
28
|
+
invoke: Option<HostInvokeCallback>,
|
|
29
|
+
next_id: u64,
|
|
30
|
+
pending: HashMap<u64, PendingRequest>,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static HOST: LazyLock<Mutex<Host>> = LazyLock::new(Default::default);
|
|
34
|
+
|
|
35
|
+
fn host() -> MutexGuard<'static, Host> {
|
|
36
|
+
HOST.lock().unwrap_or_else(PoisonError::into_inner)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// Makes `service` callable from the main process. Registering a service with
|
|
40
|
+
/// the name of one registered earlier replaces it.
|
|
41
|
+
pub fn register_service(service: impl Service) {
|
|
42
|
+
let service = Arc::new(service);
|
|
43
|
+
SERVICES.write().unwrap_or_else(PoisonError::into_inner).insert(service.full_name(), service);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
pub(crate) fn invoke(service: &str, method: &str, data: &[u8], responder: Responder) {
|
|
47
|
+
let service = SERVICES.read().unwrap_or_else(PoisonError::into_inner).get(service).cloned();
|
|
48
|
+
match service {
|
|
49
|
+
Some(service) => service.call(method, data, responder),
|
|
50
|
+
None => match LAUNCH_ERROR.get() {
|
|
51
|
+
Some(error) => responder.reject(format!("Native module failed to start: {error}")),
|
|
52
|
+
None => responder.reject("Service not found."),
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
pub(crate) fn set_launch_error(error: String) {
|
|
58
|
+
let _ = LAUNCH_ERROR.set(error);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
pub(crate) fn set_host_invoke(invoke: Option<HostInvokeCallback>) {
|
|
62
|
+
host().invoke = invoke;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// Calls `method` of the main-process `service` with a typed request and
|
|
66
|
+
/// resolves to its response. Works with any async runtime.
|
|
67
|
+
pub fn request<Req, Resp>(
|
|
68
|
+
service: &str,
|
|
69
|
+
method: &str,
|
|
70
|
+
request: &Req,
|
|
71
|
+
) -> impl Future<Output = Result<Resp, String>> + Send + 'static
|
|
72
|
+
where
|
|
73
|
+
Req: prost::Message,
|
|
74
|
+
Resp: prost::Message + Default + 'static,
|
|
75
|
+
{
|
|
76
|
+
let (sender, receiver) = oneshot::channel();
|
|
77
|
+
request_raw(service, method, &request.encode_to_vec(), move |result| sender.send(result));
|
|
78
|
+
async move {
|
|
79
|
+
let data = receiver.await.unwrap_or_else(|| Err("RPC canceled.".to_owned()))?;
|
|
80
|
+
Resp::decode(data.as_slice()).map_err(|_| "Failed to parse RPC response.".to_owned())
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// Calls `method` of the main-process `service` with a serialized request.
|
|
85
|
+
/// `callback` runs exactly once, on the main process's UI thread.
|
|
86
|
+
fn request_raw(
|
|
87
|
+
service: &str,
|
|
88
|
+
method: &str,
|
|
89
|
+
data: &[u8],
|
|
90
|
+
callback: impl FnOnce(Result<Vec<u8>, String>) + Send + 'static,
|
|
91
|
+
) {
|
|
92
|
+
let mut host = host();
|
|
93
|
+
let Some(host_invoke) = host.invoke else {
|
|
94
|
+
drop(host);
|
|
95
|
+
callback(Err("Native RPC host is not available.".to_owned()));
|
|
96
|
+
return;
|
|
97
|
+
};
|
|
98
|
+
host.next_id += 1;
|
|
99
|
+
let id = host.next_id;
|
|
100
|
+
host.pending.insert(id, Box::new(callback));
|
|
101
|
+
drop(host);
|
|
102
|
+
|
|
103
|
+
let service = to_cstring(service);
|
|
104
|
+
let method = to_cstring(method);
|
|
105
|
+
let ptr = if data.is_empty() { b"".as_ptr() } else { data.as_ptr() };
|
|
106
|
+
// The host copies all arguments before returning.
|
|
107
|
+
unsafe {
|
|
108
|
+
host_invoke(
|
|
109
|
+
id,
|
|
110
|
+
service.as_ptr(),
|
|
111
|
+
method.as_ptr(),
|
|
112
|
+
ptr.cast::<c_char>(),
|
|
113
|
+
data.len(),
|
|
114
|
+
on_host_reply,
|
|
115
|
+
on_host_reject,
|
|
116
|
+
)
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
fn complete_request(id: u64, result: Result<Vec<u8>, String>) {
|
|
121
|
+
let Some(callback) = host().pending.remove(&id) else {
|
|
122
|
+
return;
|
|
123
|
+
};
|
|
124
|
+
// Unwinding into the host is undefined behavior; the panic hook has already
|
|
125
|
+
// reported the panic.
|
|
126
|
+
let _ = catch_unwind(AssertUnwindSafe(|| callback(result)));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
unsafe extern "C" fn on_host_reply(id: u64, data: *const c_char, size: usize) {
|
|
130
|
+
let data = if data.is_null() || size == 0 {
|
|
131
|
+
Vec::new()
|
|
132
|
+
} else {
|
|
133
|
+
unsafe { std::slice::from_raw_parts(data.cast::<u8>(), size) }.to_vec()
|
|
134
|
+
};
|
|
135
|
+
complete_request(id, Ok(data));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
unsafe extern "C" fn on_host_reject(id: u64, error: *const c_char) {
|
|
139
|
+
let error = if error.is_null() {
|
|
140
|
+
String::new()
|
|
141
|
+
} else {
|
|
142
|
+
unsafe { std::ffi::CStr::from_ptr(error) }.to_string_lossy().into_owned()
|
|
143
|
+
};
|
|
144
|
+
complete_request(id, Err(error));
|
|
145
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
// Copyright (c) 2000-2026 TeamDev. All rights reserved.
|
|
2
|
+
// TeamDev PROPRIETARY and CONFIDENTIAL.
|
|
3
|
+
// Use is subject to license terms.
|
|
4
|
+
|
|
5
|
+
//! Generates Rust messages and native service bindings from the `.proto` files
|
|
6
|
+
//! of a MōBrowser project. Call it from `build.rs`:
|
|
7
|
+
//!
|
|
8
|
+
//! ```ignore
|
|
9
|
+
//! fn main() -> std::io::Result<()> {
|
|
10
|
+
//! mobrowser_build::compile("src/native/proto")
|
|
11
|
+
//! }
|
|
12
|
+
//! ```
|
|
13
|
+
//!
|
|
14
|
+
//! and include the result with `mobrowser::include_proto!()`.
|
|
15
|
+
//!
|
|
16
|
+
//! For every service, the generated code has:
|
|
17
|
+
//! - `trait <Service>`: implement it, with `async fn`s, to serve calls from the
|
|
18
|
+
//! main process;
|
|
19
|
+
//! - `struct <Service>Server`: pass it to `mobrowser::register_service`;
|
|
20
|
+
//! - `mod <name>_client`: async functions that call the main-process
|
|
21
|
+
//! implementation of the service.
|
|
22
|
+
|
|
23
|
+
use std::fmt::Write as _;
|
|
24
|
+
use std::fs;
|
|
25
|
+
use std::io::{Error, ErrorKind, Result};
|
|
26
|
+
use std::path::{Path, PathBuf};
|
|
27
|
+
|
|
28
|
+
use prost_build::{Config, Method, Service, ServiceGenerator};
|
|
29
|
+
|
|
30
|
+
/// The file in `OUT_DIR` that `mobrowser::include_proto!` includes.
|
|
31
|
+
const INCLUDE_FILE: &str = "mobrowser_proto.rs";
|
|
32
|
+
|
|
33
|
+
/// Compiles every `.proto` file under `proto_dir`, relative to the crate root,
|
|
34
|
+
/// with the `protoc` of the MōBrowser SDK installed in `node_modules`. Set the
|
|
35
|
+
/// `PROTOC` environment variable to use another one.
|
|
36
|
+
pub fn compile(proto_dir: impl AsRef<Path>) -> Result<()> {
|
|
37
|
+
let manifest_dir = PathBuf::from(env("CARGO_MANIFEST_DIR")?);
|
|
38
|
+
let out_dir = PathBuf::from(env("OUT_DIR")?);
|
|
39
|
+
let proto_dir = manifest_dir.join(proto_dir);
|
|
40
|
+
println!("cargo:rerun-if-changed={}", proto_dir.display());
|
|
41
|
+
println!("cargo:rerun-if-env-changed=PROTOC");
|
|
42
|
+
|
|
43
|
+
let mut protos = Vec::new();
|
|
44
|
+
if proto_dir.is_dir() {
|
|
45
|
+
find_protos(&proto_dir, &mut protos)?;
|
|
46
|
+
}
|
|
47
|
+
protos.sort();
|
|
48
|
+
if protos.is_empty() {
|
|
49
|
+
// Keep include_proto!() compiling in a module without services yet.
|
|
50
|
+
return fs::write(out_dir.join(INCLUDE_FILE), "");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let mut includes = vec![proto_dir];
|
|
54
|
+
let mut config = Config::new();
|
|
55
|
+
if std::env::var_os("PROTOC").is_none() {
|
|
56
|
+
let sdk_dir = find_sdk_dir(&manifest_dir)?;
|
|
57
|
+
config.protoc_executable(sdk_dir.join("bin").join(protoc_name()));
|
|
58
|
+
// The well-known types, e.g. google/protobuf/wrappers.proto.
|
|
59
|
+
includes.push(sdk_dir.join("proto"));
|
|
60
|
+
}
|
|
61
|
+
config
|
|
62
|
+
.prost_path("::mobrowser::prost")
|
|
63
|
+
.prost_types_path("::mobrowser::prost_types")
|
|
64
|
+
.include_file(INCLUDE_FILE)
|
|
65
|
+
.service_generator(Box::new(NativeServiceGenerator))
|
|
66
|
+
.compile_protos(&protos, &includes)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
fn env(name: &str) -> Result<String> {
|
|
70
|
+
std::env::var(name).map_err(|_| {
|
|
71
|
+
Error::new(ErrorKind::NotFound, format!("{name} is not set; call compile() from build.rs"))
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fn find_protos(dir: &Path, protos: &mut Vec<PathBuf>) -> Result<()> {
|
|
76
|
+
for entry in fs::read_dir(dir)? {
|
|
77
|
+
let path = entry?.path();
|
|
78
|
+
if path.is_dir() {
|
|
79
|
+
find_protos(&path, protos)?;
|
|
80
|
+
} else if path.extension().is_some_and(|ext| ext == "proto") {
|
|
81
|
+
protos.push(path);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
Ok(())
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fn protoc_name() -> &'static str {
|
|
88
|
+
if cfg!(windows) {
|
|
89
|
+
"protoc.exe"
|
|
90
|
+
} else {
|
|
91
|
+
"protoc"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// Finds the SDK package with a `protoc` for this machine in the project's own
|
|
96
|
+
/// `node_modules`, where `Cargo.toml` finds the crates too; a `protoc` elsewhere
|
|
97
|
+
/// is never run. On Apple Silicon, the x64 SDK runs under Rosetta, so it is a
|
|
98
|
+
/// fallback.
|
|
99
|
+
fn find_sdk_dir(project_dir: &Path) -> Result<PathBuf> {
|
|
100
|
+
let packages: &[&str] = match (std::env::consts::OS, std::env::consts::ARCH) {
|
|
101
|
+
("macos", "aarch64") => &["sdk-darwin-arm64", "sdk-darwin-x64"],
|
|
102
|
+
("macos", _) => &["sdk-darwin-x64"],
|
|
103
|
+
("windows", _) => &["sdk-win-x64"],
|
|
104
|
+
_ => &["sdk-linux-x64"],
|
|
105
|
+
};
|
|
106
|
+
for package in packages {
|
|
107
|
+
let sdk_dir = project_dir.join("node_modules").join("@mobrowser").join(package);
|
|
108
|
+
if sdk_dir.join("bin").join(protoc_name()).is_file() {
|
|
109
|
+
return Ok(sdk_dir);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
Err(Error::new(
|
|
113
|
+
ErrorKind::NotFound,
|
|
114
|
+
format!(
|
|
115
|
+
"protoc not found in node_modules/@mobrowser/{}. Run `npm install`, \
|
|
116
|
+
or point the PROTOC environment variable at a protoc executable.",
|
|
117
|
+
packages[0]
|
|
118
|
+
),
|
|
119
|
+
))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
struct NativeServiceGenerator;
|
|
123
|
+
|
|
124
|
+
impl ServiceGenerator for NativeServiceGenerator {
|
|
125
|
+
fn generate(&mut self, service: Service, buf: &mut String) {
|
|
126
|
+
if let Some(method) =
|
|
127
|
+
service.methods.iter().find(|m| m.client_streaming || m.server_streaming)
|
|
128
|
+
{
|
|
129
|
+
panic!(
|
|
130
|
+
"{}.{}: streaming methods are not supported by native modules",
|
|
131
|
+
service.proto_name, method.proto_name
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
let full_name = if service.package.is_empty() {
|
|
135
|
+
service.proto_name.clone()
|
|
136
|
+
} else {
|
|
137
|
+
format!("{}.{}", service.package, service.proto_name)
|
|
138
|
+
};
|
|
139
|
+
generate_trait(&service, buf);
|
|
140
|
+
generate_server(&service, &full_name, buf);
|
|
141
|
+
generate_client(&service, &full_name, buf);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
fn finalize_package(&mut self, package: &str, buf: &mut String) {
|
|
145
|
+
if let Some(name) = find_duplicate_item(buf) {
|
|
146
|
+
let package = if package.is_empty() { "the root package" } else { package };
|
|
147
|
+
panic!(
|
|
148
|
+
"`{name}` is defined twice in the code generated for {package}. For service \
|
|
149
|
+
`Foo`, it also defines `FooServer` and `mod foo_client`; rename a service or \
|
|
150
|
+
message so that the names don't clash."
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/// Finds a top-level item defined twice, e.g. `foo_client` for services `Foo`
|
|
157
|
+
/// and `FooService`, which rustc would report against the generated file.
|
|
158
|
+
fn find_duplicate_item(buf: &str) -> Option<&str> {
|
|
159
|
+
const KINDS: &[&str] = &["pub struct ", "pub enum ", "pub trait ", "pub mod "];
|
|
160
|
+
let mut names = std::collections::HashSet::new();
|
|
161
|
+
buf.lines()
|
|
162
|
+
.filter_map(|line| KINDS.iter().find_map(|kind| line.strip_prefix(kind)))
|
|
163
|
+
.map(|rest| {
|
|
164
|
+
rest.split(|c: char| !(c.is_alphanumeric() || c == '_' || c == '#'))
|
|
165
|
+
.next()
|
|
166
|
+
.unwrap_or("")
|
|
167
|
+
})
|
|
168
|
+
.find(|name| !names.insert(*name))
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
fn generate_trait(service: &Service, buf: &mut String) {
|
|
172
|
+
service.comments.append_with_indent(0, buf);
|
|
173
|
+
let _ = writeln!(
|
|
174
|
+
buf,
|
|
175
|
+
"#[allow(clippy::all)]\npub trait {}: Send + Sync + 'static {{",
|
|
176
|
+
service.name
|
|
177
|
+
);
|
|
178
|
+
for method in &service.methods {
|
|
179
|
+
method.comments.append_with_indent(1, buf);
|
|
180
|
+
let _ = writeln!(
|
|
181
|
+
buf,
|
|
182
|
+
" fn {}(&self, request: {}) -> impl ::core::future::Future<Output = ::core::result::Result<{}, ::std::string::String>> + ::core::marker::Send;",
|
|
183
|
+
method.name, method.input_type, method.output_type
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
buf.push_str("}\n\n");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
fn generate_server(service: &Service, full_name: &str, buf: &mut String) {
|
|
190
|
+
let name = &service.name;
|
|
191
|
+
let _ = write!(
|
|
192
|
+
buf,
|
|
193
|
+
"/// Makes a [`{name}`] implementation callable from the main process:\n\
|
|
194
|
+
/// `mobrowser::register_service({name}Server::new(service))`. Its methods\n\
|
|
195
|
+
/// run on `mobrowser::runtime`.\n\
|
|
196
|
+
pub struct {name}Server<T>(::std::sync::Arc<T>);\n\n\
|
|
197
|
+
#[allow(clippy::all)]\n\
|
|
198
|
+
impl<T: {name}> {name}Server<T> {{\n \
|
|
199
|
+
pub fn new(service: T) -> Self {{\n Self(::std::sync::Arc::new(service))\n }}\n\
|
|
200
|
+
}}\n\n\
|
|
201
|
+
#[allow(clippy::all)]\n\
|
|
202
|
+
impl<T: {name}> ::mobrowser::Service for {name}Server<T> {{\n \
|
|
203
|
+
fn full_name(&self) -> &'static str {{\n \"{full_name}\"\n }}\n\n \
|
|
204
|
+
fn call(&self, method: &str, data: &[u8], responder: ::mobrowser::Responder) {{\n \
|
|
205
|
+
match method {{\n"
|
|
206
|
+
);
|
|
207
|
+
for method in &service.methods {
|
|
208
|
+
let _ = write!(
|
|
209
|
+
buf,
|
|
210
|
+
" \"{proto_name}\" => match <{input} as ::mobrowser::prost::Message>::decode(data) {{\n \
|
|
211
|
+
::core::result::Result::Ok(request) => {{\n \
|
|
212
|
+
let service = ::std::sync::Arc::clone(&self.0);\n \
|
|
213
|
+
::mobrowser::runtime::spawn_call(async move {{ <T as {name}>::{rust_name}(&*service, request).await }}, responder)\n \
|
|
214
|
+
}}\n \
|
|
215
|
+
::core::result::Result::Err(error) => responder.reject(::std::format!(\"Failed to parse request: {{error}}\")),\n \
|
|
216
|
+
}},\n",
|
|
217
|
+
proto_name = method.proto_name,
|
|
218
|
+
input = method.input_type,
|
|
219
|
+
rust_name = method.name,
|
|
220
|
+
name = service.name,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
buf.push_str(
|
|
224
|
+
" _ => responder.reject(\"Method not found.\"),\n }\n }\n}\n\n",
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
fn generate_client(service: &Service, full_name: &str, buf: &mut String) {
|
|
229
|
+
let _ = writeln!(
|
|
230
|
+
buf,
|
|
231
|
+
"/// Calls the main-process implementation of `{full_name}`.\n\
|
|
232
|
+
#[allow(clippy::all)]\n\
|
|
233
|
+
pub mod {}_client {{",
|
|
234
|
+
client_module_name(&service.proto_name)
|
|
235
|
+
);
|
|
236
|
+
for method in &service.methods {
|
|
237
|
+
write_client_method(full_name, method, buf);
|
|
238
|
+
}
|
|
239
|
+
buf.push_str("}\n\n");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
fn write_client_method(full_name: &str, method: &Method, buf: &mut String) {
|
|
243
|
+
let input = outer_type(&method.input_type);
|
|
244
|
+
let output = outer_type(&method.output_type);
|
|
245
|
+
method.comments.append_with_indent(1, buf);
|
|
246
|
+
let _ = write!(
|
|
247
|
+
buf,
|
|
248
|
+
" pub fn {name}(\n \
|
|
249
|
+
request: &{input},\n \
|
|
250
|
+
) -> impl ::core::future::Future<Output = ::core::result::Result<{output}, ::std::string::String>> + ::core::marker::Send + 'static {{\n \
|
|
251
|
+
::mobrowser::request::<{input}, {output}>(\"{full_name}\", \"{proto_name}\", request)\n \
|
|
252
|
+
}}\n",
|
|
253
|
+
name = method.name,
|
|
254
|
+
proto_name = method.proto_name,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/// Resolves a type path that prost made relative to the package module from the
|
|
259
|
+
/// client module nested in it.
|
|
260
|
+
fn outer_type(rust_type: &str) -> String {
|
|
261
|
+
const PRIMITIVES: &[&str] = &["()", "bool", "f32", "f64", "i32", "i64", "u32", "u64"];
|
|
262
|
+
if rust_type.starts_with("::") || PRIMITIVES.contains(&rust_type) {
|
|
263
|
+
rust_type.to_owned()
|
|
264
|
+
} else {
|
|
265
|
+
format!("super::{rust_type}")
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/// `GreetService` → `greet`, `HTTPClientService` → `http_client`; the same name
|
|
270
|
+
/// as the C++ client instance.
|
|
271
|
+
fn client_module_name(service: &str) -> String {
|
|
272
|
+
let name = match service.strip_suffix("Service") {
|
|
273
|
+
Some(stripped) if !stripped.is_empty() => stripped,
|
|
274
|
+
_ => service,
|
|
275
|
+
};
|
|
276
|
+
let chars: Vec<char> = name.chars().collect();
|
|
277
|
+
let mut result = String::new();
|
|
278
|
+
for (i, &c) in chars.iter().enumerate() {
|
|
279
|
+
if c.is_ascii_uppercase() && !result.is_empty() {
|
|
280
|
+
let previous_is_lower_or_digit =
|
|
281
|
+
chars[i - 1].is_ascii_lowercase() || chars[i - 1].is_ascii_digit();
|
|
282
|
+
let next_is_lower = chars.get(i + 1).is_some_and(char::is_ascii_lowercase);
|
|
283
|
+
if previous_is_lower_or_digit || next_is_lower {
|
|
284
|
+
result.push('_');
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
result.push(c.to_ascii_lowercase());
|
|
288
|
+
}
|
|
289
|
+
result
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
#[cfg(test)]
|
|
293
|
+
mod tests {
|
|
294
|
+
use super::*;
|
|
295
|
+
|
|
296
|
+
#[test]
|
|
297
|
+
fn client_module_names() {
|
|
298
|
+
assert_eq!(client_module_name("GreetService"), "greet");
|
|
299
|
+
assert_eq!(client_module_name("HTTPClientService"), "http_client");
|
|
300
|
+
assert_eq!(client_module_name("Service"), "service");
|
|
301
|
+
assert_eq!(client_module_name("Files2Service"), "files2");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
#[test]
|
|
305
|
+
fn outer_types() {
|
|
306
|
+
assert_eq!(outer_type("Person"), "super::Person");
|
|
307
|
+
assert_eq!(outer_type("super::other::Msg"), "super::super::other::Msg");
|
|
308
|
+
assert_eq!(outer_type("()"), "()");
|
|
309
|
+
assert_eq!(
|
|
310
|
+
outer_type("::mobrowser::prost::alloc::string::String"),
|
|
311
|
+
"::mobrowser::prost::alloc::string::String"
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
}
|