Skip to content

Use spacetimedb-standalone to get schema for spacetime generate #2644

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 1 addition & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

42 changes: 31 additions & 11 deletions crates/cli/src/subcommands/generate.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
#![warn(clippy::uninlined_format_args)]

use anyhow::Context;
use clap::parser::ValueSource;
use clap::Arg;
use clap::ArgAction::Set;
use fs_err as fs;
use spacetimedb_codegen::{
compile_wasm, extract_descriptions_from_module, generate, Csharp, Lang, Rust, TypeScript, AUTO_GENERATED_PREFIX,
};
use spacetimedb_codegen::{generate, Csharp, Lang, Rust, TypeScript, AUTO_GENERATED_PREFIX};
use spacetimedb_lib::de::serde::DeserializeWrapper;
use spacetimedb_lib::{sats, RawModuleDef};
use spacetimedb_schema;
use spacetimedb_schema::def::ModuleDef;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};

use crate::tasks::csharp::dotnet_format;
use crate::tasks::rust::rustfmt;
use crate::util::y_or_n;
use crate::util::{resolve_sibling_binary, y_or_n};
use crate::Config;
use crate::{build, common_args};
use clap::builder::PossibleValue;
Expand Down Expand Up @@ -88,6 +89,15 @@ pub fn cli() -> clap::Command {
}

pub async fn exec(config: Config, args: &clap::ArgMatches) -> anyhow::Result<()> {
exec_ex(config, args, extract_descriptions).await
}

/// Like `exec`, but lets you specify a custom a function to extract a schema from a file.
pub async fn exec_ex(
config: Config,
args: &clap::ArgMatches,
extract_descriptions: ExtractDescriptions,
) -> anyhow::Result<()> {
let project_path = args.get_one::<PathBuf>("project_path").unwrap();
let wasm_file = args.get_one::<PathBuf>("wasm_file").cloned();
let json_module = args.get_many::<PathBuf>("json_module");
Expand All @@ -101,13 +111,13 @@ pub async fn exec(config: Config, args: &clap::ArgMatches) -> anyhow::Result<()>
return Err(anyhow::anyhow!("--namespace is only supported with --lang csharp"));
}

let module = if let Some(mut json_module) = json_module {
let DeserializeWrapper(module) = if let Some(path) = json_module.next() {
let module: ModuleDef = if let Some(mut json_module) = json_module {
let DeserializeWrapper::<RawModuleDef>(module) = if let Some(path) = json_module.next() {
serde_json::from_slice(&fs::read(path)?)?
} else {
serde_json::from_reader(std::io::stdin().lock())?
};
module
module.try_into()?
} else {
let wasm_path = if let Some(path) = wasm_file {
println!("Skipping build. Instead we are inspecting {}", path.display());
Expand All @@ -117,12 +127,9 @@ pub async fn exec(config: Config, args: &clap::ArgMatches) -> anyhow::Result<()>
};
let spinner = indicatif::ProgressBar::new_spinner();
spinner.enable_steady_tick(std::time::Duration::from_millis(60));
spinner.set_message("Compiling wasm...");
let module = compile_wasm(&wasm_path)?;
spinner.set_message("Extracting schema from wasm...");
extract_descriptions_from_module(module)?
extract_descriptions(&wasm_path).context("could not extract schema")?
};
let module: ModuleDef = module.try_into()?;

fs::create_dir_all(out_dir)?;

Expand Down Expand Up @@ -234,3 +241,16 @@ impl Language {
Ok(())
}
}

pub type ExtractDescriptions = fn(&Path) -> anyhow::Result<ModuleDef>;
fn extract_descriptions(wasm_file: &Path) -> anyhow::Result<ModuleDef> {
let bin_path = resolve_sibling_binary("spacetimedb-standalone")?;
let child = Command::new(&bin_path)
.arg("extract-schema")
.arg(wasm_file)
.stdout(Stdio::piped())
.spawn()
.with_context(|| format!("failed to spawn {}", bin_path.display()))?;
let sats::serde::SerdeWrapper::<RawModuleDef>(module) = serde_json::from_reader(child.stdout.unwrap())?;
Ok(module.try_into()?)
}
9 changes: 3 additions & 6 deletions crates/cli/src/subcommands/start.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use anyhow::Context;
use clap::{Arg, ArgMatches};
use spacetimedb_paths::SpacetimePaths;

use crate::util::resolve_sibling_binary;

pub fn cli() -> clap::Command {
clap::Command::new("start")
.about("Start a local SpacetimeDB instance")
Expand Down Expand Up @@ -45,12 +47,7 @@ pub async fn exec(paths: &SpacetimePaths, args: &ArgMatches) -> anyhow::Result<E
Edition::Standalone => "spacetimedb-standalone",
Edition::Cloud => "spacetimedb-cloud",
};
let resolved_exe = std::env::current_exe().context("could not retrieve current exe")?;
let bin_path = resolved_exe
.parent()
.unwrap()
.join(bin_name)
.with_extension(std::env::consts::EXE_EXTENSION);
let bin_path = resolve_sibling_binary(bin_name)?;
let mut cmd = Command::new(&bin_path);
cmd.arg("start")
.arg("--data-dir")
Expand Down
12 changes: 11 additions & 1 deletion crates/cli/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use spacetimedb_auth::identity::{IncomingClaims, SpacetimeIdentityClaims};
use spacetimedb_client_api_messages::name::GetNamesResponse;
use spacetimedb_lib::Identity;
use std::io::Write;
use std::path::Path;
use std::path::{Path, PathBuf};

use crate::config::Config;
use crate::login::{spacetimedb_login_force, DEFAULT_AUTH_HOST};
Expand Down Expand Up @@ -320,3 +320,13 @@ pub async fn get_login_token_or_log_in(
spacetimedb_login_force(config, &host, true).await
}
}

pub fn resolve_sibling_binary(bin_name: &str) -> anyhow::Result<PathBuf> {
let resolved_exe = std::env::current_exe().context("could not retrieve current exe")?;
let bin_path = resolved_exe
.parent()
.unwrap()
.join(bin_name)
.with_extension(std::env::consts::EXE_EXTENSION);
Ok(bin_path)
}
1 change: 0 additions & 1 deletion crates/codegen/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ spacetimedb-schema.workspace = true
anyhow.workspace = true
convert_case.workspace = true
itertools.workspace = true
wasmtime.workspace = true

[dev-dependencies]
fs-err.workspace = true
Expand Down
120 changes: 0 additions & 120 deletions crates/codegen/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
use std::mem;
use std::path::Path;

use anyhow::Context;
use spacetimedb_lib::{bsatn, RawModuleDefV8};
use spacetimedb_lib::{RawModuleDef, MODULE_ABI_MAJOR_VERSION};
use spacetimedb_primitives::errno;
use spacetimedb_schema::def::{ModuleDef, ReducerDef, ScopedTypeName, TableDef, TypeDef};
use spacetimedb_schema::identifier::Identifier;
use wasmtime::{Caller, StoreContextMut};

mod code_indenter;
pub mod csharp;
Expand Down Expand Up @@ -49,115 +41,3 @@ pub trait Lang {
fn generate_reducer(&self, module: &ModuleDef, reducer: &ReducerDef) -> String;
fn generate_globals(&self, module: &ModuleDef) -> Vec<(String, String)>;
}

pub fn extract_descriptions(wasm_file: &Path) -> anyhow::Result<RawModuleDef> {
let module = compile_wasm(wasm_file)?;
extract_descriptions_from_module(module)
}

pub fn compile_wasm(wasm_file: &Path) -> anyhow::Result<wasmtime::Module> {
wasmtime::Module::from_file(&wasmtime::Engine::default(), wasm_file)
}

#[allow(clippy::disallowed_macros)]
pub fn extract_descriptions_from_module(module: wasmtime::Module) -> anyhow::Result<RawModuleDef> {
let engine = module.engine();
let ctx = WasmCtx {
mem: None,
sink: Vec::new(),
};
let mut store = wasmtime::Store::new(engine, ctx);
let mut linker = wasmtime::Linker::new(engine);
linker.allow_shadowing(true).define_unknown_imports_as_traps(&module)?;
let module_name = &*format!("spacetime_{MODULE_ABI_MAJOR_VERSION}.0");
linker.func_wrap(
module_name,
"console_log",
|mut caller: Caller<'_, WasmCtx>,
_level: u32,
_target_ptr: u32,
_target_len: u32,
_filename_ptr: u32,
_filename_len: u32,
_line_number: u32,
message_ptr: u32,
message_len: u32| {
let (mem, _) = WasmCtx::mem_env(&mut caller);
let slice = deref_slice(mem, message_ptr, message_len).unwrap();
println!("from wasm: {}", String::from_utf8_lossy(slice));
},
)?;
linker.func_wrap(module_name, "bytes_sink_write", WasmCtx::bytes_sink_write)?;
let instance = linker.instantiate(&mut store, &module)?;
let memory = instance.get_memory(&mut store, "memory").context("no memory export")?;
store.data_mut().mem = Some(memory);

let mut preinits = instance
.exports(&mut store)
.filter_map(|exp| Some((exp.name().strip_prefix("__preinit__")?.to_owned(), exp.into_func()?)))
.collect::<Vec<_>>();
preinits.sort_by(|(a, _), (b, _)| a.cmp(b));
for (_, func) in preinits {
func.typed(&store)?.call(&mut store, ())?
}
let module: RawModuleDef = match instance.get_func(&mut store, "__describe_module__") {
Some(f) => {
store.data_mut().sink = Vec::new();
f.typed::<u32, ()>(&store)?.call(&mut store, 1)?;
let buf = mem::take(&mut store.data_mut().sink);
bsatn::from_slice(&buf)?
}
// TODO: shouldn't we return an error here?
None => RawModuleDef::V8BackCompat(RawModuleDefV8::default()),
};
Ok(module)
}

struct WasmCtx {
mem: Option<wasmtime::Memory>,
sink: Vec<u8>,
}

fn deref_slice(mem: &[u8], offset: u32, len: u32) -> anyhow::Result<&[u8]> {
anyhow::ensure!(offset != 0, "ptr is null");
mem.get(offset as usize..)
.and_then(|s| s.get(..len as usize))
.context("pointer out of bounds")
}

fn read_u32(mem: &[u8], offset: u32) -> anyhow::Result<u32> {
Ok(u32::from_le_bytes(deref_slice(mem, offset, 4)?.try_into().unwrap()))
}

impl WasmCtx {
pub fn get_mem(&self) -> wasmtime::Memory {
self.mem.expect("Initialized memory")
}

fn mem_env<'a>(ctx: impl Into<StoreContextMut<'a, Self>>) -> (&'a mut [u8], &'a mut Self) {
let ctx = ctx.into();
let mem = ctx.data().get_mem();
mem.data_and_store_mut(ctx)
}

pub fn bytes_sink_write(
mut caller: Caller<'_, Self>,
sink_handle: u32,
buffer_ptr: u32,
buffer_len_ptr: u32,
) -> anyhow::Result<u32> {
if sink_handle != 1 {
return Ok(errno::NO_SUCH_BYTES.get().into());
}

let (mem, env) = Self::mem_env(&mut caller);

// Read `buffer_len`, i.e., the capacity of `buffer` pointed to by `buffer_ptr`.
let buffer_len = read_u32(mem, buffer_len_ptr)?;
// Write `buffer` to `sink`.
let buffer = deref_slice(mem, buffer_ptr, buffer_len)?;
env.sink.extend(buffer);

Ok(0)
}
}
13 changes: 6 additions & 7 deletions crates/codegen/tests/codegen.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
use spacetimedb_codegen::{extract_descriptions, generate, Csharp, Rust, TypeScript};
use spacetimedb_codegen::{generate, Csharp, Rust, TypeScript};
use spacetimedb_data_structures::map::HashMap;
use spacetimedb_schema::def::ModuleDef;
use spacetimedb_testing::modules::{CompilationMode, CompiledModule};
use std::path::Path;
use std::sync::OnceLock;

fn compiled_module() -> &'static Path {
static COMPILED_MODULE: OnceLock<CompiledModule> = OnceLock::new();
fn compiled_module() -> &'static ModuleDef {
static COMPILED_MODULE: OnceLock<ModuleDef> = OnceLock::new();
COMPILED_MODULE
.get_or_init(|| CompiledModule::compile("module-test", CompilationMode::Debug))
.path()
.get_or_init(|| CompiledModule::compile("module-test", CompilationMode::Debug).extract_schema_blocking())
}

macro_rules! declare_tests {
($($name:ident => $lang:expr,)*) => ($(
#[test]
fn $name() {
let module = extract_descriptions(compiled_module()).unwrap().try_into().unwrap();
let module = compiled_module();
let outfiles = HashMap::<_, _>::from_iter(generate(&module, &$lang));
insta::with_settings!({ sort_maps => true }, {
insta::assert_toml_snapshot!(outfiles);
Expand Down
Loading
Loading