sparate android + ios build targets

This commit is contained in:
Brendan Allan
2023-01-21 14:31:17 +08:00
parent 87286dba13
commit 89f4f7ebca
12 changed files with 357 additions and 387 deletions

BIN
Cargo.lock generated
View File

Binary file not shown.

View File

@@ -7,7 +7,7 @@ members = [
# "crates/p2p/tunnel/utils",
"apps/cli",
"apps/desktop/src-tauri",
"apps/mobile/rust",
"apps/mobile/rust/*",
"apps/server",
]

View File

@@ -1,39 +0,0 @@
[package]
name = "sd-core-mobile"
version = "0.1.0"
edition = "2021"
rust-version = "1.64.0"
[lib]
crate-type = ["staticlib", "cdylib"] # staticlib for IOS and cdylib for Android
[dependencies]
once_cell = "1.15.0"
sd-core = { path = "../../../core", features = [
"mobile",
"p2p",
], default-features = false }
rspc = { workspace = true }
serde_json = "1.0.85"
tokio = "1.21.2"
openssl = { version = "0.10.42", features = [
"vendored",
] } # Override features of transitive dependencies
openssl-sys = { version = "0.9.76", features = [
"vendored",
] } # Override features of transitive dependencies to support IOS Simulator on M1
futures = "0.3.24"
tracing = "0.1.37"
[target.'cfg(target_os = "ios")'.dependencies]
objc = "0.2.7"
objc_id = "0.1.1"
objc-foundation = "0.1.1"
# This is `not(ios)` instead of `android` because of https://github.com/mozilla/rust-android-gradle/issues/93
[target.'cfg(not(target_os = "ios"))'.dependencies]
jni = "0.19.0"
[target.'cfg(not(target_os = "ios"))'.features]
default = ["sd-core/android"]

View File

@@ -0,0 +1,19 @@
[package]
name = "sd-core-android"
version = "0.1.0"
edition = "2021"
rust-version = "1.64.0"
[lib]
# Android can use dynamic linking since all FFI is done via JNI
crate-type = ["cdylib"]
[dependencies]
# FFI
jni = "0.19.0"
# Core mobile handling stuff
sd-core-mobile = { path = "../mobile", features = ["android"] }
# Other
tracing = "0.1.37"

View File

@@ -0,0 +1,105 @@
use std::panic;
use jni::{
objects::{JClass, JObject, JString},
JNIEnv,
};
use sd_core_mobile::*;
use tracing::error;
#[no_mangle]
pub extern "system" fn Java_com_spacedrive_app_SDCore_registerCoreEventListener(
env: JNIEnv,
class: JClass,
) {
let result = panic::catch_unwind(|| {
let jvm = env.get_java_vm().unwrap();
let class = env.new_global_ref(class).unwrap();
spawn_core_event_listener(move |data| {
let env = jvm.attach_current_thread().unwrap();
env.call_method(
&class,
"sendCoreEvent",
"(Ljava/lang/String;)V",
&[env
.new_string(data)
.expect("Couldn't create java string!")
.into()],
)
.unwrap();
})
});
if let Err(err) = result {
// TODO: Send rspc error or something here so we can show this in the UI.
// TODO: Maybe reinitialise the core cause it could be in an invalid state?
println!(
"Error in Java_com_spacedrive_app_SDCore_registerCoreEventListener: {:?}",
err
);
}
}
#[no_mangle]
pub extern "system" fn Java_com_spacedrive_app_SDCore_handleCoreMsg(
env: JNIEnv,
class: JClass,
query: JString,
callback: JObject,
) {
let result = panic::catch_unwind(|| {
let jvm = env.get_java_vm().unwrap();
let query: String = env
.get_string(query)
.expect("Couldn't get java string!")
.into();
let class = env.new_global_ref(class).unwrap();
let callback = env.new_global_ref(callback).unwrap();
let data_directory = {
let env = jvm.attach_current_thread().unwrap();
let data_dir = env
.call_method(&class, "getDataDirectory", "()Ljava/lang/String;", &[])
.unwrap()
.l()
.unwrap();
env.get_string(data_dir.into()).unwrap().into()
};
handle_core_msg(query, data_directory, move |result| match result {
Ok(data) => {
let env = jvm.attach_current_thread().unwrap();
env.call_method(
&callback,
"resolve",
"(Ljava/lang/Object;)V",
&[env
.new_string(data)
.expect("Couldn't create java string!")
.into()],
)
.unwrap();
}
Err(_) => {
// TODO: handle error
}
});
});
if let Err(err) = result {
// TODO: Send rspc error or something here so we can show this in the UI.
// TODO: Maybe reinitialise the core cause it could be in an invalid state?
// TODO: This log statement doesn't work. I recon the JNI env is being dropped before it's called.
error!(
"Error in Java_com_spacedrive_app_SDCore_registerCoreEventListener: {:?}",
err
);
}
}

View File

@@ -0,0 +1,21 @@
[package]
name = "sd-core-ios"
version = "0.1.0"
edition = "2021"
rust-version = "1.64.0"
[lib]
# iOS requires static linking
# Makes sense considering this lib needs to link against call_resolve and get_data_directory,
# which are only available when linking against the app's ObjC
crate-type = ["staticlib"]
[dependencies]
# FFI
objc = "0.2.7"
objc_id = "0.1.1"
objc-foundation = "0.1.1"
# Core mobile handling stuff
sd-core-mobile = { path = "../mobile" }

View File

@@ -0,0 +1,79 @@
use std::{
ffi::{CStr, CString},
os::raw::{c_char, c_void},
panic,
};
use objc::{msg_send, runtime::Object, sel, sel_impl};
use objc_foundation::{INSString, NSString};
use objc_id::Id;
use sd_core_mobile::*;
extern "C" {
fn get_data_directory() -> *const c_char;
fn call_resolve(resolve: *const c_void, result: *const c_char);
}
// This struct wraps the function pointer which represent a Javascript Promise. We wrap the
// function pointers in a struct so we can unsafely assert to Rust that they are `Send`.
// We know they are send as we have ensured Objective-C won't deallocate the function pointer
// until `call_resolve` is called.
struct RNPromise(*const c_void);
unsafe impl Send for RNPromise {}
impl RNPromise {
// resolve the promise
unsafe fn resolve(self, result: CString) {
call_resolve(self.0, result.as_ptr());
}
}
#[no_mangle]
pub unsafe extern "C" fn register_core_event_listener(id: *mut Object) {
let result = panic::catch_unwind(|| {
let id = Id::<Object>::from_ptr(id);
spawn_core_event_listener(move |data| {
let data = NSString::from_str(&data);
let _: () = msg_send![id, sendCoreEvent: data];
});
});
if let Err(err) = result {
// TODO: Send rspc error or something here so we can show this in the UI.
// TODO: Maybe reinitialise the core cause it could be in an invalid state?
println!("Error in register_core_event_listener: {:?}", err);
}
}
#[no_mangle]
pub unsafe extern "C" fn sd_core_msg(query: *const c_char, resolve: *const c_void) {
let result = panic::catch_unwind(|| {
// This string is cloned to the Rust heap. This is important as Objective-C may remove the query once this function completions but prior to the async block finishing.
let query = CStr::from_ptr(query).to_str().unwrap().to_string();
let resolve = RNPromise(resolve);
let data_directory = CStr::from_ptr(get_data_directory())
.to_str()
.unwrap()
.to_string();
handle_core_msg(query, data_directory, |result| {
match result {
Ok(data) => resolve.resolve(CString::new(data).unwrap()),
Err(_) => {
// TODO: handle error
}
}
});
});
if let Err(err) = result {
// TODO: Send rspc error or something here so we can show this in the UI.
// TODO: Maybe reinitialise the core cause it could be in an invalid state?
println!("Error in sd_core_msg: {:?}", err);
}
}

View File

@@ -0,0 +1,26 @@
[package]
name = "sd-core-mobile"
version = "0.1.0"
edition = "2021"
rust-version = "1.64.0"
[features]
android = ["sd-core/android"]
[dependencies]
once_cell = "1.15.0"
sd-core = { path = "../../../../core", features = [
"mobile",
"p2p",
], default-features = false }
rspc.workspace= true
serde_json = "1.0.85"
tokio = "1.21.2"
openssl = { version = "0.10.42", features = [
"vendored",
] } # Override features of transitive dependencies
openssl-sys = { version = "0.9.76", features = [
"vendored",
] } # Override features of transitive dependencies to support IOS Simulator on M1
futures = "0.3.24"
tracing = "0.1.37"

View File

@@ -0,0 +1,106 @@
use futures::future::join_all;
use once_cell::sync::{Lazy, OnceCell};
use rspc::internal::jsonrpc::*;
use sd_core::{api::Router, Node};
use serde_json::{from_str, from_value, to_string, Value};
use std::{collections::HashMap, marker::Send, sync::Arc};
use tokio::{
runtime::Runtime,
sync::{
mpsc::{unbounded_channel, UnboundedSender},
oneshot, Mutex,
},
};
use tracing::error;
pub static RUNTIME: Lazy<Runtime> = Lazy::new(|| Runtime::new().unwrap());
pub type NodeType = Lazy<Mutex<Option<(Arc<Node>, Arc<Router>)>>>;
pub static NODE: NodeType = Lazy::new(|| Mutex::new(None));
pub static SUBSCRIPTIONS: Lazy<Mutex<HashMap<RequestId, oneshot::Sender<()>>>> =
Lazy::new(Default::default);
pub static EVENT_SENDER: OnceCell<UnboundedSender<Response>> = OnceCell::new();
pub fn handle_core_msg(
query: String,
data_dir: String,
callback: impl FnOnce(Result<String, String>) + Send + 'static,
) {
RUNTIME.spawn(async move {
let (node, router) = {
let node = &mut *NODE.lock().await;
match node {
Some(node) => node.clone(),
None => {
// TODO: probably don't unwrap
let new_node = Node::new(data_dir).await.unwrap();
node.replace(new_node.clone());
new_node
}
}
};
let reqs = match from_str::<Value>(&query).and_then(|v| match v.is_array() {
true => from_value::<Vec<Request>>(v),
false => from_value::<Request>(v).map(|v| vec![v]),
}) {
Ok(v) => v,
Err(err) => {
error!("failed to decode JSON-RPC request: {}", err); // Don't use tracing here because it's before the `Node` is initialised which sets that config!
callback(Err(query));
return;
}
};
let responses = join_all(reqs.into_iter().map(|request| {
let node = node.clone();
let router = router.clone();
async move {
let mut channel = EVENT_SENDER.get().unwrap().clone();
let mut resp = Sender::ResponseAndChannel(None, &mut channel);
handle_json_rpc(
node.get_request_context(),
request,
&router,
&mut resp,
&mut SubscriptionMap::Mutex(&SUBSCRIPTIONS),
)
.await;
match resp {
Sender::ResponseAndChannel(resp, _) => resp,
_ => unreachable!(),
}
}
}))
.await;
callback(Ok(serde_json::to_string(
&responses.into_iter().flatten().collect::<Vec<_>>(),
)
.unwrap()));
});
}
pub fn spawn_core_event_listener(callback: impl Fn(String) + Send + 'static) {
let (tx, mut rx) = unbounded_channel();
let _ = EVENT_SENDER.set(tx);
RUNTIME.spawn(async move {
while let Some(event) = rx.recv().await {
let data = match to_string(&event) {
Ok(json) => json,
Err(err) => {
println!("Failed to serialize event: {}", err);
continue;
}
};
callback(data);
}
});
}

View File

@@ -1,177 +0,0 @@
use std::panic;
use crate::{EVENT_SENDER, NODE, RUNTIME, SUBSCRIPTIONS};
use futures::future::join_all;
use jni::objects::{JClass, JObject, JString};
use jni::JNIEnv;
use rspc::internal::jsonrpc::{handle_json_rpc, Request, Sender, SubscriptionMap};
use sd_core::Node;
use serde_json::Value;
use tokio::sync::mpsc::unbounded_channel;
use tracing::{error, info};
#[no_mangle]
pub extern "system" fn Java_com_spacedrive_app_SDCore_registerCoreEventListener(
env: JNIEnv,
class: JClass,
) {
let result = panic::catch_unwind(|| {
let jvm = env.get_java_vm().unwrap();
let class = env.new_global_ref(class).unwrap();
let (tx, mut rx) = unbounded_channel();
let _ = EVENT_SENDER.set(tx);
RUNTIME.spawn(async move {
while let Some(event) = rx.recv().await {
let data = match serde_json::to_string(&event) {
Ok(json) => json,
Err(err) => {
println!("Failed to serialize event: {}", err);
continue;
}
};
let env = jvm.attach_current_thread().unwrap();
env.call_method(
&class,
"sendCoreEvent",
"(Ljava/lang/String;)V",
&[env
.new_string(data)
.expect("Couldn't create java string!")
.into()],
)
.unwrap();
}
});
});
if let Err(err) = result {
// TODO: Send rspc error or something here so we can show this in the UI.
// TODO: Maybe reinitialise the core cause it could be in an invalid state?
println!(
"Error in Java_com_spacedrive_app_SDCore_registerCoreEventListener: {:?}",
err
);
}
}
#[no_mangle]
pub extern "system" fn Java_com_spacedrive_app_SDCore_handleCoreMsg(
env: JNIEnv,
class: JClass,
query: JString,
callback: JObject,
) {
let result = panic::catch_unwind(|| {
let jvm = env.get_java_vm().unwrap();
let query: String = env
.get_string(query)
.expect("Couldn't get java string!")
.into();
let class = env.new_global_ref(class).unwrap();
let callback = env.new_global_ref(callback).unwrap();
RUNTIME.spawn(async move {
let (node, router) = {
let node = &mut *NODE.lock().await;
match node {
Some(node) => node.clone(),
None => {
let data_dir: String = {
let env = jvm.attach_current_thread().unwrap();
let data_dir = env
.call_method(
&class,
"getDataDirectory",
"()Ljava/lang/String;",
&[],
)
.unwrap()
.l()
.unwrap();
env.get_string(data_dir.into()).unwrap().into()
};
let new_node = Node::new(data_dir).await;
let new_node = match new_node {
Ok(new_node) => new_node,
Err(err) => {
info!("677 {:?}", err);
// TODO: Android return?
return;
}
};
node.replace(new_node.clone());
new_node
}
}
};
let reqs =
match serde_json::from_str::<Value>(&query).and_then(|v| match v.is_array() {
true => serde_json::from_value::<Vec<Request>>(v),
false => serde_json::from_value::<Request>(v).map(|v| vec![v]),
}) {
Ok(v) => v,
Err(err) => {
error!("failed to decode JSON-RPC request: {}", err); // Don't use tracing here because it's before the `Node` is initialised which sets that config!
return;
}
};
let resps = join_all(reqs.into_iter().map(|request| {
let node = node.clone();
let router = router.clone();
async move {
let mut channel = EVENT_SENDER.get().unwrap().clone();
let mut resp = Sender::ResponseAndChannel(None, &mut channel);
handle_json_rpc(
node.get_request_context(),
request,
&router,
&mut resp,
&mut SubscriptionMap::Mutex(&SUBSCRIPTIONS),
)
.await;
match resp {
Sender::ResponseAndChannel(resp, _) => resp,
_ => unreachable!(),
}
}
}))
.await;
let env = jvm.attach_current_thread().unwrap();
env.call_method(
&callback,
"resolve",
"(Ljava/lang/Object;)V",
&[env
.new_string(
serde_json::to_string(&resps.into_iter().flatten().collect::<Vec<_>>())
.unwrap(),
)
.expect("Couldn't create java string!")
.into()],
)
.unwrap();
});
});
if let Err(err) = result {
// TODO: Send rspc error or something here so we can show this in the UI.
// TODO: Maybe reinitialise the core cause it could be in an invalid state?
// TODO: This log statement doesn't work. I recon the JNI env is being dropped before it's called.
error!(
"Error in Java_com_spacedrive_app_SDCore_registerCoreEventListener: {:?}",
err
);
}
}

View File

@@ -1,139 +0,0 @@
use crate::{EVENT_SENDER, NODE, RUNTIME, SUBSCRIPTIONS};
use futures::future::join_all;
use objc::{msg_send, runtime::Object, sel, sel_impl};
use objc_foundation::{INSString, NSString};
use objc_id::Id;
use rspc::internal::jsonrpc::{handle_json_rpc, Request, Sender, SubscriptionMap};
use sd_core::Node;
use serde_json::Value;
use std::{
ffi::{CStr, CString},
os::raw::{c_char, c_void},
panic,
};
use tokio::sync::mpsc::unbounded_channel;
extern "C" {
fn get_data_directory() -> *const c_char;
fn call_resolve(resolve: *const c_void, result: *const c_char);
}
// This struct wraps the function pointer which represent a Javascript Promise. We wrap the
// function pointers in a struct so we can unsafely assert to Rust that they are `Send`.
// We know they are send as we have ensured Objective-C won't deallocate the function pointer
// until `call_resolve` is called.
struct RNPromise(*const c_void);
unsafe impl Send for RNPromise {}
impl RNPromise {
// resolve the promise
unsafe fn resolve(self, result: CString) {
call_resolve(self.0, result.as_ptr());
}
}
#[no_mangle]
pub unsafe extern "C" fn register_core_event_listener(id: *mut Object) {
let result = panic::catch_unwind(|| {
let id = Id::<Object>::from_ptr(id);
let (tx, mut rx) = unbounded_channel();
let _ = EVENT_SENDER.set(tx);
RUNTIME.spawn(async move {
while let Some(event) = rx.recv().await {
let data = match serde_json::to_string(&event) {
Ok(json) => json,
Err(err) => {
println!("Failed to serialize event: {}", err);
continue;
}
};
let data = NSString::from_str(&data);
let _: () = msg_send![id, sendCoreEvent: data];
}
});
});
if let Err(err) = result {
// TODO: Send rspc error or something here so we can show this in the UI.
// TODO: Maybe reinitialise the core cause it could be in an invalid state?
println!("Error in register_core_event_listener: {:?}", err);
}
}
#[no_mangle]
pub unsafe extern "C" fn sd_core_msg(query: *const c_char, resolve: *const c_void) {
let result = panic::catch_unwind(|| {
// This string is cloned to the Rust heap. This is important as Objective-C may remove the query once this function completions but prior to the async block finishing.
let query = CStr::from_ptr(query).to_str().unwrap().to_string();
let resolve = RNPromise(resolve);
RUNTIME.spawn(async move {
let reqs =
match serde_json::from_str::<Value>(&query).and_then(|v| match v.is_array() {
true => serde_json::from_value::<Vec<Request>>(v),
false => serde_json::from_value::<Request>(v).map(|v| vec![v]),
}) {
Ok(v) => v,
Err(err) => {
println!("failed to decode JSON-RPC request: {}", err); // Don't use tracing here because it's before the `Node` is initialised which sets that config!
resolve.resolve(
CString::new(serde_json::to_vec(&(vec![] as Vec<Request>)).unwrap())
.unwrap(),
); // TODO: Proper error handling
return;
}
};
let resps = join_all(reqs.into_iter().map(|request| async move {
let node = &mut *NODE.lock().await;
let (node, router) = match node {
Some(node) => node.clone(),
None => {
let data_dir = CStr::from_ptr(get_data_directory())
.to_str()
.unwrap()
.to_string();
let new_node = Node::new(data_dir).await.unwrap();
node.replace(new_node.clone());
new_node
}
};
let mut channel = EVENT_SENDER.get().unwrap().clone();
let mut resp = Sender::ResponseAndChannel(None, &mut channel);
handle_json_rpc(
node.get_request_context(),
request,
&router,
&mut resp,
&mut SubscriptionMap::Mutex(&SUBSCRIPTIONS),
)
.await;
match resp {
Sender::ResponseAndChannel(resp, _) => resp,
_ => unreachable!(),
}
}))
.await;
resolve.resolve(
CString::new(
serde_json::to_vec(&resps.into_iter().filter_map(|v| v).collect::<Vec<_>>())
.unwrap(),
)
.unwrap(),
);
});
});
if let Err(err) = result {
// TODO: Send rspc error or something here so we can show this in the UI.
// TODO: Maybe reinitialise the core cause it could be in an invalid state?
println!("Error in sd_core_msg: {:?}", err);
}
}

View File

@@ -1,31 +0,0 @@
use std::{collections::HashMap, sync::Arc};
use once_cell::sync::{Lazy, OnceCell};
use rspc::internal::jsonrpc::{RequestId, Response};
use sd_core::{api::Router, Node};
use tokio::{
runtime::Runtime,
sync::{mpsc::UnboundedSender, oneshot, Mutex},
};
#[allow(dead_code)]
pub(crate) static RUNTIME: Lazy<Runtime> = Lazy::new(|| Runtime::new().unwrap());
type NodeType = Lazy<Mutex<Option<(Arc<Node>, Arc<Router>)>>>;
#[allow(dead_code)]
pub(crate) static NODE: NodeType = Lazy::new(|| Mutex::new(None));
#[allow(dead_code)]
pub(crate) static SUBSCRIPTIONS: Lazy<Mutex<HashMap<RequestId, oneshot::Sender<()>>>> =
Lazy::new(Default::default);
#[allow(dead_code)]
pub(crate) static EVENT_SENDER: OnceCell<UnboundedSender<Response>> = OnceCell::new();
#[cfg(target_os = "ios")]
mod ios;
/// This is `not(ios)` instead of `android` because of https://github.com/mozilla/rust-android-gradle/issues/93
#[cfg(not(target_os = "ios"))]
mod android;