There is actually not much to say but I still want to keep my blogging streak :) So this is about the ROS message transpiler I developed with Rust. It uses the Pest parser and handlebars-rust to convert ROS msg to Cap'n Proto format. This is my first time using Rust on a serious project so expect bad code quality :(
Parsing
Parsing is mostly documented in the previous post. However the resulting data structure is Pest's own Iter
-based format. We need to convert it into an intermediate representation with the Serialize
trait required by handlebars-rust
. This is done via a custom ASTDef
type with the required traits. Though I think there may be better methods to do that, this is the best I can come up with.
#[derive(Serialize, Deserialize, Debug)]
pub struct ConstDef {
typ: String,
name: String,
val: String,
comment: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct FieldDef {
typ: String,
name: String,
comment: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ASTDef {
consts: Vec<ConstDef>,
fields: Vec<FieldDef>,
}
fn pest_to_ast(pes: &pest::iterators::Pair<Rule>) -> Option<ASTDef> {
let mut ast = ASTDef {
consts: Vec::new(),
fields: Vec::new(),
};
// Because ident_list is silent, the iterator will contain idents
for pair in pes.clone().into_inner() {
let span = pair.clone().into_span();
match pair.as_rule() {
Rule::constant => {
let mut typ: Option<String> = None;
let mut name: Option<String> = None;
let mut val: Option<String> = None;
let mut comment: Option<String> = None;
for inner_pair in pair.clone().into_inner() {
let inner_span = inner_pair.clone().into_span();
match inner_pair.as_rule() {
Rule::itype => {
typ = Some(inner_pair.as_str().to_string());
}
Rule::identifier => {
name = Some(inner_pair.as_str().to_string());
}
Rule::value => {
val = Some(inner_pair.as_str().to_string());
}
Rule::comment => {
comment = Some(inner_pair.as_str().to_string());
}
_ => panic!("ERR {:?}: {}", inner_pair.as_rule(), inner_span.as_str()),
};
}
ast.consts.push(ConstDef {
typ: typ.expect("No type detected"),
name: name.expect("No name detected"),
val: val.expect("No val detected"),
comment: comment.unwrap_or("".to_string()),
})
}
Rule::variable => {
...
}
Rule::comment => {
// println!("{}", pair.as_str());
}
_ => panic!("ERR {:?}: {}", pair.as_rule(), span.as_str()),
}
}
return Some(ast);
}
As you can see, every node needs to be filled manually. If you have a better method, please comment below :)
Code Generation
The Cap'n Proto code is then generated by handlebars-rust
. Because we implemented the Serialize
trait in our AST, we can now simply use &serde_json::to_value(&ast)
to feed JSON data to handlebars-rust
.
fn compile_file(ast: ASTDef) {
use handlebars::Handlebars;
let reg = Handlebars::new();
let template =
r###"struct BatteryState {
{{#each consts as |c| ~}}
const {{c.name}} @0 : {{c.typ}} = {{ c.val }};{{ c.comment }}
{{/each ~}}
{{#each fields as |f| ~}}
{{f.name}} @0 : {{f.typ}};{{ f.comment }}
{{/each ~}}
}"###;
// render without register
println!("{}", reg.render_template( template, &serde_json::to_value(&ast).unwrap()).unwrap());
}
Conclusion
This project is fairly simple and really nice for a Rust newcomer like myself. Next step would probably be writing the roscpp
compatibility layer in Rust and export with C++. Anyway, stay tuned :)