Generate code for most of the widgets #20
5 changed files with 480 additions and 69 deletions
|
@ -1,12 +1,26 @@
|
|||
[package]
|
||||
name = "lvgl-codegen"
|
||||
version = "0.1.0"
|
||||
description = "Code generation based on LVGL source code"
|
||||
authors = ["Rafael Caricio <rafael@caricio.com>"]
|
||||
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"
|
||||
If a specific commit of the
If a specific commit of the `clang-rs` repo is needed, it's possible to do something like:
```text
clang = { git = "https://github.com/KyleMayes/clang-rs.git", rev = "<commit hash>" }
```
I needed to actually write some tweaks to the code. 😕 Maybe a PR to upstream would be the way to go here. I needed to actually write some tweaks to the code. 😕 Maybe a PR to upstream would be the way to go here.
|
||||
lazy_static = "1.4.0"
|
||||
clang = { path = "../../clang-rs" }
|
||||
itertools = "0.9.0"
|
||||
proc-macro2 = "1.0.18"
|
||||
Remove this, not used. Remove this, not used.
For context, I tried to use this library. But it cannot parse C99 standard. So I went back to For context, I tried to use this library. But it cannot parse C99 standard. So I went back to `clang-rs`. If we can find a good C99 parser we could simplify a lot all this code generation work.
|
||||
syn = "1.0.31"
|
||||
|
||||
[build-dependencies]
|
||||
cc = "1.0.50"
|
||||
|
|
69
lvgl-codegen/build.rs
Normal file
69
lvgl-codegen/build.rs
Normal file
|
@ -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(),
|
||||
This code produces a full This code produces a full `lvgl.c` source code that is used by the `lvgl-codegen` to argument the code generation. One question I have is how to support use-supplied `lvgl_config.h`.
I mean, how to support generic I mean, how to support generic `lvgl-config.h` for the case of people downloading pre-built `lvgl-rs` from `crates.io`.
|
||||
])
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
let content = String::from_utf8(preprocessed.stdout).unwrap();
|
||||
fs::write(out_path.join("lvgl_full.c"), content).unwrap();
|
||||
}
|
396
lvgl-codegen/src/lib.rs
Normal file
396
lvgl-codegen/src/lib.rs
Normal file
|
@ -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<T> = Result<T, Box<dyn Error>>;
|
||||
|
||||
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<T> = Result<T, WrapperError>;
|
||||
|
||||
pub trait Rusty {
|
||||
type Parent;
|
||||
|
||||
fn code(&self, parent: &Self::Parent) -> WrapperResult<TokenStream>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
pub struct LvWidget {
|
||||
name: String,
|
||||
methods: Vec<LvFunc>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
pub struct LvFunc {
|
||||
name: String,
|
||||
args: Vec<LvArg>,
|
||||
ret: Option<LvType>,
|
||||
}
|
||||
|
||||
impl LvFunc {
|
||||
pub fn new(name: String, args: Vec<LvArg>, ret: Option<LvType>) -> Self {
|
||||
Self { name, args, ret }
|
||||
}
|
||||
}
|
||||
|
||||
impl Rusty for LvFunc {
|
||||
type Parent = LvWidget;
|
||||
|
||||
fn code(&self, parent: &Self::Parent) -> WrapperResult<TokenStream> {
|
||||
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<Entity<'_>> 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::<Vec<LvArg>>(),
|
||||
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<TokenStream> {
|
||||
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<TokenStream> {
|
||||
match TYPE_MAPPINGS.get(self.typ.as_str()) {
|
||||
Some(name) => {
|
||||
let ident = format_ident!("{}", name);
|
||||
Ok(quote! {
|
||||
#ident
|
||||
})
|
||||
}
|
||||
None => Err(WrapperError::Skip),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Type<'_>> for LvType {
|
||||
fn from(ty: Type) -> Self {
|
||||
Self::new(ty.get_display_name())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CodeGen {
|
||||
functions: Vec<LvFunc>,
|
||||
widgets: Vec<LvWidget>,
|
||||
}
|
||||
|
||||
impl CodeGen {
|
||||
pub fn new() -> CGResult<Self> {
|
||||
let functions = Self::load_function_definitions()?;
|
||||
let widgets = Self::extract_widgets(&functions)?;
|
||||
Ok(Self { functions, widgets })
|
||||
}
|
||||
|
||||
fn extract_widgets(functions: &Vec<LvFunc>) -> CGResult<Vec<LvWidget>> {
|
||||
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<LvFunc>) -> Vec<String> {
|
||||
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::<Vec<_>>()
|
||||
}
|
||||
|
||||
pub fn load_function_definitions() -> CGResult<Vec<LvFunc>> {
|
||||
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::<Vec<_>>();
|
||||
Ok(entities)
|
||||
}
|
||||
|
||||
pub fn get_function_names(&self) -> CGResult<Vec<String>> {
|
||||
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());
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
use regex::Regex;
|
||||
use tera::{Context, Tera};
|
||||
|
||||
fn main() {
|
||||
let re = Regex::new(r"\((?P<prop_name>[^,]+), (?P<func_name>[^,]+), (?P<value_type>[^,]+), (?P<style_type>[^)]+), [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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 %}
|
||||
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue
Ok, I need to commit this to my own repo. The original
clang-rs
is not up to date withclang-sys
that is used byrust-bindgen
.