Skip to content

Commit bfb2ab2

Browse files
authored
feat: add API to call iOS plugin (#6242)
1 parent 05dad08 commit bfb2ab2

File tree

9 files changed

+265
-159
lines changed

9 files changed

+265
-159
lines changed

.changes/run-android-plugin.md

Lines changed: 0 additions & 5 deletions
This file was deleted.

.changes/run-mobile-plugin.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"tauri": patch
3+
---
4+
5+
Added `App::run_mobile_plugin` and `AppHandle::run_mobile_plugin`.

Package.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@ let package = Package(
1717
dependencies: [
1818
.product(name: "SwiftRs", package: "swift-rs"),
1919
],
20-
path: "core/tauri/ios/Sources/Tauri"
20+
path: "core/tauri/mobile/ios-api/Sources/Tauri"
2121
),
2222
.testTarget(
2323
name: "TauriTests",
2424
dependencies: ["Tauri"],
25-
path: "core/tauri/ios/Tests/TauriTests"
25+
path: "core/tauri/mobile/ios-api/Tests/TauriTests"
2626
),
2727
]
2828
)

core/tauri/mobile/ios-api/Sources/Tauri/Tauri.swift

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import SwiftRs
2+
import Foundation
23
import MetalKit
34
import WebKit
45
import os.log
@@ -14,15 +15,15 @@ class PluginHandle {
1415

1516
class PluginManager {
1617
static var shared: PluginManager = PluginManager()
17-
var plugins: [String:PluginHandle] = [:]
18+
var plugins: [String: PluginHandle] = [:]
1819

1920
func onWebviewCreated(_ webview: WKWebView) {
20-
for (_, handle) in plugins {
21-
if (!handle.loaded) {
22-
handle.instance.perform(#selector(Plugin.load), with: webview)
23-
}
24-
}
25-
}
21+
for (_, handle) in plugins {
22+
if (!handle.loaded) {
23+
handle.instance.perform(#selector(Plugin.load), with: webview)
24+
}
25+
}
26+
}
2627

2728
func load<P: Plugin & NSObject>(webview: WKWebView?, name: String, plugin: P) {
2829
let handle = PluginHandle(plugin: plugin)
@@ -79,8 +80,8 @@ func onWebviewCreated(webview: WKWebView) {
7980
PluginManager.shared.onWebviewCreated(webview)
8081
}
8182

82-
@_cdecl("invoke_plugin")
83-
func invokePlugin(webview: WKWebView, name: UnsafePointer<SRString>, methodName: UnsafePointer<SRString>, data: NSDictionary, callback: UInt, error: UInt) {
83+
@_cdecl("post_ipc_message")
84+
func postIpcMessage(webview: WKWebView, name: UnsafePointer<SRString>, methodName: UnsafePointer<SRString>, data: NSDictionary, callback: UInt, error: UInt) {
8485
let invoke = Invoke(sendResponse: { (successResult: JsonValue?, errorResult: JsonValue?) -> Void in
8586
let (fn, payload) = errorResult == nil ? (callback, successResult) : (error, errorResult)
8687
var payloadJson: String
@@ -93,3 +94,24 @@ func invokePlugin(webview: WKWebView, name: UnsafePointer<SRString>, methodName:
9394
}, data: data)
9495
PluginManager.shared.invoke(name: name.pointee.to_string(), methodName: methodName.pointee.to_string(), invoke: invoke)
9596
}
97+
98+
@_cdecl("run_plugin_method")
99+
func runPluginMethod(
100+
id: Int,
101+
name: UnsafePointer<SRString>,
102+
methodName: UnsafePointer<SRString>,
103+
data: NSDictionary,
104+
callback: @escaping @convention(c) (Int, Bool, UnsafePointer<CChar>?) -> Void
105+
) {
106+
let invoke = Invoke(sendResponse: { (successResult: JsonValue?, errorResult: JsonValue?) -> Void in
107+
let (success, payload) = errorResult == nil ? (true, successResult) : (false, errorResult)
108+
var payloadJson: String = ""
109+
do {
110+
try payloadJson = payload == nil ? "null" : payload!.jsonRepresentation() ?? "`Failed to serialize payload`"
111+
} catch {
112+
payloadJson = "`\(error)`"
113+
}
114+
callback(id, success, payloadJson.cString(using: String.Encoding.utf8))
115+
}, data: data)
116+
PluginManager.shared.invoke(name: name.pointee.to_string(), methodName: methodName.pointee.to_string(), invoke: invoke)
117+
}

core/tauri/src/app.rs

Lines changed: 75 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -877,12 +877,80 @@ macro_rules! shared_app_impl {
877877
Ok(())
878878
}
879879

880+
/// Executes the given plugin mobile method.
881+
#[cfg(mobile)]
882+
pub fn run_mobile_plugin<T: serde::de::DeserializeOwned, E: serde::de::DeserializeOwned>(
883+
&self,
884+
plugin: impl AsRef<str>,
885+
method: impl AsRef<str>,
886+
payload: impl serde::Serialize
887+
) -> crate::Result<Result<T, E>> {
888+
#[cfg(target_os = "ios")]
889+
{
890+
Ok(self.run_ios_plugin(plugin, method, payload))
891+
}
892+
#[cfg(target_os = "android")]
893+
{
894+
self.run_android_plugin(plugin, method, payload).map_err(Into::into)
895+
}
896+
}
897+
898+
/// Executes the given iOS plugin method.
899+
#[cfg(target_os = "ios")]
900+
fn run_ios_plugin<T: serde::de::DeserializeOwned, E: serde::de::DeserializeOwned>(
901+
&self,
902+
plugin: impl AsRef<str>,
903+
method: impl AsRef<str>,
904+
payload: impl serde::Serialize
905+
) -> Result<T, E> {
906+
use std::{os::raw::{c_int, c_char}, ffi::CStr, sync::mpsc::channel};
907+
908+
let id: i32 = rand::random();
909+
let (tx, rx) = channel();
910+
PENDING_PLUGIN_CALLS
911+
.get_or_init(Default::default)
912+
.lock()
913+
.unwrap().insert(id, Box::new(move |arg| {
914+
tx.send(arg).unwrap();
915+
}));
916+
917+
unsafe {
918+
extern "C" fn plugin_method_response_handler(id: c_int, success: c_int, payload: *const c_char) {
919+
let payload = unsafe {
920+
assert!(!payload.is_null());
921+
CStr::from_ptr(payload)
922+
};
923+
924+
if let Some(handler) = PENDING_PLUGIN_CALLS
925+
.get_or_init(Default::default)
926+
.lock()
927+
.unwrap()
928+
.remove(&id)
929+
{
930+
let payload = serde_json::from_str(payload.to_str().unwrap()).unwrap();
931+
handler(if success == 1 { Ok(payload) } else { Err(payload) });
932+
}
933+
}
934+
935+
crate::ios::run_plugin_method(
936+
id,
937+
&plugin.as_ref().into(),
938+
&method.as_ref().into(),
939+
crate::ios::json_to_dictionary(serde_json::to_value(payload).unwrap()),
940+
plugin_method_response_handler,
941+
);
942+
}
943+
rx.recv().unwrap()
944+
.map(|r| serde_json::from_value(r).unwrap())
945+
.map_err(|e| serde_json::from_value(e).unwrap())
946+
}
947+
880948
/// Executes the given Android plugin method.
881949
#[cfg(target_os = "android")]
882-
pub fn run_android_plugin<T: serde::de::DeserializeOwned, E: serde::de::DeserializeOwned>(
950+
fn run_android_plugin<T: serde::de::DeserializeOwned, E: serde::de::DeserializeOwned>(
883951
&self,
884-
plugin: impl Into<String>,
885-
method: impl Into<String>,
952+
plugin: impl AsRef<str>,
953+
method: impl AsRef<str>,
886954
payload: impl serde::Serialize
887955
) -> Result<Result<T, E>, jni::errors::Error> {
888956
use jni::{
@@ -932,8 +1000,8 @@ macro_rules! shared_app_impl {
9321000
};
9331001

9341002
let id: i32 = rand::random();
935-
let plugin = plugin.into();
936-
let method = method.into();
1003+
let plugin = plugin.as_ref().to_string();
1004+
let method = method.as_ref().to_string();
9371005
let payload = serde_json::to_value(payload).unwrap();
9381006
let handle_ = handle.clone();
9391007

@@ -1966,11 +2034,11 @@ impl Default for Builder<crate::Wry> {
19662034
}
19672035
}
19682036

1969-
#[cfg(target_os = "android")]
2037+
#[cfg(mobile)]
19702038
type PendingPluginCallHandler =
19712039
Box<dyn FnOnce(std::result::Result<serde_json::Value, serde_json::Value>) + Send + 'static>;
19722040

1973-
#[cfg(target_os = "android")]
2041+
#[cfg(mobile)]
19742042
static PENDING_PLUGIN_CALLS: once_cell::sync::OnceCell<
19752043
std::sync::Mutex<HashMap<i32, PendingPluginCallHandler>>,
19762044
> = once_cell::sync::OnceCell::new();

core/tauri/src/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@ pub enum Error {
131131
/// The Window's raw handle is invalid for the platform.
132132
#[error("Unexpected `raw_window_handle` for the current platform")]
133133
InvalidWindowHandle,
134+
/// JNI error.
135+
#[cfg(target_os = "android")]
136+
#[error("jni error: {0}")]
137+
Jni(#[from] jni::errors::Error),
134138
}
135139

136140
pub(crate) fn into_anyhow<T: std::fmt::Display>(err: T) -> anyhow::Error {

core/tauri/src/ios.rs

Lines changed: 144 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1-
use cocoa::base::id;
1+
use cocoa::base::{id, nil, NO, YES};
2+
use objc::*;
3+
use serde_json::Value as JsonValue;
24
use swift_rs::SRString;
35

6+
use std::os::raw::{c_char, c_int};
7+
8+
type PluginMessageCallback = unsafe extern "C" fn(c_int, c_int, *const c_char);
9+
410
extern "C" {
5-
pub fn invoke_plugin(
11+
pub fn post_ipc_message(
612
webview: id,
713
name: &SRString,
814
method: &SRString,
@@ -11,5 +17,141 @@ extern "C" {
1117
error: usize,
1218
);
1319

20+
pub fn run_plugin_method(
21+
id: i32,
22+
name: &SRString,
23+
method: &SRString,
24+
data: id,
25+
callback: PluginMessageCallback,
26+
);
27+
1428
pub fn on_webview_created(webview: id);
1529
}
30+
31+
pub fn json_to_dictionary(json: JsonValue) -> id {
32+
if let serde_json::Value::Object(map) = json {
33+
unsafe {
34+
let dictionary: id = msg_send![class!(NSMutableDictionary), alloc];
35+
let data: id = msg_send![dictionary, init];
36+
for (key, value) in map {
37+
add_json_entry_to_dictionary(data, key, value);
38+
}
39+
data
40+
}
41+
} else {
42+
nil
43+
}
44+
}
45+
46+
const UTF8_ENCODING: usize = 4;
47+
48+
struct NSString(id);
49+
50+
impl NSString {
51+
fn new(s: &str) -> Self {
52+
// Safety: objc runtime calls are unsafe
53+
NSString(unsafe {
54+
let ns_string: id = msg_send![class!(NSString), alloc];
55+
let ns_string: id = msg_send![ns_string,
56+
initWithBytes:s.as_ptr()
57+
length:s.len()
58+
encoding:UTF8_ENCODING];
59+
60+
// The thing is allocated in rust, the thing must be set to autorelease in rust to relinquish control
61+
// or it can not be released correctly in OC runtime
62+
let _: () = msg_send![ns_string, autorelease];
63+
64+
ns_string
65+
})
66+
}
67+
}
68+
69+
unsafe fn add_json_value_to_array(array: id, value: JsonValue) {
70+
match value {
71+
JsonValue::Null => {
72+
let null: id = msg_send![class!(NSNull), null];
73+
let () = msg_send![array, addObject: null];
74+
}
75+
JsonValue::Bool(val) => {
76+
let value = if val { YES } else { NO };
77+
let v: id = msg_send![class!(NSNumber), numberWithBool: value];
78+
let () = msg_send![array, addObject: v];
79+
}
80+
JsonValue::Number(val) => {
81+
let number: id = if let Some(v) = val.as_i64() {
82+
msg_send![class!(NSNumber), numberWithInteger: v]
83+
} else if let Some(v) = val.as_u64() {
84+
msg_send![class!(NSNumber), numberWithUnsignedLongLong: v]
85+
} else if let Some(v) = val.as_f64() {
86+
msg_send![class!(NSNumber), numberWithDouble: v]
87+
} else {
88+
unreachable!()
89+
};
90+
let () = msg_send![array, addObject: number];
91+
}
92+
JsonValue::String(val) => {
93+
let () = msg_send![array, addObject: NSString::new(&val)];
94+
}
95+
JsonValue::Array(val) => {
96+
let nsarray: id = msg_send![class!(NSMutableArray), alloc];
97+
let inner_array: id = msg_send![nsarray, init];
98+
for value in val {
99+
add_json_value_to_array(inner_array, value);
100+
}
101+
let () = msg_send![array, addObject: inner_array];
102+
}
103+
JsonValue::Object(val) => {
104+
let dictionary: id = msg_send![class!(NSMutableDictionary), alloc];
105+
let data: id = msg_send![dictionary, init];
106+
for (key, value) in val {
107+
add_json_entry_to_dictionary(data, key, value);
108+
}
109+
let () = msg_send![array, addObject: data];
110+
}
111+
}
112+
}
113+
114+
unsafe fn add_json_entry_to_dictionary(data: id, key: String, value: JsonValue) {
115+
let key = NSString::new(&key);
116+
match value {
117+
JsonValue::Null => {
118+
let null: id = msg_send![class!(NSNull), null];
119+
let () = msg_send![data, setObject:null forKey: key];
120+
}
121+
JsonValue::Bool(val) => {
122+
let value = if val { YES } else { NO };
123+
let () = msg_send![data, setObject:value forKey: key];
124+
}
125+
JsonValue::Number(val) => {
126+
let number: id = if let Some(v) = val.as_i64() {
127+
msg_send![class!(NSNumber), numberWithInteger: v]
128+
} else if let Some(v) = val.as_u64() {
129+
msg_send![class!(NSNumber), numberWithUnsignedLongLong: v]
130+
} else if let Some(v) = val.as_f64() {
131+
msg_send![class!(NSNumber), numberWithDouble: v]
132+
} else {
133+
unreachable!()
134+
};
135+
let () = msg_send![data, setObject:number forKey: key];
136+
}
137+
JsonValue::String(val) => {
138+
let () = msg_send![data, setObject:NSString::new(&val) forKey: key];
139+
}
140+
JsonValue::Array(val) => {
141+
let nsarray: id = msg_send![class!(NSMutableArray), alloc];
142+
let array: id = msg_send![nsarray, init];
143+
for value in val {
144+
add_json_value_to_array(array, value);
145+
}
146+
let () = msg_send![data, setObject:array forKey: key];
147+
}
148+
JsonValue::Object(val) => {
149+
let dictionary: id = msg_send![class!(NSMutableDictionary), alloc];
150+
let inner_data: id = msg_send![dictionary, init];
151+
for (key, value) in val {
152+
add_json_entry_to_dictionary(inner_data, key, value);
153+
}
154+
let () = msg_send![data, setObject:inner_data forKey: key];
155+
}
156+
}
157+
}

0 commit comments

Comments
 (0)