feat: add API to call iOS plugin (#6242)

This commit is contained in:
Lucas Fernandes Nogueira 2023-02-11 14:45:51 -08:00 committed by GitHub
parent 05dad08768
commit bfb2ab24e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 265 additions and 159 deletions

View File

@ -1,5 +0,0 @@
---
"tauri": patch
---
Added `App::run_android_plugin` and `AppHandle::run_android_plugin`.

View File

@ -0,0 +1,5 @@
---
"tauri": patch
---
Added `App::run_mobile_plugin` and `AppHandle::run_mobile_plugin`.

View File

@ -17,12 +17,12 @@ let package = Package(
dependencies: [
.product(name: "SwiftRs", package: "swift-rs"),
],
path: "core/tauri/ios/Sources/Tauri"
path: "core/tauri/mobile/ios-api/Sources/Tauri"
),
.testTarget(
name: "TauriTests",
dependencies: ["Tauri"],
path: "core/tauri/ios/Tests/TauriTests"
path: "core/tauri/mobile/ios-api/Tests/TauriTests"
),
]
)

View File

@ -1,4 +1,5 @@
import SwiftRs
import Foundation
import MetalKit
import WebKit
import os.log
@ -14,15 +15,15 @@ class PluginHandle {
class PluginManager {
static var shared: PluginManager = PluginManager()
var plugins: [String:PluginHandle] = [:]
var plugins: [String: PluginHandle] = [:]
func onWebviewCreated(_ webview: WKWebView) {
for (_, handle) in plugins {
if (!handle.loaded) {
handle.instance.perform(#selector(Plugin.load), with: webview)
}
}
}
for (_, handle) in plugins {
if (!handle.loaded) {
handle.instance.perform(#selector(Plugin.load), with: webview)
}
}
}
func load<P: Plugin & NSObject>(webview: WKWebView?, name: String, plugin: P) {
let handle = PluginHandle(plugin: plugin)
@ -79,8 +80,8 @@ func onWebviewCreated(webview: WKWebView) {
PluginManager.shared.onWebviewCreated(webview)
}
@_cdecl("invoke_plugin")
func invokePlugin(webview: WKWebView, name: UnsafePointer<SRString>, methodName: UnsafePointer<SRString>, data: NSDictionary, callback: UInt, error: UInt) {
@_cdecl("post_ipc_message")
func postIpcMessage(webview: WKWebView, name: UnsafePointer<SRString>, methodName: UnsafePointer<SRString>, data: NSDictionary, callback: UInt, error: UInt) {
let invoke = Invoke(sendResponse: { (successResult: JsonValue?, errorResult: JsonValue?) -> Void in
let (fn, payload) = errorResult == nil ? (callback, successResult) : (error, errorResult)
var payloadJson: String
@ -93,3 +94,24 @@ func invokePlugin(webview: WKWebView, name: UnsafePointer<SRString>, methodName:
}, data: data)
PluginManager.shared.invoke(name: name.pointee.to_string(), methodName: methodName.pointee.to_string(), invoke: invoke)
}
@_cdecl("run_plugin_method")
func runPluginMethod(
id: Int,
name: UnsafePointer<SRString>,
methodName: UnsafePointer<SRString>,
data: NSDictionary,
callback: @escaping @convention(c) (Int, Bool, UnsafePointer<CChar>?) -> Void
) {
let invoke = Invoke(sendResponse: { (successResult: JsonValue?, errorResult: JsonValue?) -> Void in
let (success, payload) = errorResult == nil ? (true, successResult) : (false, errorResult)
var payloadJson: String = ""
do {
try payloadJson = payload == nil ? "null" : payload!.jsonRepresentation() ?? "`Failed to serialize payload`"
} catch {
payloadJson = "`\(error)`"
}
callback(id, success, payloadJson.cString(using: String.Encoding.utf8))
}, data: data)
PluginManager.shared.invoke(name: name.pointee.to_string(), methodName: methodName.pointee.to_string(), invoke: invoke)
}

View File

@ -877,12 +877,80 @@ macro_rules! shared_app_impl {
Ok(())
}
/// Executes the given plugin mobile method.
#[cfg(mobile)]
pub fn run_mobile_plugin<T: serde::de::DeserializeOwned, E: serde::de::DeserializeOwned>(
&self,
plugin: impl AsRef<str>,
method: impl AsRef<str>,
payload: impl serde::Serialize
) -> crate::Result<Result<T, E>> {
#[cfg(target_os = "ios")]
{
Ok(self.run_ios_plugin(plugin, method, payload))
}
#[cfg(target_os = "android")]
{
self.run_android_plugin(plugin, method, payload).map_err(Into::into)
}
}
/// Executes the given iOS plugin method.
#[cfg(target_os = "ios")]
fn run_ios_plugin<T: serde::de::DeserializeOwned, E: serde::de::DeserializeOwned>(
&self,
plugin: impl AsRef<str>,
method: impl AsRef<str>,
payload: impl serde::Serialize
) -> Result<T, E> {
use std::{os::raw::{c_int, c_char}, ffi::CStr, sync::mpsc::channel};
let id: i32 = rand::random();
let (tx, rx) = channel();
PENDING_PLUGIN_CALLS
.get_or_init(Default::default)
.lock()
.unwrap().insert(id, Box::new(move |arg| {
tx.send(arg).unwrap();
}));
unsafe {
extern "C" fn plugin_method_response_handler(id: c_int, success: c_int, payload: *const c_char) {
let payload = unsafe {
assert!(!payload.is_null());
CStr::from_ptr(payload)
};
if let Some(handler) = PENDING_PLUGIN_CALLS
.get_or_init(Default::default)
.lock()
.unwrap()
.remove(&id)
{
let payload = serde_json::from_str(payload.to_str().unwrap()).unwrap();
handler(if success == 1 { Ok(payload) } else { Err(payload) });
}
}
crate::ios::run_plugin_method(
id,
&plugin.as_ref().into(),
&method.as_ref().into(),
crate::ios::json_to_dictionary(serde_json::to_value(payload).unwrap()),
plugin_method_response_handler,
);
}
rx.recv().unwrap()
.map(|r| serde_json::from_value(r).unwrap())
.map_err(|e| serde_json::from_value(e).unwrap())
}
/// Executes the given Android plugin method.
#[cfg(target_os = "android")]
pub fn run_android_plugin<T: serde::de::DeserializeOwned, E: serde::de::DeserializeOwned>(
fn run_android_plugin<T: serde::de::DeserializeOwned, E: serde::de::DeserializeOwned>(
&self,
plugin: impl Into<String>,
method: impl Into<String>,
plugin: impl AsRef<str>,
method: impl AsRef<str>,
payload: impl serde::Serialize
) -> Result<Result<T, E>, jni::errors::Error> {
use jni::{
@ -932,8 +1000,8 @@ macro_rules! shared_app_impl {
};
let id: i32 = rand::random();
let plugin = plugin.into();
let method = method.into();
let plugin = plugin.as_ref().to_string();
let method = method.as_ref().to_string();
let payload = serde_json::to_value(payload).unwrap();
let handle_ = handle.clone();
@ -1966,11 +2034,11 @@ impl Default for Builder<crate::Wry> {
}
}
#[cfg(target_os = "android")]
#[cfg(mobile)]
type PendingPluginCallHandler =
Box<dyn FnOnce(std::result::Result<serde_json::Value, serde_json::Value>) + Send + 'static>;
#[cfg(target_os = "android")]
#[cfg(mobile)]
static PENDING_PLUGIN_CALLS: once_cell::sync::OnceCell<
std::sync::Mutex<HashMap<i32, PendingPluginCallHandler>>,
> = once_cell::sync::OnceCell::new();

View File

@ -131,6 +131,10 @@ pub enum Error {
/// The Window's raw handle is invalid for the platform.
#[error("Unexpected `raw_window_handle` for the current platform")]
InvalidWindowHandle,
/// JNI error.
#[cfg(target_os = "android")]
#[error("jni error: {0}")]
Jni(#[from] jni::errors::Error),
}
pub(crate) fn into_anyhow<T: std::fmt::Display>(err: T) -> anyhow::Error {

View File

@ -1,8 +1,14 @@
use cocoa::base::id;
use cocoa::base::{id, nil, NO, YES};
use objc::*;
use serde_json::Value as JsonValue;
use swift_rs::SRString;
use std::os::raw::{c_char, c_int};
type PluginMessageCallback = unsafe extern "C" fn(c_int, c_int, *const c_char);
extern "C" {
pub fn invoke_plugin(
pub fn post_ipc_message(
webview: id,
name: &SRString,
method: &SRString,
@ -11,5 +17,141 @@ extern "C" {
error: usize,
);
pub fn run_plugin_method(
id: i32,
name: &SRString,
method: &SRString,
data: id,
callback: PluginMessageCallback,
);
pub fn on_webview_created(webview: id);
}
pub fn json_to_dictionary(json: JsonValue) -> id {
if let serde_json::Value::Object(map) = json {
unsafe {
let dictionary: id = msg_send![class!(NSMutableDictionary), alloc];
let data: id = msg_send![dictionary, init];
for (key, value) in map {
add_json_entry_to_dictionary(data, key, value);
}
data
}
} else {
nil
}
}
const UTF8_ENCODING: usize = 4;
struct NSString(id);
impl NSString {
fn new(s: &str) -> Self {
// Safety: objc runtime calls are unsafe
NSString(unsafe {
let ns_string: id = msg_send![class!(NSString), alloc];
let ns_string: id = msg_send![ns_string,
initWithBytes:s.as_ptr()
length:s.len()
encoding:UTF8_ENCODING];
// The thing is allocated in rust, the thing must be set to autorelease in rust to relinquish control
// or it can not be released correctly in OC runtime
let _: () = msg_send![ns_string, autorelease];
ns_string
})
}
}
unsafe fn add_json_value_to_array(array: id, value: JsonValue) {
match value {
JsonValue::Null => {
let null: id = msg_send![class!(NSNull), null];
let () = msg_send![array, addObject: null];
}
JsonValue::Bool(val) => {
let value = if val { YES } else { NO };
let v: id = msg_send![class!(NSNumber), numberWithBool: value];
let () = msg_send![array, addObject: v];
}
JsonValue::Number(val) => {
let number: id = if let Some(v) = val.as_i64() {
msg_send![class!(NSNumber), numberWithInteger: v]
} else if let Some(v) = val.as_u64() {
msg_send![class!(NSNumber), numberWithUnsignedLongLong: v]
} else if let Some(v) = val.as_f64() {
msg_send![class!(NSNumber), numberWithDouble: v]
} else {
unreachable!()
};
let () = msg_send![array, addObject: number];
}
JsonValue::String(val) => {
let () = msg_send![array, addObject: NSString::new(&val)];
}
JsonValue::Array(val) => {
let nsarray: id = msg_send![class!(NSMutableArray), alloc];
let inner_array: id = msg_send![nsarray, init];
for value in val {
add_json_value_to_array(inner_array, value);
}
let () = msg_send![array, addObject: inner_array];
}
JsonValue::Object(val) => {
let dictionary: id = msg_send![class!(NSMutableDictionary), alloc];
let data: id = msg_send![dictionary, init];
for (key, value) in val {
add_json_entry_to_dictionary(data, key, value);
}
let () = msg_send![array, addObject: data];
}
}
}
unsafe fn add_json_entry_to_dictionary(data: id, key: String, value: JsonValue) {
let key = NSString::new(&key);
match value {
JsonValue::Null => {
let null: id = msg_send![class!(NSNull), null];
let () = msg_send![data, setObject:null forKey: key];
}
JsonValue::Bool(val) => {
let value = if val { YES } else { NO };
let () = msg_send![data, setObject:value forKey: key];
}
JsonValue::Number(val) => {
let number: id = if let Some(v) = val.as_i64() {
msg_send![class!(NSNumber), numberWithInteger: v]
} else if let Some(v) = val.as_u64() {
msg_send![class!(NSNumber), numberWithUnsignedLongLong: v]
} else if let Some(v) = val.as_f64() {
msg_send![class!(NSNumber), numberWithDouble: v]
} else {
unreachable!()
};
let () = msg_send![data, setObject:number forKey: key];
}
JsonValue::String(val) => {
let () = msg_send![data, setObject:NSString::new(&val) forKey: key];
}
JsonValue::Array(val) => {
let nsarray: id = msg_send![class!(NSMutableArray), alloc];
let array: id = msg_send![nsarray, init];
for value in val {
add_json_value_to_array(array, value);
}
let () = msg_send![data, setObject:array forKey: key];
}
JsonValue::Object(val) => {
let dictionary: id = msg_send![class!(NSMutableDictionary), alloc];
let inner_data: id = msg_send![dictionary, init];
for (key, value) in val {
add_json_entry_to_dictionary(inner_data, key, value);
}
let () = msg_send![data, setObject:inner_data forKey: key];
}
}
}

View File

@ -1357,142 +1357,12 @@ impl<R: Runtime> Window<R> {
if !handled {
let plugin = plugin.to_string();
self.with_webview(move |webview| {
use cocoa::base::{id, nil, NO, YES};
use objc::*;
use serde_json::Value as JsonValue;
const UTF8_ENCODING: usize = 4;
struct NSString(id);
impl NSString {
fn new(s: &str) -> Self {
// Safety: objc runtime calls are unsafe
NSString(unsafe {
let ns_string: id = msg_send![class!(NSString), alloc];
let ns_string: id = msg_send![ns_string,
initWithBytes:s.as_ptr()
length:s.len()
encoding:UTF8_ENCODING];
// The thing is allocated in rust, the thing must be set to autorelease in rust to relinquish control
// or it can not be released correctly in OC runtime
let _: () = msg_send![ns_string, autorelease];
ns_string
})
}
}
unsafe fn add_json_value_to_array(array: id, value: JsonValue) {
match value {
JsonValue::Null => {
let null: id = msg_send![class!(NSNull), null];
let () = msg_send![array, addObject: null];
}
JsonValue::Bool(val) => {
let value = if val { YES } else { NO };
let v: id = msg_send![class!(NSNumber), numberWithBool: value];
let () = msg_send![array, addObject: v];
}
JsonValue::Number(val) => {
let number: id = if let Some(v) = val.as_i64() {
msg_send![class!(NSNumber), numberWithInteger: v]
} else if let Some(v) = val.as_u64() {
msg_send![class!(NSNumber), numberWithUnsignedLongLong: v]
} else if let Some(v) = val.as_f64() {
msg_send![class!(NSNumber), numberWithDouble: v]
} else {
unreachable!()
};
let () = msg_send![array, addObject: number];
}
JsonValue::String(val) => {
let () = msg_send![array, addObject: NSString::new(&val)];
}
JsonValue::Array(val) => {
let nsarray: id = msg_send![class!(NSMutableArray), alloc];
let inner_array: id = msg_send![nsarray, init];
for value in val {
add_json_value_to_array(inner_array, value);
}
let () = msg_send![array, addObject: inner_array];
}
JsonValue::Object(val) => {
let dictionary: id = msg_send![class!(NSMutableDictionary), alloc];
let data: id = msg_send![dictionary, init];
for (key, value) in val {
add_json_entry_to_dictionary(data, key, value);
}
let () = msg_send![array, addObject: data];
}
}
}
unsafe fn add_json_entry_to_dictionary(data: id, key: String, value: JsonValue) {
let key = NSString::new(&key);
match value {
JsonValue::Null => {
let null: id = msg_send![class!(NSNull), null];
let () = msg_send![data, setObject:null forKey: key];
}
JsonValue::Bool(val) => {
let value = if val { YES } else { NO };
let () = msg_send![data, setObject:value forKey: key];
}
JsonValue::Number(val) => {
let number: id = if let Some(v) = val.as_i64() {
msg_send![class!(NSNumber), numberWithInteger: v]
} else if let Some(v) = val.as_u64() {
msg_send![class!(NSNumber), numberWithUnsignedLongLong: v]
} else if let Some(v) = val.as_f64() {
msg_send![class!(NSNumber), numberWithDouble: v]
} else {
unreachable!()
};
let () = msg_send![data, setObject:number forKey: key];
}
JsonValue::String(val) => {
let () = msg_send![data, setObject:NSString::new(&val) forKey: key];
}
JsonValue::Array(val) => {
let nsarray: id = msg_send![class!(NSMutableArray), alloc];
let array: id = msg_send![nsarray, init];
for value in val {
add_json_value_to_array(array, value);
}
let () = msg_send![data, setObject:array forKey: key];
}
JsonValue::Object(val) => {
let dictionary: id = msg_send![class!(NSMutableDictionary), alloc];
let inner_data: id = msg_send![dictionary, init];
for (key, value) in val {
add_json_entry_to_dictionary(inner_data, key, value);
}
let () = msg_send![data, setObject:inner_data forKey: key];
}
}
}
let data = if let JsonValue::Object(map) = message.payload {
unsafe {
let dictionary: id = msg_send![class!(NSMutableDictionary), alloc];
let data: id = msg_send![dictionary, init];
for (key, value) in map {
add_json_entry_to_dictionary(data, key, value);
}
data
}
} else {
nil
};
unsafe {
crate::ios::invoke_plugin(
crate::ios::post_ipc_message(
webview.inner(),
&plugin.as_str().into(),
&message.command.as_str().into(),
data,
crate::ios::json_to_dictionary(message.payload),
resolver.callback.0,
resolver.error.0,
)

View File

@ -94,9 +94,9 @@ impl AppBuilder {
#[cfg(debug_assertions)]
window.open_devtools();
#[cfg(target_os = "android")]
#[cfg(mobile)]
{
let response = app.run_android_plugin::<serde_json::Value, serde_json::Value>(
let response = app.run_mobile_plugin::<serde_json::Value, serde_json::Value>(
"sample",
"ping",
serde_json::Value::default(),