From e9f2c5189ee38a75d2b14ab9b29e6e09806d715e Mon Sep 17 00:00:00 2001 From: Chris Beck Date: Mon, 12 Aug 2019 17:22:22 -0700 Subject: [PATCH] Make prost a `no_std` compatible library, and prost-build able to generate `no_std` code The alternative is to get collections types from `core` and `alloc`. In the `no_std` mode in `prost_build`, we force it to always use BTreeMap since HashMap was not stabilized in `alloc::collections` library. The functionality is identical and the only incompatibilities in the interface are that we cannot use `std::error::Error` or `std::io::Error` in `no_std` mode because these types have not been moved to `alloc` and there is no alternative. --- Cargo.toml | 6 +++++- prost-build/src/code_generator.rs | 20 ++++++++++-------- prost-build/src/lib.rs | 31 ++++++++++++++++++++++++++++ src/encoding.rs | 18 +++++++++------- src/error.rs | 15 +++++++++++++- src/lib.rs | 2 ++ src/message.rs | 4 ++-- tests-alloc/Cargo.toml | 34 +++++++++++++++++++++++++++++++ tests/src/build.rs | 13 ++++++++++-- 9 files changed, 122 insertions(+), 21 deletions(-) create mode 100644 tests-alloc/Cargo.toml diff --git a/Cargo.toml b/Cargo.toml index 5521708b7..9bbf3cf57 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ members = [ "protobuf", "tests", "tests-2015", + "tests-alloc", ] exclude = [ # The fuzz crate can't be compiled or tested without the 'cargo fuzz' command, @@ -33,8 +34,11 @@ exclude = [ ] [features] -default = ["prost-derive"] +default = ["prost-derive", "std"] no-recursion-limit = [] +std = [] # When disabled, we attempt to provide no_std support in prost + # Config::use_alloc_collections() should be set when using prost_build in your build.rs + # so that generated files will not have std:: either, and will use alloc crate instead [dependencies] bytes = "0.4.7" diff --git a/prost-build/src/code_generator.rs b/prost-build/src/code_generator.rs index 593bba807..a20114b02 100644 --- a/prost-build/src/code_generator.rs +++ b/prost-build/src/code_generator.rs @@ -18,6 +18,7 @@ use crate::extern_paths::ExternPaths; use crate::ident::{match_ident, to_snake, to_upper_camel}; use crate::message_graph::MessageGraph; use crate::Config; +use crate::CollectionsLib; #[derive(PartialEq)] enum Syntax { @@ -366,12 +367,14 @@ impl<'a> CodeGenerator<'a> { self.buf.push_str(&to_snake(field.name())); self.buf.push_str(": "); if repeated { - self.buf.push_str("::std::vec::Vec<"); + self.buf.push_str(self.config.collections_lib.to_str()); + self.buf.push_str("::vec::Vec<"); } else if optional { - self.buf.push_str("::std::option::Option<"); + self.buf.push_str("::core::option::Option<"); } if boxed { - self.buf.push_str("::std::boxed::Box<"); + self.buf.push_str(self.config.collections_lib.to_str()); + self.buf.push_str("::boxed::Box<"); } self.buf.push_str(&ty); if boxed { @@ -403,7 +406,7 @@ impl<'a> CodeGenerator<'a> { self.append_doc(); self.push_indent(); - let btree_map = self + let btree_map = (self.config.collections_lib != CollectionsLib::Std) || self .config .btree_map .iter() @@ -426,8 +429,9 @@ impl<'a> CodeGenerator<'a> { self.append_field_attributes(msg_name, field.name()); self.push_indent(); self.buf.push_str(&format!( - "pub {}: ::std::collections::{}<{}, {}>,\n", + "pub {}: {}::collections::{}<{}, {}>,\n", to_snake(field.name()), + self.config.collections_lib.to_str(), rust_ty, key_ty, value_ty @@ -459,7 +463,7 @@ impl<'a> CodeGenerator<'a> { self.append_field_attributes(fq_message_name, oneof.name()); self.push_indent(); self.buf.push_str(&format!( - "pub {}: ::std::option::Option<{}>,\n", + "pub {}: ::core::option::Option<{}>,\n", to_snake(oneof.name()), name )); @@ -713,8 +717,8 @@ impl<'a> CodeGenerator<'a> { Type::Int32 | Type::Sfixed32 | Type::Sint32 | Type::Enum => String::from("i32"), Type::Int64 | Type::Sfixed64 | Type::Sint64 => String::from("i64"), Type::Bool => String::from("bool"), - Type::String => String::from("std::string::String"), - Type::Bytes => String::from("std::vec::Vec"), + Type::String => [self.config.collections_lib.to_str(), "::string::String"].concat(), + Type::Bytes => [self.config.collections_lib.to_str(), "::vec::Vec"].concat(), Type::Group | Type::Message => self.resolve_ident(field.type_name()), } } diff --git a/prost-build/src/lib.rs b/prost-build/src/lib.rs index fb5e68d17..bcb7cd540 100644 --- a/prost-build/src/lib.rs +++ b/prost-build/src/lib.rs @@ -165,11 +165,32 @@ pub trait ServiceGenerator { fn finalize(&mut self, _buf: &mut String) {} } +/// Configuration enum for whether to use `std` prefixes or `alloc` prefixes in generated code +/// +/// This option also forces Btree everywhere, overriding the BtreeMap options, +/// since HashMap is not in alloc::collections, only std (it requires randomness) +/// +#[derive(PartialEq)] +pub enum CollectionsLib { + Std, + Alloc, +} + +impl CollectionsLib { + pub fn to_str(&self) -> &'static str { + match self { + CollectionsLib::Std => { "::std" }, + CollectionsLib::Alloc => { "::alloc" }, + } + } +} + /// Configuration options for Protobuf code generation. /// /// This configuration builder can be used to set non-default code generation options. pub struct Config { service_generator: Option>, + collections_lib: CollectionsLib, btree_map: Vec, type_attributes: Vec<(String, String)>, field_attributes: Vec<(String, String)>, @@ -460,6 +481,15 @@ impl Config { self } + /// Configure the code generator to use the `::alloc` namespace rather than `::std`, and Btree everywhere + /// rather than `std`. + /// + /// This allows generated code to be used in a `#![no_std]` crate + pub fn use_alloc_collections_lib(&mut self) -> &mut Self { + self.collections_lib = CollectionsLib::Alloc; + self + } + /// Configures the output directory where generated Rust files will be written. /// /// If unset, defaults to the `OUT_DIR` environment variable. `OUT_DIR` is set by Cargo when @@ -581,6 +611,7 @@ impl default::Default for Config { fn default() -> Config { Config { service_generator: None, + collections_lib: CollectionsLib::Std, btree_map: Vec::new(), type_attributes: Vec::new(), field_attributes: Vec::new(), diff --git a/src/encoding.rs b/src/encoding.rs index faa52019e..5785b2901 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -2,10 +2,10 @@ //! //! Meant to be used only from `Message` implementations. -use std::cmp::min; -use std::mem; -use std::u32; -use std::usize; +use core::cmp::min; +use core::mem; +use core::u32; +use core::usize; use ::bytes::{Buf, BufMut}; @@ -1040,10 +1040,8 @@ pub mod group { /// generic over `HashMap` and `BTreeMap`. macro_rules! map { ($map_ty:ident) => { - use std::collections::$map_ty; - use std::hash::Hash; - use crate::encoding::*; + use core::hash::Hash; /// Generic protobuf map encode function. pub fn encode( @@ -1225,11 +1223,17 @@ macro_rules! map { }; } +#[cfg(feature = "std")] pub mod hash_map { + use std::collections::HashMap; map!(HashMap); } pub mod btree_map { + #[cfg(feature = "std")] + use std::collections::BTreeMap; + #[cfg(not(feature = "std"))] + use alloc::collections::BTreeMap; map!(BTreeMap); } diff --git a/src/error.rs b/src/error.rs index 889326bea..321d0f01e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,8 +1,17 @@ //! Protobuf encoding and decoding errors. +// Note: this can always be alloc::borrow::Cow with rust >= 1.36 because alloc was stabilized +#[cfg(feature = "std")] use std::borrow::Cow; +#[cfg(not(feature = "std"))] +use alloc::borrow::Cow; + +#[cfg(feature = "std")] use std::error; -use std::fmt; + +use core::fmt; + +#[cfg(feature = "std")] use std::io; /// A Protobuf message decoding error. @@ -54,12 +63,14 @@ impl fmt::Display for DecodeError { } } +#[cfg(feature = "std")] impl error::Error for DecodeError { fn description(&self) -> &str { &self.description } } +#[cfg(feature = "std")] impl From for io::Error { fn from(error: DecodeError) -> io::Error { io::Error::new(io::ErrorKind::InvalidData, error) @@ -108,12 +119,14 @@ impl fmt::Display for EncodeError { } } +#[cfg(feature = "std")] impl error::Error for EncodeError { fn description(&self) -> &str { "failed to encode Protobuf message: insufficient buffer capacity" } } +#[cfg(feature = "std")] impl From for io::Error { fn from(error: EncodeError) -> io::Error { io::Error::new(io::ErrorKind::InvalidInput, error) diff --git a/src/lib.rs b/src/lib.rs index cfc3d2e7b..7ad5cd076 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,7 @@ #![doc(html_root_url = "https://docs.rs/prost/0.5.0")] +#![cfg_attr(not(feature = "std"), no_std)] + mod error; mod message; mod types; diff --git a/src/message.rs b/src/message.rs index a4acfd2b4..662384c9a 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1,5 +1,5 @@ -use std::fmt::Debug; -use std::usize; +use core::fmt::Debug; +use core::usize; use ::bytes::{Buf, BufMut, IntoBuf}; diff --git a/tests-alloc/Cargo.toml b/tests-alloc/Cargo.toml new file mode 100644 index 000000000..7a0d06918 --- /dev/null +++ b/tests-alloc/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "tests-alloc" +version = "0.0.0" +authors = ["Dan Burkert "] +publish = false +edition = "2018" + +build = "../tests/src/build.rs" + +[lib] +doctest = false +path = "../tests/src/lib.rs" + +[features] +default = ["nostd-collections"] +nostd-collections = [] + +[dependencies] +bytes = "0.4.7" +cfg-if = "0.1" +prost = { path = ".." } +prost-types = { path = "../prost-types" } +protobuf = { path = "../protobuf" } + +[dev-dependencies] +diff = "0.1" +prost-build = { path = "../prost-build" } +tempfile = "3" + +[build-dependencies] +cfg-if = "0.1" +env_logger = { version = "0.6", default-features = false } +prost-build = { path = "../prost-build" } +protobuf = { path = "../protobuf" } diff --git a/tests/src/build.rs b/tests/src/build.rs index 70c4b0480..a0f2a6869 100644 --- a/tests/src/build.rs +++ b/tests/src/build.rs @@ -20,12 +20,21 @@ fn main() { let src = PathBuf::from("../tests/src"); let includes = &[src.clone()]; + let mut config = prost_build::Config::new(); + // Generate BTreeMap fields for all messages. This forces encoded output to be consistent, so // that encode/decode roundtrips can use encoded output for comparison. Otherwise trying to // compare based on the Rust PartialEq implementations is difficult, due to presence of NaN // values. - let mut config = prost_build::Config::new(); - config.btree_map(&["."]); + // + // Note nostd collections implies Btree everywhere anyways + cfg_if! { + if #[cfg(feature = "nostd-collections")] { + config.use_alloc_collections_lib(); + } else { + config.btree_map(&["."]); + } + } // Tests for custom attributes config.type_attribute("Foo.Bar_Baz.Foo_barBaz", "#[derive(Eq, PartialOrd, Ord)]"); config.type_attribute(