flatc_rust/lib.rs
1//! This crate provides a programmatical way to invoke `flatc` command (e.g. from `build.rs`) to
2//! generate Rust (or, in fact, any other language) helpers to work with FlatBuffers.
3//!
4//! NOTE: You will still need
5//! [`flatc` utility](https://google.github.io/flatbuffers/flatbuffers_guide_using_schema_compiler.html)
6//! version [1.10.0+](https://github.com/google/flatbuffers/releases/tag/v1.10.0) installed (there
7//! are [windows binary releases](https://github.com/google/flatbuffers/releases), `flatbuffers`
8//! packages for [conda](https://anaconda.org/conda-forge/flatbuffers) [Windows, Linux, MacOS],
9//! [Arch Linux](https://www.archlinux.org/packages/community/x86_64/flatbuffers/)).
10//!
11//! # Examples
12//!
13//! ## Minimal useful example
14//!
15//! Let's assume you have `input.fbs` specification file in `flatbuffers` folder, and you want to
16//! generate Rust helpers into `flatbuffers-helpers-for-rust` folder:
17//!
18//! ```
19//! use std::path::Path;
20//!
21//! use flatc_rust;
22//!
23//! # fn try_main() -> flatc_rust::Result<()> {
24//! #
25//! flatc_rust::run(flatc_rust::Args {
26//! lang: "rust", // `rust` is the default, but let's be explicit
27//! inputs: &[Path::new("./flatbuffers/input.fbs")],
28//! out_dir: Path::new("./flatbuffers-helpers-for-rust/"),
29//! ..Default::default()
30//! })?;
31//! #
32//! # Ok(())
33//! # }
34//! # try_main().ok();
35//! ```
36//!
37//! ## Build scripts (`build.rs`) integration
38//!
39//! It is common to have FlatBuffers specifications as a single source of truth, and thus, it is
40//! wise to build up-to-date helpers when you build your project. There is a built-in support for
41//! [build scripts in Cargo], so you don't need to sacrifice the usual workflow (`cargo build /
42//! cargo run`) in order to generate the helpers.
43//!
44//! 1. Create `build.rs` in the root of your project (along side with `Cargo.toml`) or follow the
45//! official documentation about build scripts.
46//! 2. Adapt the following example to fit your needs and put it into `build.rs`:
47//!
48//! ```no_run
49//! extern crate flatc_rust; // or just `use flatc_rust;` with Rust 2018 edition.
50//!
51//! use std::path::Path;
52//!
53//! fn main() {
54//! println!("cargo:rerun-if-changed=src/message.fbs");
55//! flatc_rust::run(flatc_rust::Args {
56//! inputs: &[Path::new("src/message.fbs")],
57//! out_dir: Path::new("target/flatbuffers/"),
58//! ..Default::default()
59//! }).expect("flatc");
60//! }
61//! ```
62//! 3. Add `flatc-rust` into `[build-dependencies]` section in `Cargo.toml`:
63//!
64//! ```toml
65//! [build-dependencies]
66//! flatc-rust = "*"
67//! ```
68//! 4. Add `flatbuffers` into `[dependencies]` section in `Cargo.toml`:
69//!
70//! ```toml
71//! [dependencies]
72//! flatbuffers = "0.5"
73//! ```
74//! 5. Include the generated helpers in your `main.rs` or `lib.rs`:
75//!
76//! ```ignore
77//! #[allow(non_snake_case)]
78//! #[path = "../target/flatbuffers/message_generated.rs"]
79//! pub mod message_flatbuffers;
80//! ```
81//! 5. Use the helpers like any regular Rust module ([example projects])
82//!
83//! [build scripts in Cargo]: https://doc.rust-lang.org/cargo/reference/build-scripts.html
84//! [example projects]: https://github.com/frol/flatc-rust/tree/master/examples
85//!
86//! ## Usage in external projects
87//!
88//! There is [a benchmark of FlatBuffers vs other serialization
89//! frameworks](https://github.com/erickt/rust-serialization-benchmarks/pull/7), which is based on
90//! `flatc-rust` integration.
91
92#![deny(missing_docs)]
93#![deny(unsafe_code)]
94
95use std::ffi::OsString;
96use std::io;
97use std::path::{Path, PathBuf};
98use std::process;
99
100use log::info;
101
102/// The default Error type of the crate
103pub type Error = io::Error;
104/// The default Result type of the crate
105pub type Result<T> = io::Result<T>;
106
107fn err_other<E>(error: E) -> Error
108where
109 E: Into<Box<dyn std::error::Error + Send + Sync>>,
110{
111 Error::new(io::ErrorKind::Other, error)
112}
113
114/// This structure represents the arguments passed to `flatc`
115///
116/// # Example
117///
118/// ```
119/// use std::path::Path;
120///
121/// let flatc_args = flatc_rust::Args {
122/// lang: "rust",
123/// inputs: &[Path::new("./src/input.fbs")],
124/// out_dir: Path::new("./flatbuffers-helpers-for-rust/"),
125/// ..Default::default()
126/// };
127/// ```
128#[derive(Debug, Clone, Copy)]
129pub struct Args<'a> {
130 /// Specify the programming language (`rust` is the default)
131 pub lang: &'a str,
132 /// List of `.fbs` files to compile [required to be non-empty]
133 pub inputs: &'a [&'a Path],
134 /// Output path for the generated helpers (`-o PATH` parameter) [required]
135 pub out_dir: &'a Path,
136 /// Search for includes in the specified paths (`-I PATH` parameter)
137 pub includes: &'a [&'a Path],
138 /// Set the flatc '--binary' flag
139 pub binary: bool,
140 /// Set the flatc '--schema' flag
141 pub schema: bool,
142 /// Set the flatc '--json' flag
143 pub json: bool,
144 /// Extra args to pass to flatc
145 pub extra: &'a [&'a str],
146}
147
148impl Default for Args<'_> {
149 fn default() -> Self {
150 Self {
151 lang: "rust",
152 out_dir: Path::new(""),
153 includes: &[],
154 inputs: &[],
155 binary: false,
156 schema: false,
157 json: false,
158 extra: &[],
159 }
160 }
161}
162
163/// Programmatic interface (API) for `flatc` command.
164///
165/// NOTE: You may only need a small helper function [`run`].
166///
167/// [`run`]: fn.run.html
168pub struct Flatc {
169 exec: PathBuf,
170}
171
172impl Flatc {
173 /// New `flatc` command from `$PATH`
174 pub fn from_env_path() -> Flatc {
175 Flatc {
176 exec: PathBuf::from("flatc"),
177 }
178 }
179
180 /// New `flatc` command from specified path
181 pub fn from_path<P: std::convert::Into<PathBuf>>(path: P) -> Flatc {
182 Flatc { exec: path.into() }
183 }
184
185 /// Check `flatc` command found and valid
186 pub fn check(&self) -> Result<()> {
187 self.version().map(|_| ())
188 }
189
190 fn spawn(&self, cmd: &mut process::Command) -> io::Result<process::Child> {
191 info!("spawning command {:?}", cmd);
192
193 cmd.spawn()
194 .map_err(|e| Error::new(e.kind(), format!("failed to spawn `{:?}`: {}", cmd, e)))
195 }
196
197 /// Obtain `flatc` version
198 pub fn version(&self) -> Result<Version> {
199 let child = self.spawn(
200 process::Command::new(&self.exec)
201 .stdin(process::Stdio::null())
202 .stdout(process::Stdio::piped())
203 .stderr(process::Stdio::piped())
204 .args(&["--version"]),
205 )?;
206
207 let output = child.wait_with_output()?;
208 if !output.status.success() {
209 return Err(err_other("flatc failed with error"));
210 }
211 let output = String::from_utf8(output.stdout).map_err(err_other)?;
212 let output = output
213 .lines()
214 .next()
215 .ok_or_else(|| err_other("output is empty"))?;
216 let prefix = "flatc version ";
217 if !output.starts_with(prefix) {
218 return Err(err_other("output does not start with prefix"));
219 }
220 let output = &output[prefix.len()..];
221 let first_char = output
222 .chars()
223 .next()
224 .ok_or_else(|| err_other("version is empty"))?;
225 if !first_char.is_digit(10) {
226 return Err(err_other("version does not start with digit"));
227 }
228 Ok(Version {
229 version: output.to_owned(),
230 })
231 }
232
233 /// Execute `flatc` command with given args, check it completed correctly.
234 fn run_with_args(&self, args: Vec<OsString>) -> Result<()> {
235 let mut cmd = process::Command::new(&self.exec);
236 cmd.stdin(process::Stdio::null());
237 cmd.args(args);
238
239 let mut child = self.spawn(&mut cmd)?;
240
241 if !child.wait()?.success() {
242 return Err(err_other(format!(
243 "flatc ({:?}) exited with non-zero exit code",
244 cmd
245 )));
246 }
247
248 Ok(())
249 }
250
251 /// Execute configured `flatc` with given args
252 pub fn run(&self, args: Args) -> Result<()> {
253 let mut cmd_args: Vec<OsString> = Vec::new();
254
255 if args.out_dir.as_os_str().is_empty() {
256 return Err(err_other("out_dir is empty"));
257 }
258
259 cmd_args.push({
260 let mut arg = OsString::with_capacity(args.lang.len() + 3);
261 arg.push("--");
262 arg.push(args.lang);
263 arg
264 });
265
266 if args.binary {
267 cmd_args.push("--binary".into());
268 }
269
270 if args.schema {
271 cmd_args.push("--schema".into());
272 }
273
274 if args.json {
275 cmd_args.push("--json".into());
276 }
277
278 for extra_arg in args.extra {
279 cmd_args.push(extra_arg.into());
280 }
281
282 if args.lang.is_empty() {
283 return Err(err_other("lang is empty"));
284 }
285
286 for include in args.includes.iter() {
287 cmd_args.push("-I".into());
288 cmd_args.push(include.into());
289 }
290
291 cmd_args.push("-o".into());
292 cmd_args.push(
293 args.out_dir
294 .to_str()
295 .ok_or_else(|| {
296 Error::new(
297 io::ErrorKind::Other,
298 "only UTF-8 convertable paths are supported",
299 )
300 })?
301 .into(),
302 );
303
304 if args.inputs.is_empty() {
305 return Err(err_other("input is empty"));
306 }
307
308 cmd_args.extend(args.inputs.iter().map(|input| input.into()));
309
310 self.run_with_args(cmd_args)
311 }
312}
313
314/// Execute `flatc` found in `$PATH` with given args
315///
316/// # Examples
317///
318/// Please, refer to [the root crate documentation](index.html#examples).
319pub fn run(args: Args) -> Result<()> {
320 let flatc = Flatc::from_env_path();
321
322 // First check with have good `flatc`
323 flatc.check()?;
324
325 flatc.run(args)
326}
327
328/// FlatBuffers (flatc) version.
329pub struct Version {
330 version: String,
331}
332
333impl Version {
334 /// Version getter
335 pub fn version(&self) -> &str {
336 &self.version
337 }
338}
339
340#[cfg(test)]
341mod test {
342 use tempfile;
343
344 use super::*;
345
346 #[test]
347 fn version() {
348 Flatc::from_env_path().version().expect("version");
349 }
350
351 #[test]
352 fn run_can_produce_output() -> io::Result<()> {
353 let temp_dir = tempfile::Builder::new().prefix("flatc-rust").tempdir()?;
354 let input_path = temp_dir.path().join("test.fbs");
355 std::fs::write(&input_path, "table Test { text: string; } root_type Test;")
356 .expect("test input fbs file could not be written");
357
358 run(Args {
359 lang: "rust",
360 inputs: &[&input_path],
361 out_dir: temp_dir.path(),
362 ..Default::default()
363 })
364 .expect("run");
365
366 let output_path = input_path.with_file_name("test_generated.rs");
367 assert!(output_path.exists());
368 assert_ne!(output_path.metadata().unwrap().len(), 0);
369
370 Ok(())
371 }
372}