Skip to content
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
84 changes: 49 additions & 35 deletions Cargo.lock

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

62 changes: 48 additions & 14 deletions scripts/gen-rust-protobufs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,57 +16,91 @@ OUTPUT_FILE="$ROOT_DIR/src/simlin-engine/src/project_io.gen.rs"
TOOLCHAIN=$(grep '^channel' "$ROOT_DIR/rust-toolchain.toml" | cut -d '"' -f 2)
export RUSTUP_TOOLCHAIN="$TOOLCHAIN"

# The generator must be exactly the version of the `buffa` runtime the
# workspace links: generated code calls runtime internals (`__private` helpers,
# trait methods) that are not covered by semver, so a newer buffa-build than
# the locked runtime can emit code the runtime cannot compile. Cargo.lock is
# the source of truth for that runtime version.
BUFFA_VERSION=$(awk '/^name = "buffa"$/ { getline; gsub(/"/, "", $3); print $3; exit }' "$ROOT_DIR/Cargo.lock")
if [ -z "$BUFFA_VERSION" ]; then
echo "error: no buffa package in $ROOT_DIR/Cargo.lock" >&2
exit 1
fi

# Compute SHA256 hash of the proto file
PROTO_HASH=$(sha256sum "$PROTO_FILE" | cut -d ' ' -f 1)

# Create a temporary directory for the prost-build project
# Create a temporary directory for the buffa-build project
TEMP_DIR=$(mktemp -d)
trap "rm -rf $TEMP_DIR" EXIT

# Create a minimal Cargo project that uses prost-build
mkdir -p "$TEMP_DIR/src"
cat > "$TEMP_DIR/Cargo.toml" << 'EOF'
# Create a minimal Cargo project that uses buffa-build
mkdir -p "$TEMP_DIR/src" "$TEMP_DIR/out"
cat > "$TEMP_DIR/Cargo.toml" << EOF
[package]
name = "proto-gen"
version = "0.1.0"
edition = "2024"

[build-dependencies]
prost-build = "0.14"
buffa-build = "=$BUFFA_VERSION"
EOF

cat > "$TEMP_DIR/src/lib.rs" << 'EOF'
// placeholder
EOF

# Codegen options, each chosen for how the engine uses these types (as a
# serialization format converted to and from `datamodel` in serde.rs):
# - no views: nothing decodes zero-copy, and views roughly double the output.
# - one file per package: a single checked-in file carrying the hash header
# below, with the oneof enums re-exported where the message modules name them
# (`variable::V`, `view_element::Element`, ...).
# - unknown fields dropped: the engine never re-emits a message it decoded, so
# preserving them buys nothing, and without the hidden bookkeeping field the
# messages stay constructible with plain struct literals.
# - no `with_*` setters: unused.
# - idiomatic field names: the proto has camelCase fields (`viewBox`), which
# read as snake_case Rust fields (`view_box`) without renaming anything on
# the wire.
# - oneof variants stored inline: no oneof here is recursive, so the default
# per-variant `Box` would only add an allocation per variant.
cat > "$TEMP_DIR/build.rs" << EOF
fn main() {
let mut config = prost_build::Config::new();
config.protoc_arg("--experimental_allow_proto3_optional");
config.out_dir("$TEMP_DIR");
config.compile_protos(&["$PROTO_FILE"], &["$(dirname "$PROTO_FILE")"]).unwrap();
buffa_build::Config::new()
.files(&["$PROTO_FILE"])
.includes(&["$(dirname "$PROTO_FILE")"])
.out_dir("$TEMP_DIR/out")
.generate_views(false)
.file_per_package(true)
.preserve_unknown_fields(false)
.generate_with_setters(false)
.idiomatic_field_names(true)
.unbox_oneof()
.compile()
.unwrap();
}
EOF

# Run the build to generate the protobuf code. prost-build shells out to
# Run the build to generate the protobuf code. buffa-build shells out to
# `protoc`; surface its stderr on failure (a missing protoc is the common case)
# instead of exiting silently with nothing written.
cd "$TEMP_DIR"
if ! cargo build 2>"$TEMP_DIR/build.log"; then
cat "$TEMP_DIR/build.log" >&2
echo "error: prost-build failed; see output above" >&2
echo "error: buffa-build failed; see output above" >&2
exit 1
fi

# Prepend the header to the generated file
{
echo "// @generated by prost-build from project_io.proto"
echo "// @generated by buffa-build from project_io.proto"
echo "// DO NOT EDIT - regenerate with: pnpm build:gen-protobufs"
echo "//"
echo "// Proto file SHA256: $PROTO_HASH"
echo "// prost-build version: 0.14"
echo "// buffa-build version: $BUFFA_VERSION"
echo ""
cat "$TEMP_DIR/project_io.rs"
cat "$TEMP_DIR/out/project_io.rs"
} > "$OUTPUT_FILE"

echo "Generated $OUTPUT_FILE"
1 change: 0 additions & 1 deletion src/libsimlin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ mimalloc = ["dep:mimalloc"]

[dependencies]
simlin-engine = { version = "0.1", path = "../simlin-engine", default-features = false }
prost = "0.14"
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
anyhow = "1.0"
Expand Down
6 changes: 2 additions & 4 deletions src/libsimlin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ use std::sync::atomic::{AtomicBool, AtomicUsize};
use std::sync::{Arc, Mutex, MutexGuard};

#[cfg(test)]
use prost::Message;
use simlin_engine::buffa::Message;
#[cfg(test)]
use simlin_engine::serde as engine_serde;
#[cfg(test)]
Expand Down Expand Up @@ -989,9 +989,7 @@ mod tests {
use engine::test_common::TestProject;

fn open_project_from_datamodel(project: &engine::datamodel::Project) -> *mut SimlinProject {
let pb = engine_serde::serialize(project).unwrap();
let mut buf = Vec::new();
pb.encode(&mut buf).unwrap();
let buf = engine_serde::serialize(project).unwrap().encode_to_vec();
unsafe {
let mut err: *mut SimlinError = ptr::null_mut();
let proj = simlin_project_open_protobuf(
Expand Down
16 changes: 11 additions & 5 deletions src/libsimlin/src/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
//! reference counting, querying models, and checking simulatability.

use anyhow::{anyhow, Result};
use prost::Message;
use simlin_engine::buffa::Message;
use simlin_engine::{self as engine, serde as engine_serde};
use std::ffi::{CStr, CString};
use std::io::BufReader;
Expand Down Expand Up @@ -54,10 +54,16 @@ pub unsafe extern "C" fn simlin_project_open_protobuf(
}

let slice = unsafe { std::slice::from_raw_parts(data, len) };
let pb_project = engine::project_io::Project::decode(slice).map_err(|decode_err| {
FfiError::new(SimlinErrorCode::ProtobufDecode)
.with_message(format!("failed to decode project protobuf: {decode_err}"))
})?;
// buffa's default decode limits apply, including its 32 MiB budget on
// the memory repeated fields may materialize. That budget is what keeps a
// crafted payload from amplifying into gigabytes, and real projects sit
// far below it: C-LEARN, the largest model in the test corpus, needs
// under a twentieth of it.
let pb_project =
engine::project_io::Project::decode_from_slice(slice).map_err(|decode_err| {
FfiError::new(SimlinErrorCode::ProtobufDecode)
.with_message(format!("failed to decode project protobuf: {decode_err}"))
})?;

let datamodel_project: engine::datamodel::Project = engine_serde::deserialize(pb_project);
let db = new_synced_db(&datamodel_project);
Expand Down
25 changes: 15 additions & 10 deletions src/libsimlin/src/serialization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
//! systems, SVG, PNG, and scene formats. The memory for the output buffers is allocated via
//! `simlin_malloc` so that callers free it with `simlin_free`.

use prost::Message;
use simlin_engine::buffa::Message;
use simlin_engine::{self as engine, serde as engine_serde};
use std::ffi::CStr;
use std::os::raw::c_char;
Expand Down Expand Up @@ -79,15 +79,20 @@ pub unsafe extern "C" fn simlin_project_serialize_protobuf(
}
};

let mut bytes = Vec::new();
if pb_project.encode(&mut bytes).is_err() {
store_error(
out_error,
SimlinError::new(SimlinErrorCode::ProtobufDecode)
.with_message("failed to encode project protobuf"),
);
return;
}
// The fallible encode: the only failure is a project past protobuf's 2 GiB
// message limit, and the panicking `encode_to_vec` would abort the process
// there (release builds are `panic = abort`) instead of reporting it.
let bytes = match pb_project.try_encode_to_vec() {
Ok(bytes) => bytes,
Err(err) => {
store_error(
out_error,
SimlinError::new(SimlinErrorCode::ProtobufDecode)
.with_message(format!("failed to encode project protobuf: {err}")),
);
return;
}
};

let len = bytes.len();
let buf = simlin_malloc(len);
Expand Down
Loading
Loading