Skip to content

Commit 020d64c

Browse files
coolreader18bfops
andauthored
Split client codegen out into its own crate (#2593)
Co-authored-by: Zeke Foppa <[email protected]>
1 parent 228ec0c commit 020d64c

23 files changed

+303
-266
lines changed

.github/CODEOWNERS

-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ LICENSE.txt @cloutiertyler
55
/crates/client-api-messages/src/websocket.rs @centril @gefjon
66

77
/crates/cli/src/ @bfops @cloutiertyler @jdetter
8-
/crates/cli/src/subcommands/generate/ # No owners
9-
/crates/cli/src/subcommands/generate/mod.rs @bfops @cloutiertyler @jdetter # These codeowners should be the same as the "root" CLI codeowners
108

119
/crates/sdk/examples/quickstart-chat/ @gefjon
1210
/modules/quickstart-chat/ @gefjon

.github/workflows/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ jobs:
106106

107107
- name: Ensure C# autogen bindings are up-to-date
108108
run: |
109-
cargo run --example regen-csharp-moduledef
109+
cargo run -p spacetimedb-codegen --example regen-csharp-moduledef
110110
git diff --exit-code -- crates/bindings-csharp
111111
112112
- name: C# bindings tests

Cargo.lock

+20-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ members = [
4343
"crates/sdk/tests/test-client",
4444
"crates/sdk/tests/test-counter",
4545
"crates/sdk/tests/connect_disconnect_client",
46-
"tools/upgrade-version",
46+
"tools/upgrade-version", "crates/codegen",
4747
]
4848
default-members = ["crates/cli", "crates/standalone", "crates/update"]
4949
# cargo feature graph resolver. v3 is default in edition2024 but workspace
@@ -100,6 +100,7 @@ spacetimedb-bindings-sys = { path = "crates/bindings-sys", version = "1.1.1" }
100100
spacetimedb-cli = { path = "crates/cli", version = "1.1.1" }
101101
spacetimedb-client-api = { path = "crates/client-api", version = "1.1.1" }
102102
spacetimedb-client-api-messages = { path = "crates/client-api-messages", version = "1.1.1" }
103+
spacetimedb-codegen = { path = "crates/codegen", version = "1.1.1" }
103104
spacetimedb-commitlog = { path = "crates/commitlog", version = "1.1.1" }
104105
spacetimedb-core = { path = "crates/core", version = "1.1.1" }
105106
spacetimedb-data-structures = { path = "crates/data-structures", version = "1.1.1" }

crates/bindings-csharp/Runtime/README.md

+2-3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,5 @@ The result is a WebAssembly module FFI-compatible with SpacetimeDB and with no W
1919
To regenenerate the `Autogen` folder, run:
2020

2121
```sh
22-
cd ../../cli
23-
cargo run --example regen-csharp-moduledef
24-
```
22+
cargo run -p spacetimedb-codegen --example regen-csharp-moduledef
23+
```

crates/cli/Cargo.toml

+2-7
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ bench = false
2323
[dependencies]
2424
spacetimedb-auth.workspace = true
2525
spacetimedb-client-api-messages.workspace = true
26-
spacetimedb-data-structures = { workspace = true, features = ["serde"] }
26+
spacetimedb-codegen.workspace = true
27+
spacetimedb-data-structures.workspace = true
2728
spacetimedb-fs-utils.workspace = true
2829
spacetimedb-lib.workspace = true
2930
spacetimedb-paths.workspace = true
@@ -71,7 +72,6 @@ toml_edit.workspace = true
7172
tracing = { workspace = true, features = ["release_max_level_off"] }
7273
walkdir.workspace = true
7374
wasmbin.workspace = true
74-
wasmtime.workspace = true
7575
webbrowser.workspace = true
7676
clap-markdown.workspace = true
7777

@@ -81,8 +81,3 @@ tikv-jemalloc-ctl = { workspace = true }
8181

8282
[target.'cfg(windows)'.dependencies]
8383
windows-sys = { workspace = true, features = ["Win32_System_Console"] }
84-
85-
[dev-dependencies]
86-
insta.workspace = true
87-
fs-err.workspace = true
88-
spacetimedb-testing = { path = "../testing" }
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,25 @@
11
#![warn(clippy::uninlined_format_args)]
22

3-
use anyhow::Context;
43
use clap::parser::ValueSource;
54
use clap::Arg;
65
use clap::ArgAction::Set;
7-
use core::mem;
86
use fs_err as fs;
7+
use spacetimedb_codegen::{
8+
compile_wasm, extract_descriptions_from_module, generate, Csharp, Lang, Rust, TypeScript, AUTO_GENERATED_PREFIX,
9+
};
910
use spacetimedb_lib::de::serde::DeserializeWrapper;
10-
use spacetimedb_lib::{bsatn, RawModuleDefV8};
11-
use spacetimedb_lib::{RawModuleDef, MODULE_ABI_MAJOR_VERSION};
12-
use spacetimedb_primitives::errno;
1311
use spacetimedb_schema;
14-
use spacetimedb_schema::def::{ModuleDef, ReducerDef, ScopedTypeName, TableDef, TypeDef};
15-
use spacetimedb_schema::identifier::Identifier;
12+
use spacetimedb_schema::def::ModuleDef;
1613
use std::path::{Path, PathBuf};
17-
use wasmtime::{Caller, StoreContextMut};
1814

19-
use crate::generate::util::iter_reducers;
15+
use crate::tasks::csharp::dotnet_format;
16+
use crate::tasks::rust::rustfmt;
2017
use crate::util::y_or_n;
2118
use crate::Config;
2219
use crate::{build, common_args};
2320
use clap::builder::PossibleValue;
2421
use std::collections::BTreeSet;
2522
use std::io::Read;
26-
use util::AUTO_GENERATED_PREFIX;
27-
28-
mod code_indenter;
29-
pub mod csharp;
30-
pub mod rust;
31-
pub mod typescript;
32-
mod util;
3323

3424
pub fn cli() -> clap::Command {
3525
clap::Command::new("generate")
@@ -132,22 +122,23 @@ pub async fn exec(config: Config, args: &clap::ArgMatches) -> anyhow::Result<()>
132122
spinner.set_message("Extracting schema from wasm...");
133123
extract_descriptions_from_module(module)?
134124
};
125+
let module: ModuleDef = module.try_into()?;
135126

136127
fs::create_dir_all(out_dir)?;
137128

138129
let mut paths = BTreeSet::new();
139130

140131
let csharp_lang;
141-
let lang = match lang {
132+
let gen_lang = match lang {
142133
Language::Csharp => {
143-
csharp_lang = csharp::Csharp { namespace };
134+
csharp_lang = Csharp { namespace };
144135
&csharp_lang as &dyn Lang
145136
}
146-
Language::Rust => &rust::Rust,
147-
Language::TypeScript => &typescript::TypeScript,
137+
Language::Rust => &Rust,
138+
Language::TypeScript => &TypeScript,
148139
};
149140

150-
for (fname, code) in generate(module, lang)? {
141+
for (fname, code) in generate(&module, gen_lang) {
151142
let fname = Path::new(&fname);
152143
// If a generator asks for a file in a subdirectory, create the subdirectory first.
153144
if let Some(parent) = fname.parent().filter(|p| !p.as_os_str().is_empty()) {
@@ -223,156 +214,23 @@ impl clap::ValueEnum for Language {
223214
}
224215
fn to_possible_value(&self) -> Option<PossibleValue> {
225216
Some(match self {
226-
Self::Csharp => csharp::Csharp::clap_value(),
227-
Self::TypeScript => typescript::TypeScript::clap_value(),
228-
Self::Rust => rust::Rust::clap_value(),
217+
Self::Csharp => clap::builder::PossibleValue::new("csharp").aliases(["c#", "cs"]),
218+
Self::TypeScript => clap::builder::PossibleValue::new("typescript").aliases(["ts", "TS"]),
219+
Self::Rust => clap::builder::PossibleValue::new("rust").aliases(["rs", "RS"]),
229220
})
230221
}
231222
}
232223

233-
pub fn generate(module: RawModuleDef, lang: &dyn Lang) -> anyhow::Result<Vec<(String, String)>> {
234-
let module = &ModuleDef::try_from(module)?;
235-
Ok(itertools::chain!(
236-
module
237-
.tables()
238-
.map(|tbl| { (lang.table_filename(module, tbl), lang.generate_table(module, tbl),) }),
239-
module
240-
.types()
241-
.map(|typ| { (lang.type_filename(&typ.name), lang.generate_type(module, typ),) }),
242-
iter_reducers(module).map(|reducer| {
243-
(
244-
lang.reducer_filename(&reducer.name),
245-
lang.generate_reducer(module, reducer),
246-
)
247-
}),
248-
lang.generate_globals(module),
249-
)
250-
.collect())
251-
}
252-
253-
pub trait Lang {
254-
fn table_filename(&self, module: &ModuleDef, table: &TableDef) -> String;
255-
fn type_filename(&self, type_name: &ScopedTypeName) -> String;
256-
fn reducer_filename(&self, reducer_name: &Identifier) -> String;
257-
258-
fn generate_table(&self, module: &ModuleDef, tbl: &TableDef) -> String;
259-
fn generate_type(&self, module: &ModuleDef, typ: &TypeDef) -> String;
260-
fn generate_reducer(&self, module: &ModuleDef, reducer: &ReducerDef) -> String;
261-
fn generate_globals(&self, module: &ModuleDef) -> Vec<(String, String)>;
262-
263-
fn format_files(&self, generated_files: BTreeSet<PathBuf>) -> anyhow::Result<()>;
264-
fn clap_value() -> PossibleValue
265-
where
266-
Self: Sized;
267-
}
268-
269-
pub fn extract_descriptions(wasm_file: &Path) -> anyhow::Result<RawModuleDef> {
270-
let module = compile_wasm(wasm_file)?;
271-
extract_descriptions_from_module(module)
272-
}
273-
274-
fn compile_wasm(wasm_file: &Path) -> anyhow::Result<wasmtime::Module> {
275-
wasmtime::Module::from_file(&wasmtime::Engine::default(), wasm_file)
276-
}
277-
278-
fn extract_descriptions_from_module(module: wasmtime::Module) -> anyhow::Result<RawModuleDef> {
279-
let engine = module.engine();
280-
let ctx = WasmCtx {
281-
mem: None,
282-
sink: Vec::new(),
283-
};
284-
let mut store = wasmtime::Store::new(engine, ctx);
285-
let mut linker = wasmtime::Linker::new(engine);
286-
linker.allow_shadowing(true).define_unknown_imports_as_traps(&module)?;
287-
let module_name = &*format!("spacetime_{MODULE_ABI_MAJOR_VERSION}.0");
288-
linker.func_wrap(
289-
module_name,
290-
"console_log",
291-
|mut caller: Caller<'_, WasmCtx>,
292-
_level: u32,
293-
_target_ptr: u32,
294-
_target_len: u32,
295-
_filename_ptr: u32,
296-
_filename_len: u32,
297-
_line_number: u32,
298-
message_ptr: u32,
299-
message_len: u32| {
300-
let (mem, _) = WasmCtx::mem_env(&mut caller);
301-
let slice = deref_slice(mem, message_ptr, message_len).unwrap();
302-
println!("from wasm: {}", String::from_utf8_lossy(slice));
303-
},
304-
)?;
305-
linker.func_wrap(module_name, "bytes_sink_write", WasmCtx::bytes_sink_write)?;
306-
let instance = linker.instantiate(&mut store, &module)?;
307-
let memory = instance.get_memory(&mut store, "memory").context("no memory export")?;
308-
store.data_mut().mem = Some(memory);
309-
310-
let mut preinits = instance
311-
.exports(&mut store)
312-
.filter_map(|exp| Some((exp.name().strip_prefix("__preinit__")?.to_owned(), exp.into_func()?)))
313-
.collect::<Vec<_>>();
314-
preinits.sort_by(|(a, _), (b, _)| a.cmp(b));
315-
for (_, func) in preinits {
316-
func.typed(&store)?.call(&mut store, ())?
317-
}
318-
let module: RawModuleDef = match instance.get_func(&mut store, "__describe_module__") {
319-
Some(f) => {
320-
store.data_mut().sink = Vec::new();
321-
f.typed::<u32, ()>(&store)?.call(&mut store, 1)?;
322-
let buf = mem::take(&mut store.data_mut().sink);
323-
bsatn::from_slice(&buf)?
324-
}
325-
// TODO: shouldn't we return an error here?
326-
None => RawModuleDef::V8BackCompat(RawModuleDefV8::default()),
327-
};
328-
Ok(module)
329-
}
330-
331-
struct WasmCtx {
332-
mem: Option<wasmtime::Memory>,
333-
sink: Vec<u8>,
334-
}
335-
336-
fn deref_slice(mem: &[u8], offset: u32, len: u32) -> anyhow::Result<&[u8]> {
337-
anyhow::ensure!(offset != 0, "ptr is null");
338-
mem.get(offset as usize..)
339-
.and_then(|s| s.get(..len as usize))
340-
.context("pointer out of bounds")
341-
}
342-
343-
fn read_u32(mem: &[u8], offset: u32) -> anyhow::Result<u32> {
344-
Ok(u32::from_le_bytes(deref_slice(mem, offset, 4)?.try_into().unwrap()))
345-
}
346-
347-
impl WasmCtx {
348-
pub fn get_mem(&self) -> wasmtime::Memory {
349-
self.mem.expect("Initialized memory")
350-
}
351-
352-
fn mem_env<'a>(ctx: impl Into<StoreContextMut<'a, Self>>) -> (&'a mut [u8], &'a mut Self) {
353-
let ctx = ctx.into();
354-
let mem = ctx.data().get_mem();
355-
mem.data_and_store_mut(ctx)
356-
}
357-
358-
pub fn bytes_sink_write(
359-
mut caller: Caller<'_, Self>,
360-
sink_handle: u32,
361-
buffer_ptr: u32,
362-
buffer_len_ptr: u32,
363-
) -> anyhow::Result<u32> {
364-
if sink_handle != 1 {
365-
return Ok(errno::NO_SUCH_BYTES.get().into());
224+
impl Language {
225+
fn format_files(&self, generated_files: BTreeSet<PathBuf>) -> anyhow::Result<()> {
226+
match self {
227+
Language::Rust => rustfmt(generated_files)?,
228+
Language::Csharp => dotnet_format(generated_files)?,
229+
Language::TypeScript => {
230+
// TODO: implement formatting.
231+
}
366232
}
367233

368-
let (mem, env) = Self::mem_env(&mut caller);
369-
370-
// Read `buffer_len`, i.e., the capacity of `buffer` pointed to by `buffer_ptr`.
371-
let buffer_len = read_u32(mem, buffer_len_ptr)?;
372-
// Write `buffer` to `sink`.
373-
let buffer = deref_slice(mem, buffer_ptr, buffer_len)?;
374-
env.sink.extend(buffer);
375-
376-
Ok(0)
234+
Ok(())
377235
}
378236
}

0 commit comments

Comments
 (0)