Repository: capnproto/capnproto-rust (language: rust)

Issue:
Serializing messages with no-alloc
Hello,

This is more a question than an issue.

I came across this crate recently and discovered that it has a no-alloc feature, which perfectly matches my environment. I've been able to implement a deserialization in my project, as it's done in example/wasm-hello-world/wasm-app/src/lib.rs, but I don't find a way to serialize a message.

Is it implemented for no-alloc? And if yes, would it be possible to have an example of use?

Thanks!


Discussion / hints:
You'll need to provide your own implementation of the `Allocator` trait:
https://github.com/capnproto/capnproto-rust/blob/20741d11f543d672f3a88d5633652f4af651b0ec/capnp/src/message.rs#L370-L404

One way to do it would be to have a wrapper for a single `&mut[u8]`. That would work simlarly to [`ScratchSpaceHeapAllocator`](https://github.com/capnproto/capnproto-rust/blob/20741d11f543d672f3a88d5633652f4af651b0ec/capnp/src/message.rs#L781-L786), except without the `HeapAllocator` for when the segment fills up. (You would need to panic when the segment fills.)
Thank you @dwrensha for the quick reply! I will have a look at this.
I opened #441, which adds `SingleSegmentAllocator`.
Hi @dwrensha,

First, many thanks for the quick new release!

Unfortunately I still have issues to compile in my environment (no-std, no-alloc) something similar as the test case you added. The allocate_zeroed_vec() method returns a Vec<Word> which is not available, but it looks quite easy to fix, by passing to capnp::message::SingleSegmentAllocator::new() a reference to an aligned u8 slice.

The other problem I have seems harder to solve:

`error[E0433]: failed to resolve: could not find `Builder` in `message``

Indeed, the struct Builder<A> is decorated with #[cfg(feature = "alloc")], so it is not available in my environment. We can't simply remove it since it depends on BuilderArenaImpl<A> which uses a Vec.

Oops!

We need a no-alloc version of `BuilderArenaImpl`. The straightforward way to achieve that would be to hard code it to allow only one segment in the no-alloc case.

Fix patch:
diff --git a/capnp/src/lib.rs b/capnp/src/lib.rs
index daafc23c1..845d0c3b3 100644
--- a/capnp/src/lib.rs
+++ b/capnp/src/lib.rs
@@ -623,28 +623,31 @@ impl ::std::error::Error for Error {
 
 /// Helper struct that allows `MessageBuilder::get_segments_for_output()` to avoid heap allocations
 /// in the single-segment case.
-#[cfg(feature = "alloc")]
 pub enum OutputSegments<'a> {
     SingleSegment([&'a [u8]; 1]),
+
+    #[cfg(feature = "alloc")]
     MultiSegment(Vec<&'a [u8]>),
 }
 
-#[cfg(feature = "alloc")]
 impl<'a> core::ops::Deref for OutputSegments<'a> {
     type Target = [&'a [u8]];
     fn deref(&self) -> &[&'a [u8]] {
         match self {
             OutputSegments::SingleSegment(s) => s,
+
+            #[cfg(feature = "alloc")]
             OutputSegments::MultiSegment(v) => v,
         }
     }
 }
 
-#[cfg(feature = "alloc")]
 impl<'s> message::ReaderSegments for OutputSegments<'s> {
     fn get_segment(&self, id: u32) -> Option<&[u8]> {
         match self {
             OutputSegments::SingleSegment(s) => s.get(id as usize).copied(),
+
+            #[cfg(feature = "alloc")]
             OutputSegments::MultiSegment(v) => v.get(id as usize).copied(),
         }
     }
diff --git a/capnp/src/message.rs b/capnp/src/message.rs
index 7a347aae6..6a95b2055 100644
--- a/capnp/src/message.rs
+++ b/capnp/src/message.rs
@@ -73,15 +73,12 @@ use alloc::vec::Vec;
 use core::convert::From;
 
 use crate::any_pointer;
-#[cfg(feature = "alloc")]
 use crate::private::arena::{BuilderArena, BuilderArenaImpl};
 use crate::private::arena::{ReaderArena, ReaderArenaImpl};
 use crate::private::layout;
 use crate::private::units::BYTES_PER_WORD;
-#[cfg(feature = "alloc")]
 use crate::traits::{FromPointerBuilder, SetPointerBuilder};
 use crate::traits::{FromPointerReader, Owned};
-#[cfg(feature = "alloc")]
 use crate::OutputSegments;
 use crate::Result;
 
@@ -344,7 +341,6 @@ where
     }
 }
 
-#[cfg(feature = "alloc")]
 impl<A, T> From<Builder<A>> for TypedReader<Builder<A>, T>
 where
     A: Allocator,
@@ -356,7 +352,6 @@ where
     }
 }
 
-#[cfg(feature = "alloc")]
 impl<A, T> From<TypedBuilder<T, A>> for TypedReader<Builder<A>, T>
 where
     A: Allocator,
@@ -404,7 +399,6 @@ pub unsafe trait Allocator {
 }
 
 /// A container used to build a message.
-#[cfg(feature = "alloc")]
 pub struct Builder<A>
 where
     A: Allocator,
@@ -412,10 +406,8 @@ where
     arena: BuilderArenaImpl<A>,
 }
 
-#[cfg(feature = "alloc")]
 unsafe impl<A> Send for Builder<A> where A: Send + Allocator {}
 
-#[cfg(feature = "alloc")]
 fn _assert_kinds() {
     fn _assert_send<T: Send>() {}
     fn _assert_reader<S: ReaderSegments + Send>() {
@@ -426,7 +418,6 @@ fn _assert_kinds() {
     }
 }
 
-#[cfg(feature = "alloc")]
 impl<A> Builder<A>
 where
     A: Allocator,
@@ -533,7 +524,6 @@ where
     }
 }
 
-#[cfg(feature = "alloc")]
 impl<A> ReaderSegments for Builder<A>
 where
     A: Allocator,
@@ -563,6 +553,19 @@ where
     message: Builder<A>,
 }
 
+// Defined separately because the A=HeapAllocator default type
+// argument is not allowed in no-alloc mode.
+// TODO(apibump): remove the A=HeapAllocator thing above?
+#[cfg(not(feature = "alloc"))]
+pub struct TypedBuilder<T, A>
+where
+    T: Owned,
+    A: Allocator,
+{
+    marker: ::core::marker::PhantomData<T>,
+    message: Builder<A>,
+}
+
 #[cfg(feature = "alloc")]
 impl<T> TypedBuilder<T, HeapAllocator>
 where
@@ -573,7 +576,6 @@ where
     }
 }
 
-#[cfg(feature = "alloc")]
 impl<T, A> TypedBuilder<T, A>
 where
     T: Owned,
@@ -623,7 +625,6 @@ where
     }
 }
 
-#[cfg(feature = "alloc")]
 impl<T, A> From<Builder<A>> for TypedBuilder<T, A>
 where
     T: Owned,
@@ -951,7 +952,6 @@ unsafe impl<'a> Allocator for SingleSegmentAllocator<'a> {
     }
 }
 
-#[cfg(feature = "alloc")]
 unsafe impl<'a, A> Allocator for &'a mut A
 where
     A: Allocator,
diff --git a/capnp/src/private/arena.rs b/capnp/src/private/arena.rs
index 46626c1f0..065cb2f3d 100644
--- a/capnp/src/private/arena.rs
+++ b/capnp/src/private/arena.rs
@@ -20,17 +20,14 @@
 
 #[cfg(feature = "alloc")]
 use alloc::vec::Vec;
-#[cfg(feature = "alloc")]
 use core::slice;
 use core::u64;
 
 use crate::message;
-#[cfg(feature = "alloc")]
 use crate::message::Allocator;
 use crate::message::ReaderSegments;
 use crate::private::read_limiter::ReadLimiter;
 use crate::private::units::*;
-#[cfg(feature = "alloc")]
 use crate::OutputSegments;
 use crate::{Error, ErrorKind, Result};
 
@@ -163,7 +160,6 @@ pub trait BuilderArena: ReaderArena {
 }
 
 /// A wrapper around a memory segment used in building a message.
-#[cfg(feature = "alloc")]
 struct BuilderSegment {
     /// Pointer to the start of the segment.
     ptr: *mut u8,
@@ -177,17 +173,64 @@ struct BuilderSegment {
 }
 
 #[cfg(feature = "alloc")]
+type BuilderSegmentArray = Vec<BuilderSegment>;
+
+#[cfg(not(feature = "alloc"))]
+#[derive(Default)]
+struct BuilderSegmentArray {
+    // In the no-alloc case, we only allow a single segment.
+    segment: Option<BuilderSegment>,
+}
+
+#[cfg(not(feature = "alloc"))]
+impl BuilderSegmentArray {
+    fn len(&self) -> usize {
+        match self.segment {
+            Some(_) => 1,
+            None => 0,
+        }
+    }
+
+    fn push(&mut self, segment: BuilderSegment) {
+        if self.segment.is_some() {
+            panic!("multiple segments are not supported in no-alloc mode")
+        }
+        self.segment = Some(segment);
+    }
+}
+
+#[cfg(not(feature = "alloc"))]
+impl core::ops::Index<usize> for BuilderSegmentArray {
+    type Output = BuilderSegment;
+
+    fn index(&self, index: usize) -> &Self::Output {
+        assert_eq!(index, 0);
+        match &self.segment {
+            Some(s) => s,
+            None => panic!("no segment"),
+        }
+    }
+}
+
+#[cfg(not(feature = "alloc"))]
+impl core::ops::IndexMut<usize> for BuilderSegmentArray {
+    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
+        assert_eq!(index, 0);
+        match &mut self.segment {
+            Some(s) => s,
+            None => panic!("no segment"),
+        }
+    }
+}
+
 pub struct BuilderArenaImplInner<A>
 where
     A: Allocator,
 {
     allocator: Option<A>, // None if has already be deallocated.
-
-    // TODO(perf): Try using smallvec to avoid heap allocations in the single-segment case?
-    segments: Vec<BuilderSegment>,
+    segments: BuilderSegmentArray,
 }
 
-#[cfg(feature = "alloc")]
 pub struct BuilderArenaImpl<A>
 where
     A: Allocator,
@@ -195,7 +238,6 @@ where
     inner: BuilderArenaImplInner<A>,
 }
 
-#[cfg(feature = "alloc")]
 impl<A> BuilderArenaImpl<A>
 where
     A: Allocator,
@@ -204,7 +246,7 @@ where
         Self {
             inner: BuilderArenaImplInner {
                 allocator: Some(allocator),
-                segments: Vec::new(),
+                segments: Default::default(),
             },
         }
     }
@@ -227,18 +269,25 @@ where
             };
             OutputSegments::SingleSegment([slice])
         } else {
-            let mut v = Vec::with_capacity(reff.segments.len());
-            for seg in &reff.segments {
-                // See safety argument in above branch.
-                let slice = unsafe {
-                    slice::from_raw_parts(
-                        seg.ptr as *const _,
-                        seg.allocated as usize * BYTES_PER_WORD,
-                    )
-                };
-                v.push(slice);
+            #[cfg(feature = "alloc")]
+            {
+                let mut v = Vec::with_capacity(reff.segments.len());
+                for seg in &reff.segments {
+                    // See safety argument in above branch.
+                    let slice = unsafe {
+                        slice::from_raw_parts(
+                            seg.ptr as *const _,
+                            seg.allocated as usize * BYTES_PER_WORD,
+                        )
+                    };
+                    v.push(slice);
+                }
+                OutputSegments::MultiSegment(v)
+            }
+            #[cfg(not(feature = "alloc"))]
+            {
+                panic!("invalid number of segments: {}", reff.segments.len());
             }
-            OutputSegments::MultiSegment(v)
         }
     }
 
@@ -258,7 +307,6 @@ where
     }
 }
 
-#[cfg(feature = "alloc")]
 impl<A> ReaderArena for BuilderArenaImpl<A>
 where
     A: Allocator,
@@ -290,7 +338,6 @@ where
     }
 }
 
-#[cfg(feature = "alloc")]
 impl<A> BuilderArenaImplInner<A>
 where
     A: Allocator,
@@ -341,11 +388,19 @@ where
 
     fn deallocate_all(&mut self) {
         if let Some(a) = &mut self.allocator {
+            #[cfg(feature = "alloc")]
             for seg in &self.segments {
                 unsafe {
                     a.deallocate_segment(seg.ptr, seg.capacity, seg.allocated);
                 }
             }
+
+            #[cfg(not(feature = "alloc"))]
+            if let Some(seg) = &self.segments.segment {
+                unsafe {
+                    a.deallocate_segment(seg.ptr, seg.capacity, seg.allocated);
+                }
+            }
         }
     }
 
@@ -355,7 +410,6 @@ where
     }
 }
 
-#[cfg(feature = "alloc")]
 impl<A> BuilderArena for BuilderArenaImpl<A>
 where
     A: Allocator,
@@ -377,7 +431,6 @@ where
     }
 }
 
-#[cfg(feature = "alloc")]
 impl<A> Drop for BuilderArenaImplInner<A>
 where
     A: Allocator,


Test patch:
diff --git a/capnp/tests/single_segment_allocator.rs b/capnp/tests/single_segment_allocator.rs
index d5fab0c84..affb73bc5 100644
--- a/capnp/tests/single_segment_allocator.rs
+++ b/capnp/tests/single_segment_allocator.rs
@@ -1,10 +1,8 @@
-#![cfg(feature = "alloc")]
-
 use capnp::message;
 
 #[test]
 pub fn single_segment_allocator() {
-    let mut buffer = capnp::Word::allocate_zeroed_vec(200);
+    let mut buffer = [capnp::word(0, 0, 0, 0, 0, 0, 0, 0); 200];
     let allocator =
         message::SingleSegmentAllocator::new(capnp::Word::words_to_bytes_mut(&mut buffer[..]));
     let mut msg = message::Builder::new(allocator);