Skip to content

Commit

Permalink
feat: add API to call iOS plugin (#6242)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasfernog authored Feb 11, 2023
1 parent 05dad08 commit bfb2ab2
Show file tree
Hide file tree
Showing 9 changed files with 265 additions and 159 deletions.
5 changes: 0 additions & 5 deletions .changes/run-android-plugin.md

This file was deleted.

5 changes: 5 additions & 0 deletions .changes/run-mobile-plugin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"tauri": patch
---

Added `App::run_mobile_plugin` and `AppHandle::run_mobile_plugin`.
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
),
]
)
40 changes: 31 additions & 9 deletions core/tauri/mobile/ios-api/Sources/Tauri/Tauri.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import SwiftRs
import Foundation
import MetalKit
import WebKit
import os.log
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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)
}
82 changes: 75 additions & 7 deletions core/tauri/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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();
Expand Down
4 changes: 4 additions & 0 deletions core/tauri/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
146 changes: 144 additions & 2 deletions core/tauri/src/ios.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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];
}
}
}
Loading

0 comments on commit bfb2ab2

Please sign in to comment.