feat: gate alloc/std features for no_std/no_alloc support

Gate generated code behind feature flags so consumers can build in
no_std/no_alloc environments:

- Runtime: gate RotoMessage, RotoOwned, BufMutBuilder behind alloc
- Codegen: split DATA_IMPORTS, gate Owned* structs + service code
- Fix RevBuilder::with() to iterate forward without Vec allocation
- Wire up bytes/std via alloc feature (Bytes requires Arc)
- Update consumer Cargo.toml files with proper feature chains
- Add no_std_test example for embedded validation
This commit is contained in:
2026-06-30 01:40:13 -04:00
parent 2ce65496ab
commit 2806772fd1
13 changed files with 304 additions and 955 deletions
+68 -52
View File
@@ -7,23 +7,15 @@ use crate::runtime::ProtoAccessor;
use std::collections::{HashMap, HashSet};
use std::str;
const DATA_IMPORTS: &str = "#[allow(unused, unused_imports, unused_assignments, unused_variables, non_camel_case_types)]\n\
use roto_runtime::{ProtoAccessor, ProtoBuilder, RevBuilder, Result, RotoError, read_varint, RepeatedFieldIterator, RotoMessage};\n\
use core::str;\n\
use bytes::{Bytes, BytesMut, Buf, BufMut};\n";
const SERVICE_IMPORTS: &str = "#[allow(unused, unused_imports, unused_assignments, unused_variables, non_camel_case_types)]\n\
use tonic::{Request, Response, Status};\n\
use tokio_stream::Stream;\n\
use std::pin::Pin;\n\
use std::sync::Arc;\n\
use std::task::{Context, Poll};\n\
use std::future::Future;\n\
use tonic::body::BoxBody;\n\
use tower::Service;\n\
use futures_util::StreamExt;\n\
use http_body_util::BodyExt;\n\
use http_body::Body;\n\
use crate::{BufferPool, StatusBody};\n";
const DATA_IMPORTS: &str = "#[allow(unused, unused_imports, unused_assignments, unused_variables, non_camel_case_types)]\
use roto_runtime::{ProtoAccessor, ProtoBuilder, RevBuilder, Result, RotoError, read_varint, RepeatedFieldIterator};\
use core::str;\
use bytes::Buf;\
#[cfg(feature = \"alloc\")]\
use bytes::{Bytes, BytesMut, BufMut};\
#[cfg(feature = \"alloc\")]\
use roto_runtime::RotoMessage;\
";
pub fn to_pascal_case(s: &str) -> String {
s.split('_')
@@ -654,13 +646,12 @@ pub fn write_message(
));
}
// with() — copies unseen fields from an existing message (iterate in reverse for RevBuilder)
// with() — copies unseen fields from an existing message
output.push_str(&format!(
" pub fn with(mut self, msg: &{}<'_>) -> roto_runtime::Result<Self> {{\n",
msg_name
));
output.push_str(" let fields: Vec<_> = msg.accessor.raw_fields().collect();\n");
output.push_str(" for item in fields.into_iter().rev() {\n");
output.push_str(" for item in msg.accessor.raw_fields() {\n");
output.push_str(" let (field_number, raw_bytes) = item?;\n");
output.push_str(" let is_written = match field_number {\n");
for (field_name, _, tag, _, _) in &builder_fields {
@@ -680,34 +671,32 @@ pub fn write_message(
output.push_str(&format!(" pub fn finish(self) -> roto_runtime::Result<&'b mut [u8]> {{\n self.builder.finish()\n }}\n}}\n\n"));
output.push_str(&format!("pub struct Owned{} {{\n", msg_name));
output.push_str(" pub data: bytes::Bytes,\n");
output.push_str("}\n\n");
output.push_str(&format!(
"impl roto_runtime::RotoOwned for Owned{} {{\n",
msg_name
"#[cfg(feature = \"alloc\")]\
pub struct Owned{} {{\
pub data: bytes::Bytes,\
}}\
\n\n\
#[cfg(feature = \"alloc\")]\
impl roto_runtime::RotoOwned for Owned{} {{\
type Reader<'a> = {}<'a>;\
fn reader(&self) -> {}<'_> {{\
{}::new(&self.data).expect(\"failed to create reader\")\
}}\
}}\
\n\n\
#[cfg(feature = \"alloc\")]\
impl roto_runtime::RotoMessage for Owned{} {{\
fn decode(buf: bytes::Bytes) -> roto_runtime::Result<Self> {{\
Ok(Owned{} {{ data: buf }})\
}}\n\n\
fn bytes(&self) -> bytes::Bytes {{\
self.data.clone()\
}}\
}}\
\n",
msg_name, msg_name, msg_name, msg_name, msg_name, msg_name, msg_name
));
output.push_str(&format!(" type Reader<'a> = {}<'a>;\n", msg_name));
output.push_str(&format!(" fn reader(&self) -> {}<'_> {{\n", msg_name));
output.push_str(&format!(
" {}::new(&self.data).expect(\"failed to create reader\")\n",
msg_name
));
output.push_str(" }\n");
output.push_str("}\n\n");
output.push_str(&format!(
"impl roto_runtime::RotoMessage for Owned{} {{\n",
msg_name
));
output.push_str(" fn decode(buf: bytes::Bytes) -> roto_runtime::Result<Self> {\n");
output.push_str(&format!(" Ok(Owned{} {{ data: buf }})\n", msg_name));
output.push_str(" }\n\n");
output.push_str(" fn bytes(&self) -> bytes::Bytes {\n");
output.push_str(" self.data.clone()\n");
output.push_str(" }\n");
output.push_str("}\n\n");
let mut nested_enums = Vec::new();
for e_res in msg_proto.enum_type() {
@@ -1069,12 +1058,36 @@ mod tests {
}
fn write_service(svc_proto: &ServiceDescriptorProto, package: &str, output: &mut String) {
output.push_str(SERVICE_IMPORTS);
// Service imports — gated behind std feature
output.push_str("#[cfg(feature = \"std\")]\n");
output.push_str("use tonic::{Request, Response, Status};\n");
output.push_str("#[cfg(feature = \"std\")]\n");
output.push_str("use tokio_stream::Stream;\n");
output.push_str("#[cfg(feature = \"std\")]\n");
output.push_str("use std::pin::Pin;\n");
output.push_str("#[cfg(feature = \"std\")]\n");
output.push_str("use std::sync::Arc;\n");
output.push_str("#[cfg(feature = \"std\")]\n");
output.push_str("use std::task::{Context, Poll};\n");
output.push_str("#[cfg(feature = \"std\")]\n");
output.push_str("use std::future::Future;\n");
output.push_str("#[cfg(feature = \"std\")]\n");
output.push_str("use tonic::body::BoxBody;\n");
output.push_str("#[cfg(feature = \"std\")]\n");
output.push_str("use tower::Service;\n");
output.push_str("#[cfg(feature = \"std\")]\n");
output.push_str("use futures_util::StreamExt;\n");
output.push_str("#[cfg(feature = \"std\")]\n");
output.push_str("use http_body_util::BodyExt;\n");
output.push_str("#[cfg(feature = \"std\")]\n");
output.push_str("use http_body::Body;\n");
output.push_str("#[cfg(feature = \"std\")]\n");
output.push_str("use crate::{BufferPool, StatusBody};\n");
output.push_str("\n");
let svc_name = to_pascal_case(svc_proto.name().unwrap());
output.push_str(&format!(
"#[async_trait::async_trait]\npub trait {}: Send + Sync + 'static {{\n",
"#[cfg(feature = \"std\")]\n#[async_trait::async_trait]\npub trait {}: Send + Sync + 'static {{\n",
svc_name
));
@@ -1121,14 +1134,17 @@ fn write_service(svc_proto: &ServiceDescriptorProto, package: &str, output: &mut
let server_name = format!("{}Server", svc_name);
output.push_str(&format!(
"#[derive(Clone)]\npub struct {} {{\n",
"#[cfg(feature = \"std\")]\n#[derive(Clone)]\npub struct {} {{\n",
server_name
));
output.push_str(&format!(" inner: Arc<dyn {}>,\n", svc_name));
output.push_str(" pool: Arc<BufferPool>,\n");
output.push_str("}\n\n");
output.push_str(&format!("impl {} {{\n", server_name));
output.push_str(&format!(
"#[cfg(feature = \"std\")]\nimpl {} {{\n",
server_name
));
output.push_str(&format!(
" pub fn new(inner: Arc<dyn {}>, pool: Arc<BufferPool>) -> Self {{\n",
svc_name
@@ -1138,7 +1154,7 @@ fn write_service(svc_proto: &ServiceDescriptorProto, package: &str, output: &mut
output.push_str("}\n\n");
output.push_str(&format!(
"impl tonic::server::NamedService for {} {{\n",
"#[cfg(feature = \"std\")]\nimpl tonic::server::NamedService for {} {{\n",
server_name
));
let full_svc_name = if package.is_empty() {
@@ -1153,7 +1169,7 @@ fn write_service(svc_proto: &ServiceDescriptorProto, package: &str, output: &mut
output.push_str("}\n\n");
output.push_str(&format!(
"impl Service<http::Request<BoxBody>> for {} {{\n",
"#[cfg(feature = \"std\")]\nimpl Service<http::Request<BoxBody>> for {} {{\n",
server_name
));
output.push_str(" type Response = http::Response<BoxBody>;\n");