diff --git a/lvgl-codegen/Cargo.toml b/lvgl-codegen/Cargo.toml index 37456d8..8781a86 100644 --- a/lvgl-codegen/Cargo.toml +++ b/lvgl-codegen/Cargo.toml @@ -1,12 +1,26 @@ [package] name = "lvgl-codegen" version = "0.1.0" +description = "Code generation based on LVGL source code" authors = ["Rafael Caricio "] edition = "2018" +license = "MIT" publish = false +build = "build.rs" + +[lib] +name = "lvgl_codegen" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -tera = "1.3.0" regex = "1.3.9" +quote = "1.0.7" +lazy_static = "1.4.0" +clang = { path = "../../clang-rs" } +itertools = "0.9.0" +proc-macro2 = "1.0.18" +syn = "1.0.31" + +[build-dependencies] +cc = "1.0.50" diff --git a/lvgl-codegen/build.rs b/lvgl-codegen/build.rs new file mode 100644 index 0000000..59b3ffa --- /dev/null +++ b/lvgl-codegen/build.rs @@ -0,0 +1,69 @@ +use cc::Build; +use std::process::Command; +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(); +} diff --git a/lvgl-codegen/src/lib.rs b/lvgl-codegen/src/lib.rs new file mode 100644 index 0000000..d3db203 --- /dev/null +++ b/lvgl-codegen/src/lib.rs @@ -0,0 +1,396 @@ +use clang::{Clang, Entity, EntityKind, Index, Type}; +use lazy_static::lazy_static; +use proc_macro2::TokenStream; +use quote::format_ident; +use quote::quote; +use regex::Regex; +use std::collections::HashMap; +use std::error::Error; + +type CGResult = Result>; + +const LIB_PREFIX: &str = "lv_"; + +lazy_static! { + static ref TYPE_MAPPINGS: HashMap<&'static str, &'static str> = [ + ("uint16_t", "u16"), + ("int32_t", "i32"), + ("uint8_t", "u8"), + ("bool", "bool") + ] + .iter() + .cloned() + .collect(); +} + +#[derive(Debug, Copy, Clone)] +pub enum WrapperError { + Skip, +} + +pub type WrapperResult = Result; + +pub trait Rusty { + type Parent; + + fn code(&self, parent: &Self::Parent) -> WrapperResult; +} + +#[derive(Clone, Eq, PartialEq)] +pub struct LvWidget { + name: String, + methods: Vec, +} + +#[derive(Clone, Eq, PartialEq)] +pub struct LvFunc { + name: String, + args: Vec, + ret: Option, +} + +impl LvFunc { + pub fn new(name: String, args: Vec, ret: Option) -> Self { + Self { name, args, ret } + } +} + +impl Rusty for LvFunc { + type Parent = LvWidget; + + fn code(&self, parent: &Self::Parent) -> WrapperResult { + let templ = format!("{}{}_", LIB_PREFIX, parent.name.as_str()); + let new_name = self.name.replace(templ.as_str(), ""); + let func_name = format_ident!("{}", new_name); + let original_func_name = format_ident!("{}", self.name.as_str()); + + // Make sure all argumets can be generated, skip the first arg (self)! + for arg in self.args.iter().skip(1) { + arg.code(self)?; + } + + let args_decl = self + .args + .iter() + .enumerate() + .fold(quote!(), |mut args, (i, arg)| { + // if first arg is `const`, then it should be immutable + let next_arg = if i == 0 { + if arg.get_type().is_const() { + quote!(&self) + } else { + quote!(&mut self) + } + } else { + arg.code(self).unwrap() + }; + if args.is_empty() { + quote! { + #next_arg + } + } else { + quote! { + #args, #next_arg + } + } + }); + + let args_call = self + .args + .iter() + .enumerate() + .fold(quote!(), |mut args, (i, arg)| { + // if first arg is `const`, then it should be immutable + let next_arg = if i == 0 { + quote!(self.core.raw()?.as_mut()) + } else { + let var = format_ident!("{}", arg.name.as_str()); + quote!(#var) + }; + if args.is_empty() { + quote! { + #next_arg + } + } else { + quote! { + #args, #next_arg + } + } + }); + + // TODO: Handle methods that return types + Ok(quote! { + pub fn #func_name(#args_decl) -> LvResult<()> { + unsafe { + lvgl_sys::#original_func_name(#args_call); + } + Ok(()) + } + }) + } +} + +impl From> for LvFunc { + fn from(entity: Entity) -> Self { + let result = entity.get_result_type().unwrap().get_display_name(); + let ret_type = match result.as_str() { + "void" => None, + _ => Some(LvType::new(result)), + }; + Self::new( + entity.get_name().unwrap(), + entity + .get_arguments() + .unwrap() + .iter() + .map(|e| e.into()) + .collect::>(), + ret_type, + ) + } +} + +#[derive(Clone, Eq, PartialEq)] +pub struct LvArg { + name: String, + typ: LvType, +} + +impl LvArg { + pub fn new(name: String, typ: LvType) -> Self { + Self { name, typ } + } + + pub fn get_type(&self) -> &LvType { + &self.typ + } +} + +impl Rusty for LvArg { + type Parent = LvFunc; + + fn code(&self, parent: &Self::Parent) -> WrapperResult { + let name = format_ident!("{}", self.name.as_str()); + let typ = self.typ.code(self)?; + Ok(quote! { + #name: #typ + }) + } +} + +impl From<&Entity<'_>> for LvArg { + fn from(entity: &Entity) -> Self { + Self::new( + entity.get_name().unwrap(), + entity.get_type().unwrap().into(), + ) + } +} + +#[derive(Clone, Eq, PartialEq)] +pub struct LvType { + typ: String, +} + +impl LvType { + pub fn new(typ: String) -> Self { + Self { typ } + } + + pub fn is_const(&self) -> bool { + self.typ.starts_with("const ") + } +} + +impl Rusty for LvType { + type Parent = LvArg; + + fn code(&self, _parent: &Self::Parent) -> WrapperResult { + match TYPE_MAPPINGS.get(self.typ.as_str()) { + Some(name) => { + let ident = format_ident!("{}", name); + Ok(quote! { + #ident + }) + } + None => Err(WrapperError::Skip), + } + } +} + +impl From> for LvType { + fn from(ty: Type) -> Self { + Self::new(ty.get_display_name()) + } +} + +pub struct CodeGen { + functions: Vec, + widgets: Vec, +} + +impl CodeGen { + pub fn new() -> CGResult { + let functions = Self::load_function_definitions()?; + let widgets = Self::extract_widgets(&functions)?; + Ok(Self { functions, widgets }) + } + + fn extract_widgets(functions: &Vec) -> CGResult> { + let widget_names = Self::get_widget_names(functions); + + let widgets = functions.iter().fold(HashMap::new(), |mut ws, f| { + for widget_name in &widget_names { + if f.name + .starts_with(format!("{}{}", LIB_PREFIX, widget_name).as_str()) + { + ws.entry(widget_name.clone()) + .or_insert_with(|| LvWidget { + name: widget_name.clone(), + methods: Vec::new(), + }) + .methods + .push(f.clone()) + } + } + ws + }); + + Ok(widgets.values().map(|v| v.clone()).collect()) + } + + fn get_widget_names(functions: &Vec) -> Vec { + let reg = format!("^{}([^_]+)_create$", LIB_PREFIX); + let create_func = Regex::new(reg.as_str()).unwrap(); + + functions + .iter() + .filter(|e| create_func.is_match(e.name.as_str()) && e.args.len() == 2) + .filter_map(|f| { + Some(String::from( + create_func + .captures(f.name.as_str()) + .unwrap() + .get(1) + .unwrap() + .as_str(), + )) + }) + .collect::>() + } + + pub fn load_function_definitions() -> CGResult> { + let clang = Clang::new()?; + let index = Index::new(&clang, false, false); + let tu = index + .parser(concat!(env!("OUT_DIR"), "/lvgl_full.c")) + .parse()?; + let entities = tu + .get_entity() + .get_children() + .into_iter() + .filter(|e| e.get_kind() == EntityKind::FunctionDecl) + .filter(|e| e.get_name().is_some()) + .map(|e| e.into()) + .filter(|e: &LvFunc| e.name.starts_with(LIB_PREFIX)) + .collect::>(); + Ok(entities) + } + + pub fn get_function_names(&self) -> CGResult> { + Ok(self.functions.iter().map(|f| f.name.clone()).collect()) + } +} + +#[cfg(test)] +mod test { + use crate::{CodeGen, LvArg, LvFunc, LvType, LvWidget, Rusty}; + use quote::quote; + + #[test] + fn can_list_functions() { + let lv = CodeGen::new().unwrap(); + let func = String::from("lv_obj_create"); + let func_names = lv.get_function_names().unwrap(); + assert!(func_names.contains(&func)); + } + + #[test] + fn can_identify_widgets_from_function_names() { + let funcs = vec![ + LvFunc::new( + "lv_obj_create".to_string(), + vec![ + LvArg::new("parent".to_string(), LvType::new("abc".to_string())), + LvArg::new("copy_from".to_string(), LvType::new("bcf".to_string())), + ], + None, + ), + LvFunc::new( + "lv_btn_create".to_string(), + vec![ + LvArg::new("parent".to_string(), LvType::new("abc".to_string())), + LvArg::new("copy_from".to_string(), LvType::new("bcf".to_string())), + ], + None, + ), + LvFunc::new( + "lv_do_something".to_string(), + vec![ + LvArg::new("parent".to_string(), LvType::new("abc".to_string())), + LvArg::new("copy_from".to_string(), LvType::new("bcf".to_string())), + ], + None, + ), + LvFunc::new( + "lv_invalid_create".to_string(), + vec![LvArg::new( + "parent".to_string(), + LvType::new("abc".to_string()), + )], + None, + ), + LvFunc::new( + "lv_cb_create".to_string(), + vec![ + LvArg::new("parent".to_string(), LvType::new("abc".to_string())), + LvArg::new("copy_from".to_string(), LvType::new("bcf".to_string())), + ], + None, + ), + ]; + + let widget_names = CodeGen::get_widget_names(&funcs); + + assert_eq!(widget_names.len(), 3); + } + + #[test] + fn generate_method_wrapper() { + // void lv_arc_set_bg_end_angle(lv_obj_t * arc, uint16_t end); + let arc_set_bg_end_angle = LvFunc::new( + "lv_arc_set_bg_end_angle".to_string(), + vec![ + LvArg::new("arc".to_string(), LvType::new("lv_obj_t *".to_string())), + LvArg::new("end".to_string(), LvType::new("uint16_t".to_string())), + ], + None, + ); + let arc_widget = LvWidget { + name: "arc".to_string(), + methods: vec![], + }; + + let code = arc_set_bg_end_angle.code(&arc_widget).unwrap(); + let expected_code = quote! { + pub fn set_bg_end_angle(&mut self, end: u16) -> LvResult<()> { + unsafe { + lvgl_sys::lv_arc_set_bg_end_angle(self.core.raw()?.as_mut(), end); + } + Ok(()) + } + }; + + assert_eq!(code.to_string(), expected_code.to_string()); + } +} diff --git a/lvgl-codegen/src/main.rs b/lvgl-codegen/src/main.rs deleted file mode 100644 index 4659e47..0000000 --- a/lvgl-codegen/src/main.rs +++ /dev/null @@ -1,35 +0,0 @@ -use regex::Regex; -use tera::{Context, Tera}; - -fn main() { - let re = Regex::new(r"\((?P[^,]+), (?P[^,]+), (?P[^,]+), (?P[^)]+), [a-z]+\)").unwrap(); - - let input = include_str!("../../lvgl-sys/vendor/lvgl/src/lv_core/lv_obj_style_dec.h"); - - let mut tera = Tera::default(); - tera.add_raw_template("styles.rs", include_str!("../templates/style.rs.j2")) - .unwrap(); - - for line in input.lines() { - if !line.starts_with("_LV_OBJ_STYLE_SET_GET_DECLARE") { - continue; - } - - if let Some(cap) = re.captures(line) { - let style_type = cap.get(4).unwrap().as_str().to_string(); - if style_type.eq("_ptr") { - // Just a few, we will take care of this manually. - continue; - } - - let value_type = cap.get(3).unwrap().as_str().to_string(); - - let mut ctx = Context::new(); - ctx.insert("prop_name", cap.get(1).unwrap().as_str()); - ctx.insert("func_name", cap.get(2).unwrap().as_str()); - ctx.insert("value_type", value_type.as_str()); - ctx.insert("style_type", style_type.as_str()); - println!("{}", tera.render("styles.rs", &ctx).unwrap()); - } - } -} diff --git a/lvgl-codegen/templates/style.rs.j2 b/lvgl-codegen/templates/style.rs.j2 deleted file mode 100644 index 12aa56d..0000000 --- a/lvgl-codegen/templates/style.rs.j2 +++ /dev/null @@ -1,33 +0,0 @@ -pub fn set_{{func_name}}(&mut self, state: State, - - {% if style_type == "_color" %} - value: Color - {% elif style_type == "_int" %} - value: i16 - {% elif style_type == "_ptr" %} - value: Any - {% elif style_type == "_opa" %} - value: Opacity - {% endif %} - -) { - let native_state: u32 = state.get_bits(); - unsafe { - lvgl_sys::_lv_style_set{{style_type}}( - self.raw.as_mut(), - (lvgl_sys::LV_STYLE_{{prop_name}} - | (native_state << lvgl_sys::LV_STYLE_STATE_POS as u32)) as u16, - - {% if style_type == "_color" %} - value.raw, - {% elif style_type == "_int" %} - value - {% elif style_type == "_opa" %} - value.into() - {% elif style_type == "_ptr" %} - value.into() - {% endif %} - - ); - } -}