[CoreRC] rcgenmsg: ROS Message Transpiler

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 :)