Skip to content

Commit dcd8640

Browse files
authored
Rust module bindings and macros for defining procedures (#3444)
# Description of Changes This commit adds a macro attribute `#[procedure]` which applies to functions, and various in-WASM machinery for defining, calling and running procedures. A simple example of a procedure, included in `modules/module-test`: ```rust fn sleep_one_second(ctx: &mut ProcedureContext) { let prev_time = ctx.timestamp; let target = prev_time + Duration::from_secs(1); ctx.sleep_until(target); let new_time = ctx.timestamp; let actual_delta = new_time.duration_since(prev_time).unwrap(); log::info!("Slept from {prev_time} to {new_time}, a total of {actual_delta:?}"); } ``` We intend eventually to make procedures be `async` functions (with the trivial `now_or_never` executor from `future-util`), but I found that making the types work for this was giving me a lot of trouble, and decided to put it off in the interest of unblocking more parallelizable work. Host-side infrastructure for executing procedures is not included in this commit. I have a prototype working, but cleaning it up for review and merge will come a bit later. One item of complexity in this PR is enabling scheduled tables to specify either reducers or procedures, while still providing compile-time diagnostics for ill-typed scheduled functions (as opposed to publish-time). I had to rewrite the previous `schedule_reducer_typecheck` into a more complex `schedule_typecheck` with a trait `ExportFunctionForScheduledTable`, which takes a "tacit trait parameter" encoding reducer-ness or procedure-ness, as described in https://willcrichton.net/notes/defeating-coherence-rust/ . The trait name `ExportFunctionForScheduledTable` is user-facing in the sense that it will appear in compiler diagnostics in ill-typed modules. As such, I am open to bikeshedding. # API and ABI breaking changes Adds a new user-facing API, which we intend to change before releasing. # Expected complexity level and risk 2? Mostly pretty mechanical changes to macros and bindings. # Testing - [x] Added a procedure definition to `module-test`, saw that it typechecks. - [x] Executed same procedure definition using #3390 , the prototype implementation this PR draws from.
1 parent 0b16f8a commit dcd8640

File tree

12 files changed

+661
-78
lines changed

12 files changed

+661
-78
lines changed

crates/bindings-macro/src/lib.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
//
99
// (private documentation for the macro authors is totally fine here and you SHOULD write that!)
1010

11+
mod procedure;
1112
mod reducer;
1213
mod sats;
1314
mod table;
@@ -105,6 +106,14 @@ mod sym {
105106
}
106107
}
107108

109+
#[proc_macro_attribute]
110+
pub fn procedure(args: StdTokenStream, item: StdTokenStream) -> StdTokenStream {
111+
cvt_attr::<ItemFn>(args, item, quote!(), |args, original_function| {
112+
let args = procedure::ProcedureArgs::parse(args)?;
113+
procedure::procedure_impl(args, original_function)
114+
})
115+
}
116+
108117
#[proc_macro_attribute]
109118
pub fn reducer(args: StdTokenStream, item: StdTokenStream) -> StdTokenStream {
110119
cvt_attr::<ItemFn>(args, item, quote!(), |args, original_function| {
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
use crate::reducer::{assert_only_lifetime_generics, extract_typed_args};
2+
use crate::sym;
3+
use crate::util::{check_duplicate, ident_to_litstr, match_meta};
4+
use proc_macro2::TokenStream;
5+
use quote::quote;
6+
use syn::parse::Parser as _;
7+
use syn::{ItemFn, LitStr};
8+
9+
#[derive(Default)]
10+
pub(crate) struct ProcedureArgs {
11+
/// For consistency with reducers: allow specifying a different export name than the Rust function name.
12+
name: Option<LitStr>,
13+
}
14+
15+
impl ProcedureArgs {
16+
pub(crate) fn parse(input: TokenStream) -> syn::Result<Self> {
17+
let mut args = Self::default();
18+
syn::meta::parser(|meta| {
19+
match_meta!(match meta {
20+
sym::name => {
21+
check_duplicate(&args.name, &meta)?;
22+
args.name = Some(meta.value()?.parse()?);
23+
}
24+
});
25+
Ok(())
26+
})
27+
.parse2(input)?;
28+
Ok(args)
29+
}
30+
}
31+
32+
pub(crate) fn procedure_impl(args: ProcedureArgs, original_function: &ItemFn) -> syn::Result<TokenStream> {
33+
let func_name = &original_function.sig.ident;
34+
let vis = &original_function.vis;
35+
36+
let procedure_name = args.name.unwrap_or_else(|| ident_to_litstr(func_name));
37+
38+
assert_only_lifetime_generics(original_function, "procedures")?;
39+
40+
let typed_args = extract_typed_args(original_function)?;
41+
42+
// TODO: Require that procedures be `async` functions syntactically,
43+
// and use `futures_util::FutureExt::now_or_never` to poll them.
44+
// if !&original_function.sig.asyncness.is_some() {
45+
// return Err(syn::Error::new_spanned(
46+
// original_function.sig.clone(),
47+
// "procedures must be `async`",
48+
// ));
49+
// };
50+
51+
// Extract all function parameter names.
52+
let opt_arg_names = typed_args.iter().map(|arg| {
53+
if let syn::Pat::Ident(i) = &*arg.pat {
54+
let name = i.ident.to_string();
55+
quote!(Some(#name))
56+
} else {
57+
quote!(None)
58+
}
59+
});
60+
61+
let arg_tys = typed_args.iter().map(|arg| arg.ty.as_ref()).collect::<Vec<_>>();
62+
let first_arg_ty = arg_tys.first().into_iter();
63+
let rest_arg_tys = arg_tys.iter().skip(1);
64+
65+
// Extract the return type.
66+
let ret_ty_for_assert = match &original_function.sig.output {
67+
syn::ReturnType::Default => None,
68+
syn::ReturnType::Type(_, t) => Some(&**t),
69+
}
70+
.into_iter();
71+
72+
let ret_ty_for_info = match &original_function.sig.output {
73+
syn::ReturnType::Default => quote!(()),
74+
syn::ReturnType::Type(_, t) => quote!(#t),
75+
};
76+
77+
let register_describer_symbol = format!("__preinit__20_register_describer_{}", procedure_name.value());
78+
79+
let lifetime_params = &original_function.sig.generics;
80+
let lifetime_where_clause = &lifetime_params.where_clause;
81+
82+
let generated_describe_function = quote! {
83+
#[export_name = #register_describer_symbol]
84+
pub extern "C" fn __register_describer() {
85+
spacetimedb::rt::register_procedure::<_, _, #func_name>(#func_name)
86+
}
87+
};
88+
89+
Ok(quote! {
90+
const _: () = {
91+
#generated_describe_function
92+
};
93+
#[allow(non_camel_case_types)]
94+
#vis struct #func_name { _never: ::core::convert::Infallible }
95+
const _: () = {
96+
fn _assert_args #lifetime_params () #lifetime_where_clause {
97+
#(let _ = <#first_arg_ty as spacetimedb::rt::ProcedureContextArg>::_ITEM;)*
98+
#(let _ = <#rest_arg_tys as spacetimedb::rt::ProcedureArg>::_ITEM;)*
99+
#(let _ = <#ret_ty_for_assert as spacetimedb::rt::IntoProcedureResult>::to_result;)*
100+
}
101+
};
102+
impl #func_name {
103+
fn invoke(__ctx: spacetimedb::ProcedureContext, __args: &[u8]) -> spacetimedb::ProcedureResult {
104+
spacetimedb::rt::invoke_procedure(#func_name, __ctx, __args)
105+
}
106+
}
107+
#[automatically_derived]
108+
impl spacetimedb::rt::FnInfo for #func_name {
109+
/// The type of this function.
110+
type Invoke = spacetimedb::rt::ProcedureFn;
111+
112+
/// The function kind, which will cause scheduled tables to accept procedures.
113+
type FnKind = spacetimedb::rt::FnKindProcedure<#ret_ty_for_info>;
114+
115+
/// The name of this function
116+
const NAME: &'static str = #procedure_name;
117+
118+
/// The parameter names of this function
119+
const ARG_NAMES: &'static [Option<&'static str>] = &[#(#opt_arg_names),*];
120+
121+
/// The pointer for invoking this function
122+
const INVOKE: spacetimedb::rt::ProcedureFn = #func_name::invoke;
123+
124+
/// The return type of this function
125+
fn return_type(ts: &mut impl spacetimedb::sats::typespace::TypespaceBuilder) -> Option<spacetimedb::sats::AlgebraicType> {
126+
Some(<#ret_ty_for_info as spacetimedb::SpacetimeType>::make_type(ts))
127+
}
128+
}
129+
})
130+
}

crates/bindings-macro/src/reducer.rs

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use proc_macro2::{Span, TokenStream};
44
use quote::{quote, quote_spanned};
55
use syn::parse::Parser as _;
66
use syn::spanned::Spanned;
7-
use syn::{FnArg, Ident, ItemFn, LitStr};
7+
use syn::{FnArg, Ident, ItemFn, LitStr, PatType};
88

99
#[derive(Default)]
1010
pub(crate) struct ReducerArgs {
@@ -59,33 +59,50 @@ impl ReducerArgs {
5959
}
6060
}
6161

62-
pub(crate) fn reducer_impl(args: ReducerArgs, original_function: &ItemFn) -> syn::Result<TokenStream> {
63-
let func_name = &original_function.sig.ident;
64-
let vis = &original_function.vis;
65-
66-
let reducer_name = args.name.unwrap_or_else(|| ident_to_litstr(func_name));
67-
62+
pub(crate) fn assert_only_lifetime_generics(original_function: &ItemFn, function_kind_plural: &str) -> syn::Result<()> {
6863
for param in &original_function.sig.generics.params {
6964
let err = |msg| syn::Error::new_spanned(param, msg);
7065
match param {
7166
syn::GenericParam::Lifetime(_) => {}
72-
syn::GenericParam::Type(_) => return Err(err("type parameters are not allowed on reducers")),
73-
syn::GenericParam::Const(_) => return Err(err("const parameters are not allowed on reducers")),
67+
syn::GenericParam::Type(_) => {
68+
return Err(err(format!(
69+
"type parameters are not allowed on {function_kind_plural}"
70+
)))
71+
}
72+
syn::GenericParam::Const(_) => {
73+
return Err(err(format!(
74+
"const parameters are not allowed on {function_kind_plural}"
75+
)))
76+
}
7477
}
7578
}
79+
Ok(())
80+
}
7681

77-
let lifecycle = args.lifecycle.iter().filter_map(|lc| lc.to_lifecycle_value());
78-
79-
// Extract all function parameters, except for `self` ones that aren't allowed.
80-
let typed_args = original_function
82+
/// Extract all function parameters, except for `self` ones that aren't allowed.
83+
pub(crate) fn extract_typed_args(original_function: &ItemFn) -> syn::Result<Vec<&PatType>> {
84+
original_function
8185
.sig
8286
.inputs
8387
.iter()
8488
.map(|arg| match arg {
8589
FnArg::Typed(arg) => Ok(arg),
8690
_ => Err(syn::Error::new_spanned(arg, "expected typed argument")),
8791
})
88-
.collect::<syn::Result<Vec<_>>>()?;
92+
.collect()
93+
}
94+
95+
pub(crate) fn reducer_impl(args: ReducerArgs, original_function: &ItemFn) -> syn::Result<TokenStream> {
96+
let func_name = &original_function.sig.ident;
97+
let vis = &original_function.vis;
98+
99+
let reducer_name = args.name.unwrap_or_else(|| ident_to_litstr(func_name));
100+
101+
assert_only_lifetime_generics(original_function, "reducers")?;
102+
103+
let lifecycle = args.lifecycle.iter().filter_map(|lc| lc.to_lifecycle_value());
104+
105+
let typed_args = extract_typed_args(original_function)?;
89106

90107
// Extract all function parameter names.
91108
let opt_arg_names = typed_args.iter().map(|arg| {
@@ -141,6 +158,8 @@ pub(crate) fn reducer_impl(args: ReducerArgs, original_function: &ItemFn) -> syn
141158
#[automatically_derived]
142159
impl spacetimedb::rt::FnInfo for #func_name {
143160
type Invoke = spacetimedb::rt::ReducerFn;
161+
/// The function kind, which will cause scheduled tables to accept reducers.
162+
type FnKind = spacetimedb::rt::FnKindReducer;
144163
const NAME: &'static str = #reducer_name;
145164
#(const LIFECYCLE: Option<spacetimedb::rt::LifecycleReducer> = Some(#lifecycle);)*
146165
const ARG_NAMES: &'static [Option<&'static str>] = &[#(#opt_arg_names),*];

crates/bindings-macro/src/table.rs

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ impl TableAccess {
4040

4141
struct ScheduledArg {
4242
span: Span,
43-
reducer: Path,
43+
reducer_or_procedure: Path,
4444
at: Option<Ident>,
4545
}
4646

@@ -113,7 +113,7 @@ impl TableArgs {
113113
impl ScheduledArg {
114114
fn parse_meta(meta: ParseNestedMeta) -> syn::Result<Self> {
115115
let span = meta.path.span();
116-
let mut reducer = None;
116+
let mut reducer_or_procedure = None;
117117
let mut at = None;
118118

119119
meta.parse_nested_meta(|meta| {
@@ -126,16 +126,26 @@ impl ScheduledArg {
126126
}
127127
})
128128
} else {
129-
check_duplicate_msg(&reducer, &meta, "can only specify one scheduled reducer")?;
130-
reducer = Some(meta.path);
129+
check_duplicate_msg(
130+
&reducer_or_procedure,
131+
&meta,
132+
"can only specify one scheduled reducer or procedure",
133+
)?;
134+
reducer_or_procedure = Some(meta.path);
131135
}
132136
Ok(())
133137
})?;
134138

135-
let reducer = reducer.ok_or_else(|| {
136-
meta.error("must specify scheduled reducer associated with the table: scheduled(reducer_name)")
139+
let reducer_or_procedure = reducer_or_procedure.ok_or_else(|| {
140+
meta.error(
141+
"must specify scheduled reducer or procedure associated with the table: scheduled(function_name)",
142+
)
137143
})?;
138-
Ok(Self { span, reducer, at })
144+
Ok(Self {
145+
span,
146+
reducer_or_procedure,
147+
at,
148+
})
139149
}
140150
}
141151

@@ -818,17 +828,20 @@ pub(crate) fn table_impl(mut args: TableArgs, item: &syn::DeriveInput) -> syn::R
818828
)
819829
})?;
820830

821-
let reducer = &sched.reducer;
831+
let reducer_or_procedure = &sched.reducer_or_procedure;
822832
let scheduled_at_id = scheduled_at_column.index;
823833
let desc = quote!(spacetimedb::table::ScheduleDesc {
824-
reducer_name: <#reducer as spacetimedb::rt::FnInfo>::NAME,
834+
reducer_or_procedure_name: <#reducer_or_procedure as spacetimedb::rt::FnInfo>::NAME,
825835
scheduled_at_column: #scheduled_at_id,
826836
});
827837

828838
let primary_key_ty = primary_key_column.ty;
829839
let scheduled_at_ty = scheduled_at_column.ty;
830840
let typecheck = quote! {
831-
spacetimedb::rt::scheduled_reducer_typecheck::<#original_struct_ident>(#reducer);
841+
spacetimedb::rt::scheduled_typecheck::<
842+
#original_struct_ident,
843+
<#reducer_or_procedure as spacetimedb::rt::FnInfo>::FnKind,
844+
>(#reducer_or_procedure);
832845
spacetimedb::rt::assert_scheduled_table_primary_key::<#primary_key_ty>();
833846
let _ = |x: #scheduled_at_ty| { let _: spacetimedb::ScheduleAt = x; };
834847
};

crates/bindings-macro/src/view.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,9 @@ pub(crate) fn view_impl(_args: ViewArgs, original_function: &ItemFn) -> syn::Res
158158
/// The type of this function
159159
type Invoke = <spacetimedb::rt::ViewKind<#ctx_ty> as spacetimedb::rt::ViewKindTrait>::InvokeFn;
160160

161+
/// The function kind, which will cause scheduled tables to reject views.
162+
type FnKind = spacetimedb::rt::FnKindView;
163+
161164
/// The name of this function
162165
const NAME: &'static str = #view_name;
163166

crates/bindings-sys/src/lib.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1218,3 +1218,11 @@ impl Drop for RowIter {
12181218
}
12191219
}
12201220
}
1221+
1222+
pub mod procedure {
1223+
//! Side-effecting or asynchronous operations which only procedures are allowed to perform.
1224+
#[inline]
1225+
pub fn sleep_until(_wake_at_timestamp: i64) -> i64 {
1226+
todo!("Add `procedure_sleep_until` host function")
1227+
}
1228+
}

0 commit comments

Comments
 (0)