Files
roto/TASKS.md
T
2026-06-30 00:49:03 -04:00

4.8 KiB
Raw Blame History

Task: Verify Generated Code Works in no_std / no_alloc

Goal: Ensure all generated protobuf code compiles and runs in no_std + no_alloc environments. Features requiring alloc must be gated behind feature flags in the proto generator's output.


Findings

Already clean (no_std + no_alloc):

  • All reader structs (Message<'a>) — zero-copy accessors over &[u8]
  • All field accessors, enums, builders (MessageBuilder<'b>), typed iterators
  • Core runtime: ProtoAccessor, ProtoBuilder, RevBuilder, read_varint, write_varint, all iterators

Needs alloc feature gate:

  • RotoMessage trait — uses bytes::Bytes (has Arc internally)
  • RotoOwned trait — same issue
  • OwnedMessage structs generated by codegen
  • RevBuilder::with() — uses Vec<_> to collect raw fields
  • Bytes, BytesMut imports in DATA_IMPORTS

Needs std feature gate:

  • All service code (SERVICE_IMPORTS) — uses std::sync::Arc, std::pin::Pin, String, etc.
  • std::error::Error for RotoError (already gated)

Plan

Phase 1: Runtime (runtime/src/lib.rs, runtime/Cargo.toml)

  1. Gate RotoMessage and RotoOwned behind #[cfg(feature = "alloc")]
  2. Gate BufMutBuilder behind #[cfg(feature = "alloc")] — only useful with BytesMut, which requires alloc
  3. Gate use bytes::BufMut — only import it conditionally
  4. Wire up bytes features in Cargo.toml:
    bytes = { version = "1.7", default-features = false }
    # ...
    [features]
    default = ["std", "alloc"]
    std = ["bytes/std"]
    alloc = ["bytes"]
    

Phase 2: Codegen — DATA_IMPORTS & Generated Data Code (codegen/src/generator/mod.rs)

  1. Split DATA_IMPORTS into unconditional + gated sections:

    // Always emitted
    use roto_runtime::{ProtoAccessor, ProtoBuilder, RevBuilder, Result, RotoError, read_varint, RepeatedFieldIterator};
    use core::str;
    use bytes::{Buf, BufMut};
    
    // Only emitted when alloc feature is enabled
    #[cfg(feature = "alloc")]
    use bytes::{Bytes, BytesMut};
    #[cfg(feature = "alloc")]
    use roto_runtime::RotoMessage;
    
  2. Gate OwnedMessage structs + RotoMessage impls with #[cfg(feature = "alloc")]

  3. Fix RevBuilder::with() — replace let fields: Vec<_> = ... .collect() with a forward iteration that doesn't allocate. The reverse order doesn't matter for protobuf encoding (fields are independent):

    // Before: let fields: Vec<_> = msg.accessor.raw_fields().collect();
    //         for (raw, _) in fields.iter().rev() { ... }
    // After:  for (raw, _) in msg.accessor.raw_fields() { ... }
    

    Alternatively, gate the with() method itself behind #[cfg(feature = "alloc")].

Phase 3: Codegen — Service Code (codegen/src/generator/mod.rs)

  1. Gate SERVICE_IMPORTS with #[cfg(feature = "std")]
  2. Gate all service traits, server impls, and *Server structs with #[cfg(feature = "std")]

Phase 4: Generated Consumer Setup

  1. Emit a Cargo.toml (or instruct consumers) with the right feature setup:
    [features]
    default = ["std"]
    std = ["roto-runtime/std", "alloc"]
    alloc = ["roto-runtime/alloc", "bytes"]
    

Phase 5: Validation

  1. Update examples/no_std_test to build with --no-default-features and verify compilation
  2. Run existing tests with default features to ensure nothing breaks
  3. Add a compile-test that verifies --no-default-features builds succeed for generated code

Dependency Graph

no_std + no_alloc  →  readers, enums, builders, iterators
         ↓ alloc   →  + OwnedMessage, RotoMessage trait
         ↓ std     →  + service code, std::error::Error

Risk Assessment

Risk Mitigation
Breaking existing consumers who use OwnedMessage OwnedMessage was already behind a manual #[cfg] in the no_std_test example — consumers aren't relying on it unconditionally
BufMutBuilder removal in no-alloc mode It's only useful with BytesMut anyway, which requires alloc
RevBuilder::with() change Forward iteration is semantically equivalent for protobuf encoding

Files to Modify

File Changes
runtime/src/lib.rs Gate RotoMessage, RotoOwned, BufMutBuilder, use bytes::BufMut
runtime/Cargo.toml Wire allocbytes dependency, add bytes/std to std feature
codegen/src/generator/mod.rs Split DATA_IMPORTS, gate OwnedMessage, gate service code, fix RevBuilder::with()
examples/no_std_test/ Update to use new feature structure

Execution Order

  1. Phase 1 (runtime) — foundation
  2. Phase 2 (codegen data imports) — depends on runtime
  3. Phase 3 (codegen services) — independent
  4. Phase 45 (consumer setup + validation) — depends on all above