Use bindgen output to generate code #22

Merged
rafaelcaricio merged 3 commits from syn-codegen into master 2020-06-14 09:10:30 +00:00
11 changed files with 158 additions and 233 deletions

2
.gitignore vendored
View file

@ -12,5 +12,3 @@ Cargo.lock
examples/demo/target/ examples/demo/target/
lvgl-sys/target/ lvgl-sys/target/
lvgl/target/ lvgl/target/
lvgl-sys/src/bindings.rs
lvgl/src/widgets/generated.rs

View file

@ -62,14 +62,6 @@ $ git submodule init
$ git submodule update $ git submodule update
``` ```
Some parts of `lvgl-rs` are auto-generated. The package `lvgl-codegen` is responsible for the generated code.
When building from source, we need to first compile the `lvgl-codegen` tool. The `lvgl-codegen` tool is used
automatically in the [`lvgl` package build](./lvgl/build.rs).
```bash
$ DEP_LV_CONFIG_PATH=`pwd`/examples/include cargo build --package lvgl-codegen
```
Then run the `demo` example: Then run the `demo` example:
```shell ```shell

View file

@ -1,23 +1,17 @@
[package] [package]
name = "lvgl-codegen" name = "lvgl-codegen"
version = "0.1.0" version = "0.3.0"
description = "Code generation based on LVGL source code" description = "Code generation based on LVGL source code"
authors = ["Rafael Caricio <rafael@caricio.com>"] authors = ["Rafael Caricio <rafael@caricio.com>"]
edition = "2018" edition = "2018"
license = "MIT" license = "MIT"
publish = false
build = "build.rs"
[dependencies] [dependencies]
regex = "1.3.9" regex = "1.3.9"
quote = "1.0.7" quote = "1.0.7"
lazy_static = "1.4.0" lazy_static = "1.4.0"
clang = { git = "https://github.com/rafaelcaricio/clang-rs" }
itertools = "0.9.0" itertools = "0.9.0"
proc-macro2 = "1.0.18" proc-macro2 = "1.0.18"
Inflector = "0.11.4" Inflector = "0.11.4"
lang-c = "0.8.1" syn = { version = "1.0.31", features = ["full"]}
syn = "1.0.31"
[build-dependencies]
cc = "1.0.50"

View file

@ -1,70 +0,0 @@
use cc::Build;
use std::ffi::OsStr;
use std::process::{Command, Stdio};
use std::{env, fs, path::Path, path::PathBuf};
static CONFIG_NAME: &str = "DEP_LV_CONFIG_PATH";
fn main() {
let project_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap())
.canonicalize()
.unwrap();
let lvgl_sys = project_dir.join("..").join("lvgl-sys");
let vendor = lvgl_sys.join("vendor");
let lvgl_top_path = vendor.join("lvgl");
let lvgl_src_path = lvgl_top_path.join("src");
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
let lv_config_dir = {
let raw_path = env::var(CONFIG_NAME).unwrap_or_else(|_| {
panic!(
"The environment variable {} is required to be defined",
CONFIG_NAME
);
});
let conf_path = PathBuf::from(raw_path);
if !conf_path.exists() {
panic!(format!(
"Directory referenced by {} needs to exist",
CONFIG_NAME
));
}
if !conf_path.is_dir() {
panic!(format!("{} needs to be a directory", CONFIG_NAME));
}
if !conf_path.join("lv_conf.h").exists() {
panic!(format!(
"Directory referenced by {} needs to contain a file called lv_conf.h",
CONFIG_NAME
));
}
println!(
"cargo:rerun-if-changed={}",
conf_path.join("lv_conf.h").to_str().unwrap()
);
conf_path
};
let build = Build::new();
let tool = build.get_compiler();
let preprocessed = Command::new(tool.path().to_str().unwrap())
.args(&[
"-E",
"-std=c99",
"-DLV_CONF_INCLUDE_SIMPLE",
"-I",
lvgl_top_path.to_string_lossy().as_ref(),
"-I",
lvgl_src_path.to_string_lossy().as_ref(),
"-I",
lv_config_dir.to_string_lossy().as_ref(),
lvgl_top_path.join("lvgl.h").to_string_lossy().as_ref(),
])
.output()
.unwrap();
let content = String::from_utf8(preprocessed.stdout).unwrap();
fs::write(out_path.join("lvgl_full.c"), content).unwrap();
}

View file

@ -1,4 +1,3 @@
use clang::{Clang, Entity, EntityKind, Index, Linkage, Type};
use inflector::cases::pascalcase::to_pascal_case; use inflector::cases::pascalcase::to_pascal_case;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use proc_macro2::{Ident, TokenStream}; use proc_macro2::{Ident, TokenStream};
@ -7,6 +6,8 @@ use quote::quote;
use regex::Regex; use regex::Regex;
use std::collections::HashMap; use std::collections::HashMap;
use std::error::Error; use std::error::Error;
use syn::export::ToTokens;
use syn::{FnArg, ForeignItem, ForeignItemFn, Item, ReturnType};
type CGResult<T> = Result<T, Box<dyn Error>>; type CGResult<T> = Result<T, Box<dyn Error>>;
@ -14,12 +15,11 @@ const LIB_PREFIX: &str = "lv_";
lazy_static! { lazy_static! {
static ref TYPE_MAPPINGS: HashMap<&'static str, &'static str> = [ static ref TYPE_MAPPINGS: HashMap<&'static str, &'static str> = [
("uint16_t", "u16"), ("u16", "u16"),
("int32_t", "i32"), ("i32", "i32"),
("uint8_t", "u8"), ("u8", "u8"),
("bool", "bool"), ("bool", "bool"),
("_Bool", "bool"), ("* const cty :: c_char", "&str"),
("const char *", "&str"),
] ]
.iter() .iter()
.cloned() .cloned()
@ -39,7 +39,7 @@ pub trait Rusty {
fn code(&self, parent: &Self::Parent) -> WrapperResult<TokenStream>; fn code(&self, parent: &Self::Parent) -> WrapperResult<TokenStream>;
} }
#[derive(Clone, Eq, PartialEq)] #[derive(Clone)]
pub struct LvWidget { pub struct LvWidget {
name: String, name: String,
methods: Vec<LvFunc>, methods: Vec<LvFunc>,
@ -66,7 +66,7 @@ impl Rusty for LvWidget {
} }
} }
#[derive(Clone, Eq, PartialEq)] #[derive(Clone)]
pub struct LvFunc { pub struct LvFunc {
name: String, name: String,
args: Vec<LvArg>, args: Vec<LvArg>,
@ -81,7 +81,7 @@ impl LvFunc {
pub fn is_method(&self) -> bool { pub fn is_method(&self) -> bool {
if self.args.len() > 0 { if self.args.len() > 0 {
let first_arg = &self.args[0]; let first_arg = &self.args[0];
return first_arg.typ.typ.contains("lv_obj_t"); return first_arg.typ.literal_name.contains("lv_obj_t");
} }
false false
} }
@ -209,32 +209,44 @@ impl Rusty for LvFunc {
} }
} }
impl From<Entity<'_>> for LvFunc { impl From<ForeignItemFn> for LvFunc {
fn from(entity: Entity) -> Self { fn from(ffi: ForeignItemFn) -> Self {
let result = entity.get_result_type().unwrap().get_display_name(); let ret = match ffi.sig.output {
let ret_type = match result.as_str() { ReturnType::Default => None,
"void" => None, ReturnType::Type(_, typ) => Some(typ.into()),
_ => Some(LvType::new(result)),
}; };
Self::new( Self::new(
entity.get_name().unwrap(), ffi.sig.ident.to_string(),
entity ffi.sig
.get_arguments() .inputs
.unwrap()
.iter() .iter()
.map(|e| e.into()) .filter_map(|fa| {
// Since we know those are foreign functions, we only care about typed arguments
if let FnArg::Typed(tya) = fa {
Some(tya)
} else {
None
}
})
.map(|a| a.clone().into())
.collect::<Vec<LvArg>>(), .collect::<Vec<LvArg>>(),
ret_type, ret,
) )
} }
} }
#[derive(Clone, Eq, PartialEq)] #[derive(Clone)]
pub struct LvArg { pub struct LvArg {
name: String, name: String,
typ: LvType, typ: LvType,
} }
impl From<syn::PatType> for LvArg {
fn from(fa: syn::PatType) -> Self {
Self::new(fa.pat.to_token_stream().to_string(), fa.ty.into())
}
}
impl LvArg { impl LvArg {
pub fn new(name: String, typ: LvType) -> Self { pub fn new(name: String, typ: LvType) -> Self {
Self { name, typ } Self { name, typ }
@ -290,31 +302,33 @@ impl Rusty for LvArg {
} }
} }
impl From<&Entity<'_>> for LvArg { #[derive(Clone)]
fn from(entity: &Entity) -> Self {
Self::new(
entity.get_name().unwrap(),
entity.get_type().unwrap().into(),
)
}
}
#[derive(Clone, Eq, PartialEq)]
pub struct LvType { pub struct LvType {
typ: String, literal_name: String,
r_type: Option<Box<syn::Type>>,
} }
impl LvType { impl LvType {
pub fn new(typ: String) -> Self { pub fn new(literal_name: String) -> Self {
Self { typ } Self {
literal_name,
r_type: None,
}
}
pub fn from(r_type: Box<syn::Type>) -> Self {
Self {
literal_name: r_type.to_token_stream().to_string(),
r_type: Some(r_type),
}
} }
pub fn is_const(&self) -> bool { pub fn is_const(&self) -> bool {
self.typ.starts_with("const ") self.literal_name.starts_with("const ")
} }
pub fn is_str(&self) -> bool { pub fn is_str(&self) -> bool {
self.typ.ends_with("char *") self.literal_name.ends_with("* const cty :: c_char")
} }
} }
@ -322,7 +336,7 @@ impl Rusty for LvType {
type Parent = LvArg; type Parent = LvArg;
fn code(&self, _parent: &Self::Parent) -> WrapperResult<TokenStream> { fn code(&self, _parent: &Self::Parent) -> WrapperResult<TokenStream> {
match TYPE_MAPPINGS.get(self.typ.as_str()) { match TYPE_MAPPINGS.get(self.literal_name.as_str()) {
Some(name) => { Some(name) => {
let val = if self.is_str() { let val = if self.is_str() {
quote!(&str) quote!(&str)
@ -339,9 +353,9 @@ impl Rusty for LvType {
} }
} }
impl From<Type<'_>> for LvType { impl From<Box<syn::Type>> for LvType {
fn from(ty: Type) -> Self { fn from(t: Box<syn::Type>) -> Self {
Self::new(ty.get_display_name()) Self::from(t)
} }
} }
@ -351,8 +365,8 @@ pub struct CodeGen {
} }
impl CodeGen { impl CodeGen {
pub fn new() -> CGResult<Self> { pub fn from(code: &str) -> CGResult<Self> {
let functions = Self::load_function_definitions()?; let functions = Self::load_func_defs(code)?;
let widgets = Self::extract_widgets(&functions)?; let widgets = Self::extract_widgets(&functions)?;
Ok(Self { functions, widgets }) Ok(Self { functions, widgets })
} }
@ -405,23 +419,31 @@ impl CodeGen {
.collect::<Vec<_>>() .collect::<Vec<_>>()
} }
pub fn load_function_definitions() -> CGResult<Vec<LvFunc>> { pub fn load_func_defs(bindgen_code: &str) -> CGResult<Vec<LvFunc>> {
let clang = Clang::new()?; let ast: syn::File = syn::parse_str(bindgen_code)?;
let index = Index::new(&clang, false, false); let fns = ast
let tu = index .items
.parser(concat!(env!("OUT_DIR"), "/lvgl_full.c"))
.parse()?;
let entities = tu
.get_entity()
.get_children()
.into_iter() .into_iter()
.filter(|e| e.get_kind() == EntityKind::FunctionDecl) .filter_map(|e| {
.filter(|e| e.get_name().is_some()) if let Item::ForeignMod(fm) = e {
.filter(|e| e.get_linkage().unwrap() != Linkage::Internal) Some(fm)
.map(|e| e.into()) } else {
.filter(|e: &LvFunc| e.name.starts_with(LIB_PREFIX)) None
.collect::<Vec<_>>(); }
Ok(entities) })
.flat_map(|e| {
e.items.into_iter().filter_map(|it| {
if let ForeignItem::Fn(f) = it {
Some(f)
} else {
None
}
})
})
.filter(|ff| ff.sig.ident.to_string().starts_with(LIB_PREFIX))
.map(|ff| ff.into())
.collect::<Vec<LvFunc>>();
Ok(fns)
} }
pub fn get_function_names(&self) -> CGResult<Vec<String>> { pub fn get_function_names(&self) -> CGResult<Vec<String>> {
@ -435,11 +457,21 @@ mod test {
use quote::quote; use quote::quote;
#[test] #[test]
fn can_list_functions() { fn can_load_bindgen_fns() {
let lv = CodeGen::new().unwrap(); let bindgen_code = quote! {
let func = String::from("lv_obj_create"); extern "C" {
let func_names = lv.get_function_names().unwrap(); #[doc = " Return with the screen of an object"]
assert!(func_names.contains(&func)); #[doc = " @param obj pointer to an object"]
#[doc = " @return pointer to a screen"]
pub fn lv_obj_get_screen(obj: *const lv_obj_t) -> *mut lv_obj_t;
}
};
let cg = CodeGen::load_func_defs(bindgen_code.to_string().as_str()).unwrap();
let ffn = cg.get(0).unwrap();
assert_eq!(ffn.name, "lv_obj_get_screen");
assert_eq!(ffn.args[0].name, "obj");
} }
#[test] #[test]
@ -494,12 +526,12 @@ mod test {
#[test] #[test]
fn generate_method_wrapper() { fn generate_method_wrapper() {
// void lv_arc_set_bg_end_angle(lv_obj_t * arc, uint16_t end); // pub fn lv_arc_set_bg_end_angle(arc: *mut lv_obj_t, end: u16);
let arc_set_bg_end_angle = LvFunc::new( let arc_set_bg_end_angle = LvFunc::new(
"lv_arc_set_bg_end_angle".to_string(), "lv_arc_set_bg_end_angle".to_string(),
vec![ vec![
LvArg::new("arc".to_string(), LvType::new("lv_obj_t *".to_string())), LvArg::new("arc".to_string(), LvType::new("*mut lv_obj_t".to_string())),
LvArg::new("end".to_string(), LvType::new("uint16_t".to_string())), LvArg::new("end".to_string(), LvType::new("u16".to_string())),
], ],
None, None,
); );
@ -523,15 +555,17 @@ mod test {
#[test] #[test]
fn generate_method_wrapper_for_str_types_as_argument() { fn generate_method_wrapper_for_str_types_as_argument() {
// void lv_label_set_text(lv_obj_t * label, const char * text) let bindgen_code = quote! {
let label_set_text = LvFunc::new( extern "C" {
"lv_label_set_text".to_string(), #[doc = " Set a new text for a label. Memory will be allocated to store the text by the label."]
vec![ #[doc = " @param label pointer to a label object"]
LvArg::new("label".to_string(), LvType::new("lv_obj_t *".to_string())), #[doc = " @param text '\\0' terminated character string. NULL to refresh with the current text."]
LvArg::new("text".to_string(), LvType::new("const char *".to_string())), pub fn lv_label_set_text(label: *mut lv_obj_t, text: *const cty::c_char);
], }
None, };
); let cg = CodeGen::load_func_defs(bindgen_code.to_string().as_str()).unwrap();
let label_set_text = cg.get(0).unwrap().clone();
let parent_widget = LvWidget { let parent_widget = LvWidget {
name: "label".to_string(), name: "label".to_string(),
methods: vec![], methods: vec![],
@ -575,17 +609,17 @@ mod test {
#[test] #[test]
fn generate_widget_with_constructor_code() { fn generate_widget_with_constructor_code() {
// lv_obj_t * lv_arc_create(lv_obj_t * par, const lv_obj_t * copy); // pub fn lv_arc_create(par: *mut lv_obj_t, copy: *const lv_obj_t) -> *mut lv_obj_t;
let arc_create = LvFunc::new( let arc_create = LvFunc::new(
"lv_arc_create".to_string(), "lv_arc_create".to_string(),
vec![ vec![
LvArg::new("par".to_string(), LvType::new("lv_obj_t *".to_string())), LvArg::new("par".to_string(), LvType::new("*mut lv_obj_t".to_string())),
LvArg::new( LvArg::new(
"copy".to_string(), "copy".to_string(),
LvType::new("const lv_obj_t *".to_string()), LvType::new("*const lv_obj_t".to_string()),
), ),
], ],
Some(LvType::new("lv_obj_t *".to_string())), Some(LvType::new("*mut lv_obj_t".to_string())),
); );
let arc_widget = LvWidget { let arc_widget = LvWidget {

View file

@ -1,33 +0,0 @@
use lvgl_codegen::{CodeGen, Rusty};
use proc_macro2::TokenStream;
use quote::quote;
use std::fs::File;
use std::io::prelude::*;
use std::path::Path;
fn main() {
let rs = Path::new(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../lvgl/src/widgets/generated.rs"
));
let codegen = CodeGen::new().unwrap();
let widgets_impl: Vec<TokenStream> = codegen
.get_widgets()
.iter()
.flat_map(|w| w.code(&()))
.collect();
let code = quote! {
#(#widgets_impl)*
};
let mut file = File::create(rs).unwrap();
writeln!(
file,
"/* automatically generated by lvgl-codegen */\n{}",
code
)
.unwrap();
}

View file

@ -1,7 +1,7 @@
[package] [package]
name = "lvgl-sys" name = "lvgl-sys"
description = "Raw bindings to the LittlevGL C library." description = "Raw bindings to the LittlevGL C library."
version = "0.2.0" version = "0.3.0"
authors = ["Rafael Caricio <crates.lvgl-sys@caric.io>"] authors = ["Rafael Caricio <crates.lvgl-sys@caric.io>"]
edition = "2018" edition = "2018"
license = "MIT" license = "MIT"

View file

@ -7,6 +7,10 @@
include!(concat!(env!("OUT_DIR"), "/bindings.rs")); include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
pub fn _bindgen_raw_src() -> &'static str {
include_str!(concat!(env!("OUT_DIR"), "/bindings.rs"))
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View file

@ -1,7 +1,7 @@
[package] [package]
name = "lvgl" name = "lvgl"
description = "LittlevGL bindings for Rust. A powerful and easy-to-use embedded GUI with many widgets, advanced visual effects (opacity, antialiasing, animations) and low memory requirements (16K RAM, 64K Flash)." description = "LittlevGL bindings for Rust. A powerful and easy-to-use embedded GUI with many widgets, advanced visual effects (opacity, antialiasing, animations) and low memory requirements (16K RAM, 64K Flash)."
version = "0.2.1" version = "0.3.0"
authors = ["Rafael Caricio <crates.lvgl@caric.io>"] authors = ["Rafael Caricio <crates.lvgl@caric.io>"]
edition = "2018" edition = "2018"
repository = "https://github.com/rafaelcaricio/lvgl-rs" repository = "https://github.com/rafaelcaricio/lvgl-rs"
@ -12,9 +12,15 @@ keywords = ["littlevgl", "lvgl", "graphical_interfaces"]
include = ["Cargo.toml", "src/**/*", "src/widgets/generated.rs"] include = ["Cargo.toml", "src/**/*", "src/widgets/generated.rs"]
[dependencies] [dependencies]
lvgl-sys = { path = "../lvgl-sys", version = "0.2.0" } lvgl-sys = { version = "0.3.0", path = "../lvgl-sys" }
cty = "0.2.1" cty = "0.2.1"
embedded-graphics = "0.6.2" embedded-graphics = "0.6.2"
cstr_core = { version = "0.2.0", default-features = false, features = ["alloc"] } cstr_core = { version = "0.2.0", default-features = false, features = ["alloc"] }
bitflags = "1.2.1" bitflags = "1.2.1"
[build-dependencies]
quote = "1.0.7"
proc-macro2 = "1.0.18"
lvgl-codegen = { version = "0.3.0", path = "../lvgl-codegen" }
lvgl-sys = { version = "0.3.0", path = "../lvgl-sys" }

View file

@ -1,33 +1,33 @@
use lvgl_codegen::{CodeGen, Rusty};
use proc_macro2::TokenStream;
use quote::quote;
use std::env; use std::env;
use std::path::Path; use std::fs::File;
use std::process::Command; use std::io::prelude::*;
use std::path::PathBuf;
fn main() { fn main() {
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
let widgets_rs_path = manifest_dir.join("src/widgets/generated.rs"); let rs = out_path.join("generated.rs");
let codegen_bin = manifest_dir
.join("..")
.join("target")
.join("debug")
.join("lvgl-codegen")
.canonicalize()
.unwrap();
println!("cargo:rerun-if-changed={}", codegen_bin.to_string_lossy());
if env::var("LVGL_FORCE_CODEGEN").is_ok() || !widgets_rs_path.exists() { let widgets_impl = lvgl_sys::_bindgen_raw_src();
println!("Generating `src/widgets/generated.rs`");
let status = Command::new(codegen_bin) let codegen = CodeGen::from(widgets_impl).unwrap();
.spawn() let widgets_impl: Vec<TokenStream> = codegen
.unwrap_or_else(|_| { .get_widgets()
panic!( .iter()
"Code generation failed because no codegen executable was found. \ .flat_map(|w| w.code(&()))
Please run `cargo build --package lvgl-codegen` and then try again.", .collect();
let code = quote! {
#(#widgets_impl)*
};
let mut file = File::create(rs).unwrap();
writeln!(
file,
"/* automatically generated by lvgl-codegen */\n{}",
code
) )
})
.wait()
.unwrap(); .unwrap();
if !status.success() {
panic!("Code generation failed");
}
}
} }

View file

@ -3,7 +3,7 @@ mod bar;
mod gauge; mod gauge;
mod label; mod label;
include!("generated.rs"); include!(concat!(env!("OUT_DIR"), "/generated.rs"));
use crate::{NativeObject, Widget}; use crate::{NativeObject, Widget};
pub use arc::*; pub use arc::*;