Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
4110165
Schema and moduledef extensions to describe procedures
gefjon Jul 23, 2025
ac1379a
Nix `on_abort` annotations
gefjon Jul 25, 2025
e6d6586
Fix `datastore` build errors due to renamed field
gefjon Jul 25, 2025
479d5cc
Rust `bindings` code for defining and running procedures
gefjon Aug 5, 2025
c0257bf
Add procedures to module-test, and various fixes revealed by same
gefjon Aug 5, 2025
33f7644
Merge branch 'master' into phoebe/procedures
gefjon Aug 11, 2025
175ed81
Machinery to call procedures via WS and HTTP APIs
gefjon Aug 15, 2025
fef7da5
Make procedures fully callable!
gefjon Sep 3, 2025
11276ec
In-WASM bindings for `procedure_sleep_until`
gefjon Sep 10, 2025
f26e20f
Rewrite `jobs.rs` to use single-threaded Tokio runtimes
gefjon Sep 12, 2025
54b7d5a
Thread `jobs.rs` changes through all of `core` crate.
gefjon Sep 22, 2025
ba27106
Merge remote-tracking branch 'origin/master' into phoebe/wasmtime-async
gefjon Sep 22, 2025
4c088f2
Fix standalone main tyck
gefjon Sep 22, 2025
0fedef3
Clean up testing `JobCores` initialization
gefjon Sep 22, 2025
b26d447
Wrap some tests in a `mod` so that I can `--skip` them
gefjon Sep 22, 2025
e54e728
Use Wasmtime `async` functions where necessary
gefjon Sep 22, 2025
21830cd
Fix clippy lints (except unused method I can't explain)
gefjon Sep 22, 2025
56c89e6
Add futures-util and use their `now_or_never`
gefjon Sep 23, 2025
9bb6df0
Revert "Wrap some tests in a `mod` so that I can `--skip` them"
gefjon Sep 24, 2025
c1da1b8
Fix typo'd comments revealed in PR review
gefjon Sep 25, 2025
db90e6d
Expand comments as requested in code review
gefjon Sep 25, 2025
b27c69d
Merge branch 'phoebe/wasmtime-async' of github.com:clockworklabs/Spac…
gefjon Sep 25, 2025
a4f5d50
Merge branch 'master' into phoebe/wasmtime-async
gefjon Sep 25, 2025
f1c5938
Add metric for time spent in `create_instance`
gefjon Sep 25, 2025
492bd93
Use existing `HostType` enum
gefjon Sep 25, 2025
81c6366
Remove unused `initial_instances` method
gefjon Sep 26, 2025
2c38b30
Merge branch 'phoebe/wasmtime-async' into phoebe/procedures
gefjon Oct 1, 2025
6316fff
Wire up `procedure_sleep_until`
gefjon Oct 2, 2025
9dbb4e7
Merge remote-tracking branch 'origin/master' into phoebe/wasmtime-async
gefjon Oct 2, 2025
a55c0e0
Fix panic on shutdown
gefjon Oct 2, 2025
2cdcd2a
Merge branch 'phoebe/wasmtime-async' into phoebe/procedures
gefjon Oct 3, 2025
93dd95f
Merge remote-tracking branch 'origin/master' into phoebe/procedures
gefjon Oct 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions crates/bindings-macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
//
// (private documentation for the macro authors is totally fine here and you SHOULD write that!)

mod procedure;
mod reducer;
mod sats;
mod table;
Expand Down Expand Up @@ -104,6 +105,14 @@ mod sym {
}
}

#[proc_macro_attribute]
pub fn procedure(args: StdTokenStream, item: StdTokenStream) -> StdTokenStream {
cvt_attr::<ItemFn>(args, item, quote!(), |args, original_function| {
let args = procedure::ProcedureArgs::parse(args)?;
procedure::procedure_impl(args, original_function)
})
}

#[proc_macro_attribute]
pub fn reducer(args: StdTokenStream, item: StdTokenStream) -> StdTokenStream {
cvt_attr::<ItemFn>(args, item, quote!(), |args, original_function| {
Expand Down
102 changes: 102 additions & 0 deletions crates/bindings-macro/src/procedure.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
use crate::reducer::{assert_only_lifetime_generics, extract_typed_args};
use crate::sym;
use crate::util::{check_duplicate, ident_to_litstr, match_meta};
use proc_macro2::TokenStream;
use quote::quote;
use syn::parse::Parser as _;
use syn::{ItemFn, LitStr};

#[derive(Default)]
pub(crate) struct ProcedureArgs {
name: Option<LitStr>,
}

impl ProcedureArgs {
pub(crate) fn parse(input: TokenStream) -> syn::Result<Self> {
let mut args = Self::default();
syn::meta::parser(|meta| {
match_meta!(match meta {
sym::name => {
check_duplicate(&args.name, &meta)?;
args.name = Some(meta.value()?.parse()?);
}
});
Ok(())
})
.parse2(input)?;
Ok(args)
}
}

pub(crate) fn procedure_impl(args: ProcedureArgs, original_function: &ItemFn) -> syn::Result<TokenStream> {
let func_name = &original_function.sig.ident;
let vis = &original_function.vis;

let procedure_name = args.name.unwrap_or_else(|| ident_to_litstr(func_name));

assert_only_lifetime_generics(original_function, "procedures")?;

let typed_args = extract_typed_args(original_function)?;

// Extract all function parameter names.
let opt_arg_names = typed_args.iter().map(|arg| {
if let syn::Pat::Ident(i) = &*arg.pat {
let name = i.ident.to_string();
quote!(Some(#name))
} else {
quote!(None)
}
});

let arg_tys = typed_args.iter().map(|arg| arg.ty.as_ref()).collect::<Vec<_>>();
let first_arg_ty = arg_tys.first().into_iter();
let rest_arg_tys = arg_tys.iter().skip(1);

// Extract the return type.
let ret_ty = match &original_function.sig.output {
syn::ReturnType::Default => None,
syn::ReturnType::Type(_, t) => Some(&**t),
}
.into_iter();

let register_describer_symbol = format!("__preinit__20_register_describer_{}", procedure_name.value());

let lifetime_params = &original_function.sig.generics;
let lifetime_where_clause = &lifetime_params.where_clause;

let generated_describe_function = quote! {
#[export_name = #register_describer_symbol]
pub extern "C" fn __register_describer() {
spacetimedb::rt::register_procedure::<_, _, #func_name>(#func_name)
}
};

Ok(quote! {
const _: () = {
#generated_describe_function
};
#[allow(non_camel_case_types)]
#vis struct #func_name { _never: ::core::convert::Infallible }
const _: () = {
fn _assert_args #lifetime_params () #lifetime_where_clause {
#(let _ = <#first_arg_ty as spacetimedb::rt::ProcedureContextArg>::_ITEM;)*
#(let _ = <#rest_arg_tys as spacetimedb::rt::ProcedureArg>::_ITEM;)*
#(let _ = <#ret_ty as spacetimedb::rt::IntoProcedureResult>::into_result;)*
}
};
impl #func_name {
fn invoke(__ctx: spacetimedb::ProcedureContext, __args: &[u8]) -> spacetimedb::ProcedureResult {
spacetimedb::rt::invoke_procedure(#func_name, __ctx, __args)
}
}
#[automatically_derived]
impl spacetimedb::rt::ExportFunctionInfo for #func_name {
const NAME: &'static str = #procedure_name;
const ARG_NAMES: &'static [Option<&'static str>] = &[#(#opt_arg_names),*];
}
#[automatically_derived]
impl spacetimedb::rt::ProcedureInfo for #func_name {
const INVOKE: spacetimedb::rt::ProcedureFn = #func_name::invoke;
}
})
}
52 changes: 36 additions & 16 deletions crates/bindings-macro/src/reducer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use proc_macro2::{Span, TokenStream};
use quote::{quote, quote_spanned};
use syn::parse::Parser as _;
use syn::spanned::Spanned;
use syn::{FnArg, Ident, ItemFn, LitStr};
use syn::{FnArg, Ident, ItemFn, LitStr, PatType};

#[derive(Default)]
pub(crate) struct ReducerArgs {
Expand Down Expand Up @@ -59,33 +59,50 @@ impl ReducerArgs {
}
}

pub(crate) fn reducer_impl(args: ReducerArgs, original_function: &ItemFn) -> syn::Result<TokenStream> {
let func_name = &original_function.sig.ident;
let vis = &original_function.vis;

let reducer_name = args.name.unwrap_or_else(|| ident_to_litstr(func_name));

pub(crate) fn assert_only_lifetime_generics(original_function: &ItemFn, function_kind_plural: &str) -> syn::Result<()> {
for param in &original_function.sig.generics.params {
let err = |msg| syn::Error::new_spanned(param, msg);
match param {
syn::GenericParam::Lifetime(_) => {}
syn::GenericParam::Type(_) => return Err(err("type parameters are not allowed on reducers")),
syn::GenericParam::Const(_) => return Err(err("const parameters are not allowed on reducers")),
syn::GenericParam::Type(_) => {
return Err(err(format!(
"type parameters are not allowed on {function_kind_plural}"
)))
}
syn::GenericParam::Const(_) => {
return Err(err(format!(
"const parameters are not allowed on {function_kind_plural}"
)))
}
}
}
Ok(())
}

let lifecycle = args.lifecycle.iter().filter_map(|lc| lc.to_lifecycle_value());

// Extract all function parameters, except for `self` ones that aren't allowed.
let typed_args = original_function
pub(crate) fn extract_typed_args(original_function: &ItemFn) -> syn::Result<Vec<&PatType>> {
original_function
.sig
.inputs
.iter()
.map(|arg| match arg {
FnArg::Typed(arg) => Ok(arg),
_ => Err(syn::Error::new_spanned(arg, "expected typed argument")),
})
.collect::<syn::Result<Vec<_>>>()?;
.collect()
}

pub(crate) fn reducer_impl(args: ReducerArgs, original_function: &ItemFn) -> syn::Result<TokenStream> {
let func_name = &original_function.sig.ident;
let vis = &original_function.vis;

let reducer_name = args.name.unwrap_or_else(|| ident_to_litstr(func_name));

assert_only_lifetime_generics(original_function, "reducers")?;

let lifecycle = args.lifecycle.iter().filter_map(|lc| lc.to_lifecycle_value());

// Extract all function parameters, except for `self` ones that aren't allowed.
let typed_args = extract_typed_args(original_function)?;

// Extract all function parameter names.
let opt_arg_names = typed_args.iter().map(|arg| {
Expand Down Expand Up @@ -139,10 +156,13 @@ pub(crate) fn reducer_impl(args: ReducerArgs, original_function: &ItemFn) -> syn
}
}
#[automatically_derived]
impl spacetimedb::rt::ReducerInfo for #func_name {
impl spacetimedb::rt::ExportFunctionInfo for #func_name {
const NAME: &'static str = #reducer_name;
#(const LIFECYCLE: Option<spacetimedb::rt::LifecycleReducer> = Some(#lifecycle);)*
const ARG_NAMES: &'static [Option<&'static str>] = &[#(#opt_arg_names),*];
}
#[automatically_derived]
impl spacetimedb::rt::ReducerInfo for #func_name {
#(const LIFECYCLE: Option<spacetimedb::rt::LifecycleReducer> = Some(#lifecycle);)*
const INVOKE: spacetimedb::rt::ReducerFn = #func_name::invoke;
}
})
Expand Down
30 changes: 20 additions & 10 deletions crates/bindings-macro/src/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ impl TableAccess {

struct ScheduledArg {
span: Span,
reducer: Path,
reducer_or_procedure: Path,
at: Option<Ident>,
}

Expand Down Expand Up @@ -113,7 +113,7 @@ impl TableArgs {
impl ScheduledArg {
fn parse_meta(meta: ParseNestedMeta) -> syn::Result<Self> {
let span = meta.path.span();
let mut reducer = None;
let mut reducer_or_procedure = None;
let mut at = None;

meta.parse_nested_meta(|meta| {
Expand All @@ -126,16 +126,26 @@ impl ScheduledArg {
}
})
} else {
check_duplicate_msg(&reducer, &meta, "can only specify one scheduled reducer")?;
reducer = Some(meta.path);
check_duplicate_msg(
&reducer_or_procedure,
&meta,
"can only specify one scheduled reducer or procedure",
)?;
reducer_or_procedure = Some(meta.path);
}
Ok(())
})?;

let reducer = reducer.ok_or_else(|| {
meta.error("must specify scheduled reducer associated with the table: scheduled(reducer_name)")
let reducer_or_procedure = reducer_or_procedure.ok_or_else(|| {
meta.error(
"must specify scheduled reducer or procedure associated with the table: scheduled(function_name)",
)
})?;
Ok(Self { span, reducer, at })
Ok(Self {
span,
reducer_or_procedure,
at,
})
}
}

Expand Down Expand Up @@ -818,17 +828,17 @@ pub(crate) fn table_impl(mut args: TableArgs, item: &syn::DeriveInput) -> syn::R
)
})?;

let reducer = &sched.reducer;
let reducer_or_procedure = &sched.reducer_or_procedure;
let scheduled_at_id = scheduled_at_column.index;
let desc = quote!(spacetimedb::table::ScheduleDesc {
reducer_name: <#reducer as spacetimedb::rt::ReducerInfo>::NAME,
reducer_or_procedure_name: <#reducer_or_procedure as spacetimedb::rt::ExportFunctionInfo>::NAME,
scheduled_at_column: #scheduled_at_id,
});

let primary_key_ty = primary_key_column.ty;
let scheduled_at_ty = scheduled_at_column.ty;
let typecheck = quote! {
spacetimedb::rt::scheduled_reducer_typecheck::<#original_struct_ident>(#reducer);
spacetimedb::rt::scheduled_typecheck::<#original_struct_ident>(#reducer_or_procedure);
spacetimedb::rt::assert_scheduled_table_primary_key::<#primary_key_ty>();
let _ = |x: #scheduled_at_ty| { let _: spacetimedb::ScheduleAt = x; };
};
Expand Down
24 changes: 24 additions & 0 deletions crates/bindings-sys/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,21 @@ pub mod raw {
// See comment on previous `extern "C"` block re: ABI version.
#[link(wasm_import_module = "spacetime_10.1")]
extern "C" {
/// Suspends execution of this WASM instance until approximately `wake_at_micros_since_unix_epoch`.
///
/// Returns immediately if `wake_at_micros_since_unix_epoch` is in the past.
///
/// Upon resuming, returns the current timestamp as microseconds since the Unix epoch.
///
/// Not particularly useful, except for testing SpacetimeDB internals related to suspending procedure execution.
/// # Traps
///
/// Traps if:
///
/// - The calling WASM instance is holding open a transaction.
/// - The calling WASM instance is not executing a procedure.
pub fn procedure_sleep_until(wake_at_micros_since_unix_epoch: i64) -> i64;

/// Read the remaining length of a [`BytesSource`] and write it to `out`.
///
/// Note that the host automatically frees byte sources which are exhausted.
Expand Down Expand Up @@ -1169,3 +1184,12 @@ impl Drop for RowIter {
}
}
}

pub mod procedure {
#[inline]
pub fn sleep_until(wake_at_timestamp: i64) -> i64 {
// Safety: Just calling an `extern "C"` function.
// Nothing weird happening here.
unsafe { super::raw::procedure_sleep_until(wake_at_timestamp) }
}
}
Loading
Loading