Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat(core): add IPC channel (#6813)
  • Loading branch information
lucasfernog committed May 11, 2023
1 parent b072daa commit 0ab5f40
Show file tree
Hide file tree
Showing 18 changed files with 216 additions and 184 deletions.
6 changes: 6 additions & 0 deletions .changes/channel-api.md
@@ -0,0 +1,6 @@
---
"api": patch
"tauri": patch
---

Add channel API for sending data across the IPC.
@@ -0,0 +1,11 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

package app.tauri.plugin

class Channel(val id: Long, private val handler: (data: JSObject) -> Unit) {
fun send(data: JSObject) {
handler(data)
}
}
18 changes: 14 additions & 4 deletions core/tauri/mobile/android/src/main/java/app/tauri/plugin/Invoke.kt
Expand Up @@ -6,19 +6,23 @@ package app.tauri.plugin

import app.tauri.Logger

const val CHANNEL_PREFIX = "__CHANNEL__:"

class Invoke(
val id: Long,
val command: String,
private val sendResponse: (success: PluginResult?, error: PluginResult?) -> Unit,
val callback: Long,
val error: Long,
private val sendResponse: (callback: Long, data: PluginResult?) -> Unit,
val data: JSObject) {

fun resolve(data: JSObject?) {
val result = PluginResult(data)
sendResponse(result, null)
sendResponse(callback, result)
}

fun resolve() {
sendResponse(null, null)
sendResponse(callback, null)
}

fun reject(msg: String?, code: String?, ex: Exception?, data: JSObject?) {
Expand All @@ -35,7 +39,7 @@ class Invoke(
} catch (jsonEx: Exception) {
Logger.error(Logger.tags("Plugin"), jsonEx.message!!, jsonEx)
}
sendResponse(null, errorResult)
sendResponse(error, errorResult)
}

fun reject(msg: String?, ex: Exception?, data: JSObject?) {
Expand Down Expand Up @@ -197,4 +201,10 @@ class Invoke(
fun hasOption(name: String): Boolean {
return data.has(name)
}

fun getChannel(name: String): Channel? {
val channelDef = getString(name, "")
val callback = channelDef.substring(CHANNEL_PREFIX.length).toLongOrNull() ?: return null
return Channel(callback) { res -> sendResponse(callback, PluginResult(res)) }
}
}
Expand Up @@ -15,8 +15,8 @@ import app.tauri.Logger
import app.tauri.PermissionHelper
import app.tauri.PermissionState
import app.tauri.annotation.ActivityCallback
import app.tauri.annotation.PermissionCallback
import app.tauri.annotation.Command
import app.tauri.annotation.PermissionCallback
import app.tauri.annotation.TauriPlugin
import org.json.JSONException
import java.util.*
Expand Down Expand Up @@ -126,7 +126,7 @@ abstract class Plugin(private val activity: Activity) {

// If call was made without any custom permissions, request all from plugin annotation
val aliasSet: MutableSet<String> = HashSet()
if (providedPermsList == null || providedPermsList.isEmpty()) {
if (providedPermsList.isNullOrEmpty()) {
for (perm in annotation.permissions) {
// If a permission is defined with no permission strings, separate it for auto-granting.
// Otherwise, the alias is added to the list to be requested.
Expand All @@ -153,7 +153,7 @@ abstract class Plugin(private val activity: Activity) {
permAliases = aliasSet.toTypedArray()
}
}
if (permAliases != null && permAliases.isNotEmpty()) {
if (!permAliases.isNullOrEmpty()) {
// request permissions using provided aliases or all defined on the plugin
requestPermissionForAliases(permAliases, invoke, "checkPermissions")
} else if (autoGrantPerms.isNotEmpty()) {
Expand Down
Expand Up @@ -87,11 +87,7 @@ class PluginManager(val activity: AppCompatActivity) {

@JniMethod
fun postIpcMessage(webView: WebView, pluginId: String, command: String, data: JSObject, callback: Long, error: Long) {
val invoke = Invoke(callback, command, { successResult, errorResult ->
val (fn, result) = if (errorResult == null) Pair(callback, successResult) else Pair(
error,
errorResult
)
val invoke = Invoke(callback, command, callback, error, { fn, result ->
webView.evaluateJavascript("window['_$fn']($result)", null)
}, data)

Expand All @@ -100,8 +96,17 @@ class PluginManager(val activity: AppCompatActivity) {

@JniMethod
fun runCommand(id: Int, pluginId: String, command: String, data: JSObject) {
val invoke = Invoke(id.toLong(), command, { successResult, errorResult ->
handlePluginResponse(id, successResult?.toString(), errorResult?.toString())
val successId = 0L
val errorId = 1L
val invoke = Invoke(id.toLong(), command, successId, errorId, { fn, result ->
var success: PluginResult? = null
var error: PluginResult? = null
if (fn == successId) {
success = result
} else {
error = result
}
handlePluginResponse(id, success?.toString(), error?.toString())
}, data)

dispatchPluginMessage(invoke, pluginId)
Expand Down
17 changes: 17 additions & 0 deletions core/tauri/mobile/ios-api/Sources/Tauri/Channel.swift
@@ -0,0 +1,17 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

public class Channel {
var callback: UInt64
var handler: (JsonValue) -> Void

public init(callback: UInt64, handler: @escaping (JsonValue) -> Void) {
self.callback = callback
self.handler = handler
}

public func send(_ data: JsonObject) {
handler(.dictionary(data))
}
}
35 changes: 28 additions & 7 deletions core/tauri/mobile/ios-api/Sources/Tauri/Invoke.swift
Expand Up @@ -5,6 +5,8 @@
import Foundation
import UIKit

let CHANNEL_PREFIX = "__CHANNEL__:"

@objc public class Invoke: NSObject, JSValueContainer, BridgedJSValueContainer {
public var dictionaryRepresentation: NSDictionary {
return data as NSDictionary
Expand All @@ -15,25 +17,29 @@ import UIKit
}()

public var command: String
var callback: UInt64
var error: UInt64
public var data: JSObject
var sendResponse: (JsonValue?, JsonValue?) -> Void
var sendResponse: (UInt64, JsonValue?) -> Void

public init(command: String, sendResponse: @escaping (JsonValue?, JsonValue?) -> Void, data: JSObject?) {
public init(command: String, callback: UInt64, error: UInt64, sendResponse: @escaping (UInt64, JsonValue?) -> Void, data: JSObject?) {
self.command = command
self.callback = callback
self.error = error
self.data = data ?? [:]
self.sendResponse = sendResponse
}

public func resolve() {
sendResponse(nil, nil)
sendResponse(callback, nil)
}

public func resolve(_ data: JsonObject) {
resolve(.dictionary(data))
}

public func resolve(_ data: JsonValue) {
sendResponse(data, nil)
sendResponse(callback, data)
}

public func reject(_ message: String, _ code: String? = nil, _ error: Error? = nil, _ data: JsonValue? = nil) {
Expand All @@ -46,22 +52,37 @@ import UIKit
}
}
}
sendResponse(nil, .dictionary(payload as! JsonObject))
sendResponse(self.error, .dictionary(payload as! JsonObject))
}

public func unimplemented() {
unimplemented("not implemented")
}

public func unimplemented(_ message: String) {
sendResponse(nil, .dictionary(["message": message]))
sendResponse(error, .dictionary(["message": message]))
}

public func unavailable() {
unavailable("not available")
}

public func unavailable(_ message: String) {
sendResponse(nil, .dictionary(["message": message]))
sendResponse(error, .dictionary(["message": message]))
}

public func getChannel(_ key: String) -> Channel? {
let channelDef = getString(key, "")
if channelDef.starts(with: CHANNEL_PREFIX) {
let index = channelDef.index(channelDef.startIndex, offsetBy: CHANNEL_PREFIX.count)
guard let callback = UInt64(channelDef[index...]) else {
return nil
}
return Channel(callback: callback, handler: { (res: JsonValue) -> Void in
self.sendResponse(callback, res)
})
} else {
return nil
}
}
}
11 changes: 6 additions & 5 deletions core/tauri/mobile/ios-api/Sources/Tauri/Tauri.swift
Expand Up @@ -109,9 +109,8 @@ func onWebviewCreated(webview: WKWebView, viewController: UIViewController) {
}

@_cdecl("post_ipc_message")
func postIpcMessage(webview: WKWebView, name: SRString, command: SRString, data: NSDictionary, callback: UInt, error: UInt) {
let invoke = Invoke(command: command.toString(), sendResponse: { (successResult: JsonValue?, errorResult: JsonValue?) -> Void in
let (fn, payload) = errorResult == nil ? (callback, successResult) : (error, errorResult)
func postIpcMessage(webview: WKWebView, name: SRString, command: SRString, data: NSDictionary, callback: UInt64, error: UInt64) {
let invoke = Invoke(command: command.toString(), callback: callback, error: error, sendResponse: { (fn: UInt64, payload: JsonValue?) -> Void in
var payloadJson: String
do {
try payloadJson = payload == nil ? "null" : payload!.jsonRepresentation() ?? "`Failed to serialize payload`"
Expand All @@ -131,8 +130,10 @@ func runCommand(
data: NSDictionary,
callback: @escaping @convention(c) (Int, Bool, UnsafePointer<CChar>?) -> Void
) {
let invoke = Invoke(command: command.toString(), sendResponse: { (successResult: JsonValue?, errorResult: JsonValue?) -> Void in
let (success, payload) = errorResult == nil ? (true, successResult) : (false, errorResult)
let callbackId: UInt64 = 0
let errorId: UInt64 = 1
let invoke = Invoke(command: command.toString(), callback: callbackId, error: errorId, sendResponse: { (fn: UInt64, payload: JsonValue?) -> Void in
let success = fn == callbackId
var payloadJson: String = ""
do {
try payloadJson = payload == nil ? "null" : payload!.jsonRepresentation() ?? "`Failed to serialize payload`"
Expand Down
4 changes: 2 additions & 2 deletions core/tauri/scripts/bundle.global.js

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions core/tauri/scripts/stringify-ipc-message-fn.js
Expand Up @@ -8,6 +8,8 @@
let o = {};
val.forEach((v, k) => o[k] = v);
return o;
} else if (val instanceof Object && '__TAURI_CHANNEL_MARKER__' in val && typeof val.id === 'number') {
return `__CHANNEL__:${val.id}`
} else {
return val;
}
Expand Down
55 changes: 55 additions & 0 deletions core/tauri/src/api/ipc.rs
Expand Up @@ -10,6 +10,61 @@ use serde::{Deserialize, Serialize};
use serde_json::value::RawValue;
pub use serialize_to_javascript::Options as SerializeOptions;
use serialize_to_javascript::Serialized;
use tauri_macros::default_runtime;

use crate::{
command::{CommandArg, CommandItem},
InvokeError, Runtime, Window,
};

const CHANNEL_PREFIX: &str = "__CHANNEL__:";

/// An IPC channel.
#[default_runtime(crate::Wry, wry)]
pub struct Channel<R: Runtime> {
id: CallbackFn,
window: Window<R>,
}

impl Serialize for Channel {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&format!("{CHANNEL_PREFIX}{}", self.id.0))
}
}

impl<R: Runtime> Channel<R> {
/// Sends the given data through the channel.
pub fn send<S: Serialize>(&self, data: &S) -> crate::Result<()> {
let js = format_callback(self.id, data)?;
self.window.eval(&js)
}
}

impl<'de, R: Runtime> CommandArg<'de, R> for Channel<R> {
/// Grabs the [`Window`] from the [`CommandItem`] and returns the associated [`Channel`].
fn from_command(command: CommandItem<'de, R>) -> Result<Self, InvokeError> {
let name = command.name;
let arg = command.key;
let window = command.message.window();
let value: String =
Deserialize::deserialize(command).map_err(|e| crate::Error::InvalidArgs(name, arg, e))?;
if let Some(callback_id) = value
.split_once(CHANNEL_PREFIX)
.and_then(|(_prefix, id)| id.parse().ok())
{
return Ok(Channel {
id: CallbackFn(callback_id),
window,
});
}
Err(InvokeError::from_anyhow(anyhow::anyhow!(
"invalid channel value `{value}`, expected a string in the `{CHANNEL_PREFIX}ID` format"
)))
}
}

/// The `Callback` type is the return value of the `transformCallback` JavaScript function.
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
Expand Down
2 changes: 1 addition & 1 deletion examples/api/dist/assets/index.css

Large diffs are not rendered by default.

65 changes: 30 additions & 35 deletions examples/api/dist/assets/index.js

Large diffs are not rendered by default.

12 changes: 0 additions & 12 deletions examples/api/src/App.svelte
Expand Up @@ -6,9 +6,7 @@
import Cli from './views/Cli.svelte'
import Communication from './views/Communication.svelte'
import Window from './views/Window.svelte'
import Updater from './views/Updater.svelte'
import WebRTC from './views/WebRTC.svelte'
import App from './views/App.svelte'
import { onMount } from 'svelte'
import { listen } from '@tauri-apps/api/event'
Expand Down Expand Up @@ -36,21 +34,11 @@
component: Cli,
icon: 'i-codicon-terminal'
},
!isMobile && {
label: 'App',
component: App,
icon: 'i-codicon-hubot'
},
!isMobile && {
label: 'Window',
component: Window,
icon: 'i-codicon-window'
},
!isMobile && {
label: 'Updater',
component: Updater,
icon: 'i-codicon-cloud-download'
},
{
label: 'WebRTC',
component: WebRTC,
Expand Down

0 comments on commit 0ab5f40

Please sign in to comment.