Skip to content

Commit

Permalink
feat: derive(Events) to generate listen bindings
Browse files Browse the repository at this point in the history
  • Loading branch information
jvatic committed Aug 2, 2024
1 parent 65849f4 commit d5f0d58
Show file tree
Hide file tree
Showing 3 changed files with 240 additions and 8 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
edition = "2021"
name = "tauri-bindgen-rs-macros"
version = "0.1.0"
version = "0.1.1"

[lib]
proc-macro = true
Expand Down
33 changes: 29 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ I couldn't find a comfortable way of defining commands that would maintain type

## Usage

1. Create an intermediary crate in the workspace of your Tauri app to house traits defining your commands and generated IPC bindings to import into the Rust frontend, e.g:
1. Create an intermediary crate in the workspace of your Tauri app to house traits defining your commands, events, and generated IPC bindings to import into the Rust frontend, e.g:

```toml
[package]
Expand All @@ -19,7 +19,7 @@ I couldn't find a comfortable way of defining commands that would maintain type
version = "0.1.0"

[dependencies]
tauri-bindgen-rs-macros = { version = "0.1.0", git = "https://github.com/jvatic/tauri-bindgen-rs-macros.git" }
tauri-bindgen-rs-macros = { version = "0.1.1", git = "https://github.com/jvatic/tauri-bindgen-rs-macros.git" }
serde = { version = "1.0.204", features = ["derive"] }
serde-wasm-bindgen = "0.6"
wasm-bindgen = "0.2"
Expand All @@ -32,8 +32,17 @@ I couldn't find a comfortable way of defining commands that would maintain type
pub trait Commands {
async hello(name: String) -> Result<String, String>;
}

#[derive(tauri_bindgen_rs_macros::Events, Debug, Clone, ::serde::Serialize, ::serde::Deserialize)]
enum Event {
SomethingHappened { payload: Vec<u8> },
SomeoneSaidHello(String),
NoPayload,
}
```

**NOTE:** If you have multiple enums deriving `Events`, these will need to be in separate modules since there's some common boilerplate types that are included currently (that will be moved into another crate at some point).

And if you're using a plugin on the frontend and want bindings generated for it, you can do so by defining a trait for it, e.g:

```rust
Expand All @@ -46,9 +55,9 @@ I couldn't find a comfortable way of defining commands that would maintain type
}
```

**NOTE:** If you have multiple traits implementing `invoke_bindings` they'll each need to be in their own `mod` since an `invoke` WASM binding will be derived in scope of where the trait is defined.
**NOTE:** If you have multiple traits implementing `invoke_bindings` they'll each need to be in their own `mod` since an `invoke` WASM binding will be derived in scope of where the trait is defined (this will be moved into another crate at some point).

2. Import the trait into your Tauri backend and wrap your command definitions in the `impl_trait` macro, e.g:
2. Import the commands trait into your Tauri backend and wrap your command definitions in the `impl_trait` macro, e.g:

```rust
use my_commands::Commands;
Expand All @@ -62,6 +71,15 @@ I couldn't find a comfortable way of defining commands that would maintain type

This will define a shadow struct with an `impl Commands` block with all the functions passed into the macro minus any fn generics or arguments where the type starts with `tauri::`, and spits out the actual fns untouched. The Rust compiler will then emit helpful errors if the defined commands are different (after being processed) from those in the trait, yay!

3. Import the event enum into your Tauri backend if you wish to emit events from there, e.g.:

```rust
use my_commands::Event;
fn emit_event(app_handle: tauri::AppHandle, event: Event) -> anyhow::Result<()> {
Ok(app_handle.emit(event.event_name(), event)?)
}
```

3. Use the generated IPC bindings in your Rust frontend, eg:

```rust
Expand All @@ -71,4 +89,11 @@ I couldn't find a comfortable way of defining commands that would maintain type
set_greeting(greeting);
});
// ...
spawn_local(async move {
let listener = my_commands::EventBinding::SomethingHappened.listen(|event: my_commands::Event| {
// ...
}).await;
drop(listener); // unlisten
});
// ...
```
213 changes: 210 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
use proc_macro::TokenStream;
use proc_macro2::Span;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{quote, ToTokens, TokenStreamExt};
use syn::{
self, braced,
parse::Parse,
parse_macro_input, parse_quote,
punctuated::{Pair, Punctuated},
token::{self, Comma},
Field, FieldMutability, FnArg, Ident, ItemFn, ItemTrait, LitStr, Pat, Signature, Token,
TraitItem, Type, Visibility,
Field, FieldMutability, Fields, FnArg, Generics, Ident, ItemEnum, ItemFn, ItemTrait, LitStr,
Pat, Signature, Token, TraitItem, Type, Variant, Visibility,
};

#[derive(Default)]
Expand Down Expand Up @@ -138,6 +138,213 @@ pub fn invoke_bindings(attrs: TokenStream, tokens: TokenStream) -> TokenStream {
TokenStream::from(ret)
}

/// # Examples
///
/// ```ignore
/// #[derive(Events, Debug, Clone, ::serde::Serialize, ::serde::Deserialize)]
/// enum Event {
/// SomethingHappened { payload: Vec<u8> },
/// SomeoneSaidHello(String),
/// NoPayload,
/// }
///
/// fn emit_event(app_handle: tauri::AppHandle, event: Event) -> anyhow::Result<()> {
/// Ok(app_handle.emit(event.event_name(), event)?)
/// }
///
/// // ...
///
/// let listener = EventBinding::SomethingHappened.listen(|event: Event| {
/// // ...
/// }).await;
/// drop(listener); // unlisten
/// ```
#[proc_macro_derive(Events)]
pub fn derive_event(tokens: TokenStream) -> TokenStream {
let item_enum = parse_macro_input!(tokens as ItemEnum);
let ItemEnum {
attrs: _,
vis,
enum_token: _,
ident,
generics,
brace_token: _,
variants,
} = item_enum;

fn derive_impl_display(
vis: Visibility,
_generics: Generics, // TODO: support generics
ident: Ident,
variants: Punctuated<Variant, Comma>,
) -> TokenStream2 {
let match_arms: Punctuated<TokenStream2, Comma> = variants
.iter()
.map(|v| -> TokenStream2 {
let ident = ident.clone();
let v_ident = &v.ident;
let v_ident_str = v_ident.to_string();
let fields: TokenStream2 = match &v.fields {
Fields::Unit => quote! {}.into(),
Fields::Unnamed(fields) => {
let placeholders: Punctuated<TokenStream2, Comma> = fields
.unnamed
.iter()
.map(|_| -> TokenStream2 { quote! { _ }.into() })
.collect();
quote! { (#placeholders) }.into()
}
Fields::Named(fields) => {
let placeholders: Punctuated<TokenStream2, Comma> = fields
.named
.iter()
.map(|f| -> TokenStream2 {
let ident = f.ident.as_ref().unwrap();
quote! { #ident: _ }.into()
})
.collect();
quote! { {#placeholders} }.into()
}
};
quote! {
#ident::#v_ident #fields => #v_ident_str
}
.into()
})
.collect();
let ret = quote! {
impl #ident {
#vis fn event_name(&self) -> &'static str {
match self {
#match_arms
}
}
}
};
TokenStream2::from(ret)
}

fn derive_event_binding(
_generics: Generics, // TODO: support generics
ident: Ident,
variants: Punctuated<Variant, Comma>,
) -> TokenStream2 {
let event_binding_ident =
Ident::new(&format!("{}Binding", ident.to_string()), Span::call_site());
let variant_names: Punctuated<Ident, Comma> =
variants.iter().map(|v| v.ident.clone()).collect();
let variant_to_str_match_arms: Punctuated<TokenStream2, Comma> = variants
.iter()
.map(|v| -> TokenStream2 {
let ident = &v.ident;
let ident_str = ident.to_string();
quote! {
#event_binding_ident::#ident => #ident_str
}
.into()
})
.collect();
let ret = quote! {
pub enum #event_binding_ident {
#variant_names
}

impl #event_binding_ident {
pub async fn listen<F>(&self, handler: F) -> Result<EventListener, JsValue>
where
F: Fn(#ident) + 'static,
{
let event_name = self.as_str();
EventListener::new(event_name, move |event| {
let event: TauriEvent<#ident> = ::serde_wasm_bindgen::from_value(event).unwrap();
handler(event.payload);
})
.await
}

fn as_str(&self) -> &str {
match self {
#variant_to_str_match_arms
}
}
}
};
TokenStream2::from(ret)
}

// TODO: break this out into another crate (it doesn't need to be in a macro)
fn events_mod(vis: Visibility) -> TokenStream2 {
quote! {
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = ["window", "__TAURI__", "event"], catch)]
async fn listen(
event_name: &str,
handler: &Closure<dyn FnMut(JsValue)>,
) -> Result<JsValue, JsValue>;
}

#vis struct EventListener {
event_name: String,
_closure: Closure<dyn FnMut(JsValue)>,
unlisten: js_sys::Function,
}

impl EventListener {
pub async fn new<F>(event_name: &str, handler: F) -> Result<Self, JsValue>
where
F: Fn(JsValue) + 'static,
{
let closure = Closure::new(handler);
let unlisten = listen(event_name, &closure).await?;
let unlisten = js_sys::Function::from(unlisten);

tracing::trace!("EventListener created for {event_name}");

Ok(Self {
event_name: event_name.to_string(),
_closure: closure,
unlisten,
})
}
}

impl Drop for EventListener {
fn drop(&mut self) {
tracing::trace!("EventListener dropped for {}", self.event_name);
let context = JsValue::null();
self.unlisten.call0(&context).unwrap();
}
}

#[derive(::serde::Deserialize)]
struct TauriEvent<T> {
pub payload: T,
}
}
}

let impl_display = derive_impl_display(
vis.clone(),
generics.clone(),
ident.clone(),
variants.clone(),
);
let event_binding = derive_event_binding(generics, ident, variants);
let events_mod = events_mod(vis);

let ret = quote! {
#impl_display

#event_binding

#events_mod
};
TokenStream::from(ret)
}

struct ImplTrait {
trait_ident: Ident,
fns: ItemList<ItemFn>,
Expand Down

0 comments on commit d5f0d58

Please sign in to comment.