最近在看 amp
—— 一个受 vim
启发的、由 Rust
编写的文本编辑器——的源代码时,在看 key mapping
的部分的时候,有这几个地方让我印象深刻:
- 使用
yaml
格式的文件保存按键之间的映射关系,而不是直接写入代码 使用
build.rs
在预编译阶段自动生成一些代码,减少了维护成本使用
yaml
格式的文件保存映射关系使用
yaml
格式的文件保存按键和按键对应的动作之间的映射关系,例如:insert:
_: buffer::insert_char
enter: buffer::insert_newline
tab: buffer::insert_tab
backspace: buffer::backspace
up: cursor::move_up
down: cursor::move_down
left: 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`的地方写下如下宏即可:
```rust
pub fn hash_map() -> HashMap<&'static str, Command> {
include!(concat!(env!("OUT_DIR"), "/hash_map"))
}
联想
其实这一套在C语言里面早就有了,使用C语言的宏和 #include
等预处理命令就可以做到这些。但是在高级语言里面使用这种做法,我还是第一次看到,也体会到了 rust
的宏的强大和 build.rs
的方便之处。