Rust游戏服务端开发随笔【配置表代码生成】

567 阅读9分钟

简介

这篇文章是基于配置表解析这篇文章接着写的,如有不明白的地方,可以先看这篇文章。本篇文章需要读者对syn以及quote有一定的了解,我们需要使用这两个库来进行代码生成,后续的一些功能也会使用到这两个库。

我们希望有一个工具可以自动把excel配置表结构生成出Rust的配置表代码,这样就不用去手写了,最终生成的形式如下:


#[derive(Debug, Default, Clone, Serialize, Deserialize, Encode, Decode)]
pub struct AchievementConfig {
    pub id: i32,
    pub group: i32,
    pub task_id: i32,
    pub condition: i32,
    pub reward: Vec<(i32, i32, i32)>,
    pub point: i32,
}

#[derive(Debug, Default, Clone, Serialize, Deserialize, Encode, Decode)]
pub struct AchievementConfigs {
    pub achievement: HashMap<i32, AchievementConfig>,
}

impl AchievementConfigs {
    pub fn get_by_id(&self, id: &i32) -> anyhow::Result<&AchievementConfig> {
        self.achievement.get(id).ok_or(anyhow::anyhow!(
            "{} not found, id: {}",
            id,
            std::any::type_name::<AchievementConfig>()
        ))
    }
}

impl Deref for AchievementConfigs {
    type Target = HashMap<i32, AchievementConfig>;
    fn deref(&self) -> &Self::Target {
        &self.achievement
    }
}

impl DerefMut for AchievementConfigs {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.achievement
    }
}

impl Config for AchievementConfig {
    fn id(&self) -> Id {
        self.id.clone().into()
    }
    fn parse(row: Row) -> anyhow::Result<Self>
    where
        Self: Sized,
    {
        let id = row.get("id")?;
        let group = row.get("group")?;
        let task_id = row.get("task_id")?;
        let condition = row.get("condition")?;
        let reward = row.get("reward")?;
        let point = row.get("point")?;
        Ok(Self {
            id,
            group,
            task_id,
            condition,
            reward,
            point,
        })
    }
}

impl Configs for AchievementConfigs {
    fn ids(&self) -> Vec<Id> {
        self.achievement
            .keys()
            .map(|id| (*id).clone().into())
            .collect()
    }
    fn name() -> &'static str
    where
        Self: Sized,
    {
        "achievement"
    }
    fn load(&mut self, excel_config: &ExcelConfig) -> anyhow::Result<()> {
        let name_index = excel_config.name_index();
        for (row_number, row) in excel_config.data.iter().enumerate() {
            let config = AchievementConfig::parse(Row {
                excel_name: Self::name(),
                index: &name_index,
                row: &row,
                row_number,
            })?;
            self.achievement.insert(config.id.clone(), config);
        }
        Ok(())
    }
    fn as_any(&self) -> &dyn Any {
        self
    }
    fn as_any_mut(&mut self) -> &mut dyn Any {
        self
    }
    fn into_any(self: Box<Self>) -> Box<dyn Any> {
        self
    }
}

impl IntoIterator for AchievementConfigs {
    type Item = (i32, AchievementConfig);
    type IntoIter = std::collections::hash_map::IntoIter<i32, AchievementConfig>;
    fn into_iter(self) -> Self::IntoIter {
        self.achievement.into_iter()
    }
}

impl<'a> IntoIterator for &'a AchievementConfigs {
    type Item = (&'a i32, &'a AchievementConfig);
    type IntoIter = std::collections::hash_map::Iter<'a, i32, AchievementConfig>;
    fn into_iter(self) -> Self::IntoIter {
        self.achievement.iter()
    }
}

impl<'a> IntoIterator for &'a mut AchievementConfigs {
    type Item = (&'a i32, &'a mut AchievementConfig);
    type IntoIter = std::collections::hash_map::IterMut<'a, i32, AchievementConfig>;
    fn into_iter(self) -> Self::IntoIter {
        self.achievement.iter_mut()
    }
}

从最终的结构可以看到大致的生成逻辑

  1. 定义单行配置表数据的结构,并实现Configtrait
  2. 定义一个容纳所有行的数据结构,通常是一个HashMap,并实现Configstrait
  3. 实现Deref DerefMut IntoIterator,这部分可选,有的话更方便使用

另外我们还需要生成从ExcelConfigs -> GameConfigMgr -> GameConfigs这个过程的辅助函数,不然要手写很多代码。

pub fn build_config_mgr(excel_configs: &ExcelConfigs) -> anyhow::Result<GameConfigMgr> {
    let mut mgr = GameConfigMgr::new();
    let achievement_configs = excel_configs.load::<AchievementConfigs>()?;
    mgr.insert(achievement_configs);
    ...//省略若干字段
    Ok(mgr)
}

pub fn build_game_configs(mut mgr: GameConfigMgr) -> anyhow::Result<GameConfigs> {
    let config = GameConfigs {
        achievement_configs: mgr.remove::<AchievementConfigs>()?,
        ...//省略若干字段
    };
    Ok(config)
}

Excel配置表生成Rust配置表代码

提取配置表表头信息

首先我们要将配置表表头数据进行打组:

let ExcelConfig { name, cell_name, cell_type, cell_key, .. } = excel_config;
let name_type = cell_name
    .iter()
    .zip(cell_type)
    .zip(cell_key)
    .filter(|(_, key)| { matches!(key, KeyType::AllKey | KeyType::All | KeyType::ServerKey | KeyType::Server) })
    .map(|((name, ty), _)| (name, ty))
    .collect::<Vec<_>>();

这里打组的时候只提取了服务端需要的数据,这样可以减少最后生成的配置表二进制文件的大小。

然后定义以下变量收集代码生成的数据:

let mut fields = vec![];
let mut fields_with_ty = vec![];
let mut fields_parser = vec![];
let mut id: Option<syn::Ident> = None;
let mut id_ty: Option<TokenStream> = None;
  • fields 配置表里面的每列数据的名称,对应Rust中配置表结构的字段名
  • fields_with_ty 配置表里面每列数据的类型,对应到Rust中到类型
  • fields_parser 每个字段的解析代码
  • id id_ty 主键以及主键类型

下面就是迭代表头,处理这些数据:

name_type.iter().enumerate().for_each(|(index, (cell_name, cell_ty))| {
    let config_field = if is_rust_keywords(&cell_name) {
        syn::Ident::new_raw(cell_name, Span::call_site())
    } else {
        syn::Ident::new(cell_name, Span::call_site())
    };
    fields.push(config_field.clone());
    let ty = cell_ty.token();
    if index == 0 {
        id = Some(config_field.clone());
        id_ty = Some(ty.clone());
    }
    let field_parser = quote! {
        let #config_field = row.get(#cell_name)?;
    };
    fields_parser.push(field_parser);
    let field = quote! {
        pub #config_field: #ty,
    };
    fields_with_ty.push(field);
});

如果对is_rust_keywordstoken函数不清楚的话,可以翻阅前文,这里不再赘述。

下面把单行配置表的结构以及整个配置表结构的类型、配置表名这些定义出来:

let config_name = syn::Ident::new(&name, Span::call_site());
let pascal_config_name = name.to_case(Case::Pascal);
let config_ty: syn::Type = syn::parse_str(&*format!("{}Config", pascal_config_name))?;
let configs_ty: syn::Type = syn::parse_str(&*format!("{}Configs", pascal_config_name))?;

编写生成XXConfig的函数

fn game_config_stream(config_ty: &syn::Type, fields: Vec<TokenStream>) -> TokenStream {
    quote! {
        #[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)]
        pub struct #config_ty {
            #(#fields)*
        }
    }
}
  • config_ty 配置表的类型
  • fields参数对应上文中的fields_with_ty

编写生成XXConfigs的函数:

fn game_configs_stream(
    id_ty: &TokenStream,
    configs_name: &syn::Ident,
    config_ty: &syn::Type,
    configs_ty: &syn::Type,
) -> TokenStream {
    quote! {
        #[derive(Debug, Default, Clone, Serialize, Deserialize, Encode, Decode)]
        pub struct #configs_ty {
            pub #configs_name: HashMap<#id_ty, #config_ty>,
        }
    }
}
  • id_ty 配置表主键,作为HashMap主键
  • configs_name 配置表名称,这里是转换成小写下划线的形式

编写为XXConfig实现Configtrait的函数

fn impl_config_for_game_config(
    id: &syn::Ident,
    config_ty: &syn::Type,
    fields_parser: Vec<TokenStream>,
    fields: Vec<syn::Ident>,
) -> TokenStream {
    quote! {
        impl Config for #config_ty {
            fn id(&self) -> Id {
                self.#id.clone().into()
            }

            fn parse(row: Row) -> anyhow::Result<Self> where Self: Sized {
                #(#fields_parser)*
                Ok(Self {
                    #(#fields,)*
                })
            }
        }
    }
}
  • fields_parser 根据字段名称读取配置表数据并解析成最终类型
  • fields 配置表所有的字段名

编写为XXConfigs实现Configstrait的函数

fn impl_configs_for_game_configs(
    name: &str,
    id: &syn::Ident,
    configs_name: &syn::Ident,
    config_ty: &syn::Type,
    configs_ty: &syn::Type,
) -> TokenStream {
    quote! {
        impl Configs for #configs_ty {
            fn ids(&self) -> Vec<Id> {
                self.#configs_name.keys().map(|id| (*id).clone().into()).collect()
            }

            fn name() -> &'static str where Self: Sized {
                #name
            }

            fn load(&mut self, excel_config: &ExcelConfig) -> anyhow::Result<()> {
                let name_index = excel_config.name_index();
                for (row_number, row) in excel_config.data.iter().enumerate() {
                    let config = #config_ty::parse(Row {
                        excel_name: Self::name(),
                        index: &name_index,
                        row: &row,
                        row_number,
                    })?;
                    self.#configs_name.insert(config.#id.clone(), config);
                }
                Ok(())
            }

            fn as_any(&self) -> &dyn Any {
                self
            }

            fn as_any_mut(&mut self) -> &mut dyn Any {
                self
            }

            fn into_any(self: Box<Self>) -> Box<dyn Any> {
                self
            }
        }
    }
}

load函数中,迭代配置表的每一行数据,解析成具体的数据结构之后插入到HashMap中。

拼接TokenStream写入文件

实现Deref DerefMut Iterator部分的代码就省略了,最后把这些TokenStream拼接一下,写成rs文件就算完成了:

let stream = quote! {
    use std::any::Any;
    use std::ops::{Deref, DerefMut};

    use ahash::HashMap;
    use bincode::{Decode, Encode};
    use serde::{Deserialize, Serialize};

    use crate::{Config, Configs};
    use crate::excel::excel_config::{ExcelConfig, Row};
    use crate::excel::id::Id;

    #config_stream
    #configs_stream
    #configs_fn_stream
    #deref_impl
    #config_impl
    #configs_impl
    #iter_impl
};
generate_rs_file(path, stream)?;

配置表加载辅助函数生成

这一步相当于我们生成好所有的配置表结构之后,对所有的配置表进行解析、重构数据、配置表合法性验证,然后生成最终程序中使用的GameConfigs这个结构的过程。

这里我们需要扫描整个配置表的文件了,把所有的实现了Configs的结构收集起来,过程有点类似于Java中的反射,所以我们需要定义一个函数,来把rs文件解析成Rust源代码语法树,这个函数在后面也会用到。

pub fn parse_syn_file<P: AsRef<Path>>(path: P) -> anyhow::Result<syn::File> {
    let path = path.as_ref();
    let mut file = File::open(path).context(format!("unable to open file {}", path.display()))?;
    let mut src = String::new();
    file.read_to_string(&mut src).context(format!("unable to read file {}", path.display()))?;
    let syntax = syn::parse_file(&src).context(format!("unable to parse file {}", path.display()))?;
    Ok(syntax)
}

筛选实现了Configstrait的类型:

fn config_tys(syn_file: syn::File) -> Vec<Box<syn::Type>> {
    let mut tys = vec![];
    for item in syn_file.items {
        if let Item::Impl(item_impl) = item {
            if let Some((_, path, _)) = &item_impl.trait_ {
                if path.segments.last().unwrap().ident == "Configs" {
                    tys.push(item_impl.self_ty.clone());
                }
            }
        }
    }
    tys
}

收集所有的配置表名称以及类型:

let config_names = configs.iter().map(|ty| {
    let name = ty.to_token_stream().to_string().to_case(Case::Snake);
    syn::Ident::new(&name, Span::call_site())
}).collect::<Vec<_>>();
let name_ty = config_names.iter().zip(configs).collect::<Vec<_>>();

生成build_config_mgr函数:

我们从ExcelConfigs中把原始的配置表数据解析成程序可用的配置表数据,然后把解析好的配置表插入到HashMap中,当所有配置表解析完毕之后,我们调用complete进行配置表数据重构,然后调用validate对每个配置表进行校验。

fn loader(name_ty: &Vec<(&syn::Ident, Box<syn::Type>)>) -> TokenStream {
    let loaders = name_ty.iter().map(|(name, ty)| {
        quote! {
            let #name = excel_configs.load::<#ty>()?;
            mgr.insert(#name);
        }
    });
    quote! {
        pub fn build_config_mgr(excel_configs: &ExcelConfigs) -> anyhow::Result<GameConfigMgr> {
            let mut mgr = GameConfigMgr::new();
            #(#loaders)*
            mgr.complete()?;
            mgr.validate()?;
            Ok(mgr)
        }
    }
}

ExcelConfigs中的load函数的定义如下:

pub fn load<C>(&self) -> anyhow::Result<C> where C: Configs + Default {
    let mut config = C::default();
    let excel = self.get(C::name()).ok_or(anyhow::anyhow!("{} not found", C::name()))?;
    config.load(excel)?;
    Ok(config)
}

GameConfigMgr中的complete以及validate函数的定义如下:

pub fn complete(&mut self) -> anyhow::Result<()> {
    for name in &self.complete_ordering {
        let mut config = self.configs.remove(name).ok_or(anyhow::anyhow!("{} not found", name))?;
        config.complete(self)?;
        self.configs.insert(name, config);
    }
    let others = self.configs.keys().copied().collect::<Vec<_>>();
    for name in others {
        let mut config = self.configs.remove(name).ok_or(anyhow::anyhow!("{} not found", name))?;
        config.complete(self)?;
        self.configs.insert(name, config);
    }
    Ok(())
}

pub fn validate(&self) -> anyhow::Result<()> {
    for config in self.configs.values() {
        config.validate(self)?;
    }
    Ok(())
}

因为Configstrait中的complete函数是用&mut self修饰的,并且还要传入GameConfigMgr的引用,因为我们在重构当前配置表数据结构的时候,可能需要引用其他配置表的数据。由于Rust的借用规则限制,我们只能先把要操作的配置表从GameConfigMgr中移除出来,在complete执行完成之后,再重新插入到GameConfigMgr中。

生成build_game_configs函数

fn builder(name_ty: &Vec<(&syn::Ident, Box<syn::Type>)>) -> TokenStream {
    let builders = name_ty.iter().map(|(name, ty)| {
        quote! {
            #name: mgr.remove::<#ty>()?,
        }
    });
    quote! {
        pub fn build_game_configs(mut mgr: GameConfigMgr) -> anyhow::Result<GameConfigs> {
            let config = GameConfigs {
                #(#builders)*
            };
            Ok(config)
        }
    }
}

GameConfigMgr::remove定义如下:

pub fn remove<C>(&mut self) -> anyhow::Result<Box<C>> where C: Configs {
    let config = self.configs.remove(C::name()).ok_or(anyhow::anyhow!("{} not found", C::name()))?;
    let config = config.into_any().downcast().map_err(|_| anyhow::anyhow!("{} downcast failed", C::name()))?;
    Ok(config)
}

生成use语句将配置所在路径引入作用域

fn uses(name_ty: &Vec<(&syn::Ident, Box<syn::Type>)>) -> TokenStream {
    let uses = name_ty.iter().map(|(name, ty)| {
        let name = name.to_string();
        let name = name.strip_suffix("_configs").unwrap_or(&name);
        let name = syn::Ident::new(name, Span::call_site());
        quote! {
            use crate::game_config::#name::#ty;
        }
    });
    quote! {
        #(#uses)*
    }
}

生成辅助函数最终的TokenStream

let loader = quote! {
    use bincode::{Decode, Encode};
    use serde::{Deserialize, Serialize};

    use crate::excel::excel_config::ExcelConfigs;
    use crate::game_config_mgr::GameConfigMgr;
    use crate::game_configs::GameConfigs;

    #uses_stream

    #loader_stream
    #builder_stream
};

这部分代码的功能就是将ExcelConfigs解析成GameConfigMgr,然后再从GameConfigMgr生成GameConfigs的过程。我们最后生成配置表二进制文件的时候,只需要将excel配置表读取成ExcelConfigs就可以了,然后分别运行这两个函数,就可以得到二进制文件了。

生成GameConfigs结构

fn game_configs(name_ty: &Vec<(&syn::Ident, Box<syn::Type>)>) -> TokenStream {
    let fields = name_ty.iter().map(|(name, ty)| {
        quote! {
            pub #name: Box<#ty>,
        }
    });
    quote! {
        #[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)]
        pub struct GameConfigs {
            #(#fields)*
        }
    }
}

生成GameConfigs最终的TokenStream

let game_configs = game_configs(&name_ty);
let game_configs_stream = quote! {
    #uses_stream
    #game_configs
};

结语

代码生成是一个可选项,其实我们把配置表解析这部分做好就算是完成了,这些部分也可以每次新加配置表的时候,挨个手写一遍。但是为了高效开发,这部分还是值得做的。

在策划新加配置表的时候,我们只需要运行将Excel结构转换成Rust代码的函数就可以快速得到配置表的解析代码,然后再运行生成辅助函数的方法,就可以快速的将这些新加的配置表结构添加到已有的代码中,我们就只需要关心要编写哪些配置表校验逻辑即可。