最近在看 amp—— 一个受 vim启发的、由 Rust编写的文本编辑器——的源代码时,在看 key mapping的部分的时候,有这几个地方让我印象深刻:
- 使用
yaml格式的文件保存按键之间的映射关系,而不是直接写入代码 使用
build.rs在预编译阶段自动生成一些代码,减少了维护成本使用
yaml格式的文件保存映射关系使用
yaml格式的文件保存按键和按键对应的动作之间的映射关系,例如:insert:_: buffer::insert_charenter: buffer::insert_newlinetab: buffer::insert_tabbackspace: buffer::backspaceup: cursor::move_updown: cursor::move_downleft: cursor::move_left# ...
这样子,这些映射关系就不必写入代码,更加利于维护、更加直观。
在另一个rs文件中,解析yaml文件。但是,这样子解析出来的数据并不能直接绑定按键和对应的动作,还需要一个函数字符串与函数字符串对应的函数的map才可以进行绑定:我们暂且称其为commands_map。yaml解析出了key -> action_str的对应关系,而commands_map解析出了action_str -> action_function的对应关系。使用
build.rs在预编译阶段自动生成一些代码上个
section提到的commands_map是使用build.rs在预编译阶段自动生成的——显而易见的,commands内添加了新的command以后,如果还需要手动在生成commands_map的函数里面添加一句insert,无疑是十分不方便且容易遗忘的。
使用build.rs可以自动生成生成commands_map的代码,例如,以下是自动生成的hash_map文件:{let mut commands: HashMap<&'static str, Command> = HashMap::new();commands.insert("line_jump::accept_input", line_jump::accept_input);commands.insert("line_jump::push_search_char", line_jump::push_search_char);commands.insert("line_jump::pop_search_char", line_jump::pop_search_char);commands.insert("cursor::move_up", cursor::move_up);commands.insert("cursor::move_down", cursor::move_down);commands.insert("cursor::move_left", cursor::move_left);commands.insert("cursor::move_right", cursor::move_right);// ...}
怎么做到的呢?
首先,类似这种函数,函数签名必定是很有规律的,例如,amp里面自动生成的commands的函数签名符合以下的正则表达式:const COMMAND_REGEX: &'static str =r"pub fn (.*)\(app: &mut Application\) -> Result";
这样,就可以轻松地找到某个文件里所有的函数定义。
例如,以下的代码就是找到src/commands内的所有文件内签名符合上述正则表达式的函数,并且打印到编译期文件中: ```rust fn write_commands(output: &mut File) -> Result<(), &str> { let expression = Regex::new(COMMAND_REGEX).expect("Failed to compile command matching regex");
let entries = fs::read_dir(“./src/commands/“)
.map_err(|_| "Failed to read command module directory")?;
for entry in entries {
let path = entry.map_err(|_| "Failed to read command module directory entry")?.path();let module_name = module_name(&path).unwrap();let content = read_to_string(&path).map_err(|_| "Failed to read command module data")?;for captures in expression.captures_iter(&content) {let function_name = captures.get(1).unwrap().as_str();write_command(output, &module_name, function_name)?;}
}
Ok(()) }
fn writecommand(output: &mut File, module_name: &str, function_name: &str) -> Result<usize, &’static str> { output.write( format!( “ commands.insert(\”{}::{}\”, {}::{});\n”, module_name, function_name, module_name, function_name ).as_bytes() ).map_err(|| “Failed to write command”) }
最后,只需要在生成 `commands_map`的地方写下如下宏即可:```rustpub fn hash_map() -> HashMap<&'static str, Command> {include!(concat!(env!("OUT_DIR"), "/hash_map"))}
联想
其实这一套在C语言里面早就有了,使用C语言的宏和 #include等预处理命令就可以做到这些。但是在高级语言里面使用这种做法,我还是第一次看到,也体会到了 rust的宏的强大和 build.rs的方便之处。
