最近在看 amp—— 一个受 vim启发的、由 Rust编写的文本编辑器——的源代码时,在看 key mapping的部分的时候,有这几个地方让我印象深刻:

  1. 使用 yaml格式的文件保存按键之间的映射关系,而不是直接写入代码
  2. 使用build.rs在预编译阶段自动生成一些代码,减少了维护成本

    使用 yaml格式的文件保存映射关系

    使用 yaml格式的文件保存按键和按键对应的动作之间的映射关系,例如:

    1. insert:
    2. _: buffer::insert_char
    3. enter: buffer::insert_newline
    4. tab: buffer::insert_tab
    5. backspace: buffer::backspace
    6. up: cursor::move_up
    7. down: cursor::move_down
    8. left: cursor::move_left
    9. # ...

    这样子,这些映射关系就不必写入代码,更加利于维护、更加直观。
    在另一个 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文件:

    1. {
    2. let mut commands: HashMap<&'static str, Command> = HashMap::new();
    3. commands.insert("line_jump::accept_input", line_jump::accept_input);
    4. commands.insert("line_jump::push_search_char", line_jump::push_search_char);
    5. commands.insert("line_jump::pop_search_char", line_jump::pop_search_char);
    6. commands.insert("cursor::move_up", cursor::move_up);
    7. commands.insert("cursor::move_down", cursor::move_down);
    8. commands.insert("cursor::move_left", cursor::move_left);
    9. commands.insert("cursor::move_right", cursor::move_right);
    10. // ...
    11. }

    怎么做到的呢?
    首先,类似这种函数,函数签名必定是很有规律的,例如,amp里面自动生成的 commands的函数签名符合以下的正则表达式:

    1. const COMMAND_REGEX: &'static str =
    2. r"pub fn (.*)\(app: &mut Application\) -> Result";

    这样,就可以轻松地找到某个文件里所有的函数定义。
    例如,以下的代码就是找到 src/commands内的所有文件内签名符合上述正则表达式的函数,并且打印到编译期文件中: ```rust fn write_commands(output: &mut File) -> Result<(), &str> { let expression = Regex::new(COMMAND_REGEX)

    1. .expect("Failed to compile command matching regex");

    let entries = fs::read_dir(“./src/commands/“)

    1. .map_err(|_| "Failed to read command module directory")?;

    for entry in entries {

    1. let path = entry
    2. .map_err(|_| "Failed to read command module directory entry")?.path();
    3. let module_name = module_name(&path).unwrap();
    4. let content = read_to_string(&path)
    5. .map_err(|_| "Failed to read command module data")?;
    6. for captures in expression.captures_iter(&content) {
    7. let function_name = captures.get(1).unwrap().as_str();
    8. write_command(output, &module_name, function_name)?;
    9. }

    }

    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”) }

  1. 最后,只需要在生成 `commands_map`的地方写下如下宏即可:
  2. ```rust
  3. pub fn hash_map() -> HashMap<&'static str, Command> {
  4. include!(concat!(env!("OUT_DIR"), "/hash_map"))
  5. }

即将生成的文件的内容作为这个函数的代码运行。

联想

其实这一套在C语言里面早就有了,使用C语言的宏和 #include等预处理命令就可以做到这些。但是在高级语言里面使用这种做法,我还是第一次看到,也体会到了 rust的宏的强大和 build.rs的方便之处。