mirror of
https://github.com/tauri-apps/tauri.git
synced 2024-12-29 05:51:59 +03:00
feat: add API to call iOS plugin (#6242)
This commit is contained in:
parent
05dad08768
commit
bfb2ab24e0
@ -1,5 +0,0 @@
|
||||
---
|
||||
"tauri": patch
|
||||
---
|
||||
|
||||
Added `App::run_android_plugin` and `AppHandle::run_android_plugin`.
|
5
.changes/run-mobile-plugin.md
Normal file
5
.changes/run-mobile-plugin.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"tauri": patch
|
||||
---
|
||||
|
||||
Added `App::run_mobile_plugin` and `AppHandle::run_mobile_plugin`.
|
@ -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"
|
||||
),
|
||||
]
|
||||
)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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 {
|
||||
|
@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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(),
|
||||
|
Loading…
Reference in New Issue
Block a user