Skip to content

Latest commit

 

History

History
145 lines (106 loc) · 7.17 KB

File metadata and controls

145 lines (106 loc) · 7.17 KB

axvisor_api (实验性下一代 Axvisor API)

⚠️这个仓库是实验性的,API的内容和语法可能发生变动⚠️

⚠️这些API可能最终转正,也可能在未来被删除⚠️

⚠️维护者会尽力保持API的兼容性,但 breaking changes 仍可能发生⚠️

为什么需要新一代 API?

Axvisor 的各个部件需要访问 ArceOS 系统提供的功能。对于 Axvisor 本体,ArceOS 是它的依赖项,它可以直接访问 ArceOS 的 API;然而出于解耦合的考虑,其他位于更下层的组件不应该将 ArceOS 作为自己的依赖项,也就不能直接访问 ArceOS 的 API;因此,需要一种“依赖注入”的方式来将 ArceOS 的 API 提供给 Axvisor 的各个部件。

目前 Axvisor 中主要是通过 Trait + 泛型参数来实现“依赖注入”,给各个组件提供 API 的,例如:

// 组件定义自己需要的 API
pub trait ModAHal {
    fn foo() -> u32;
}

pub struct ModA<T: ModAHal> {
    state: u32,
}

impl<T: ModAHal> ModA<T> {
    pub fn new() -> Self {
        Self { state: T::foo() }
    }
}

// Axvisor 提供实现
pub struct ModAHalImpl;

impl ModAHal for ModAHalImpl {
    fn foo() -> u32 {
        42
    }
}

pub fn main() {
    let mod_a = ModA::<ModAHalImpl>::new();
    println!("ModA state: {}", mod_a.state);
}

这种方法的好处是显而易见的:

  1. 非常优雅,完全符合 Rust 的编程范式,没有任何黑魔法,也容易理解;
  2. 耦合性低,理论上可以将任何底层组件移植到任何其他 Kernel 上,只要它们能提供对应的 API 实现;

然而这样的做法也有缺点:

  1. 引用者(结构体或函数)必须带着它使用的所有依赖项(结构体或函数)的泛型参数,这会导致代码冗长,降低可读性;

  2. 不同的 Trait 之间难免有重复的方法,导致代码冗余;

  3. 前两个问题有一个共同的解决方式,那就是对 API 进行分组归类,然而这又会导致 Trait 之间的嵌套和耦合关系增加,例如:

    pub trait MemoryHal {
        // 内存相关的 API
    }
    
    pub trait VCpuHal {
        type Memory: MemoryHal;
        // 虚拟 CPU 相关的 API
    }
    
    pub trait VMHal {
        type VCpu: VCpuHal;
        // 虚拟机相关的 API
    }
  4. 最严重的问题在于,如果位于依赖图末端的某个结构体或方法增加了一个依赖的 API,那么它的所有上游使用者的类型签名都必须修改以适应这个变化,这会导致代码的维护成本大幅增加。

新一代 API 的设计

axvisor_api 旨在解决上述问题。它的设计思路是:

  1. 使用 crate_interface 来定义 API 的接口,并且将 crate_interface 提供的 API 包装成普通的函数;
  2. 以模块为单位组织 API,一个模块对应一个功能方向,对应一个 crate_interface Trait;
  3. 每个模块内部,除了 API 函数的定义以外,还可以包含类型定义、常量和基于 API 函数实现的其他函数等;

axvisor_api 的示例代码如下:

// 定义一个 API 模块
#[api_mod]
mod memory {
    pub use memory_addr::{PhysAddr, VirtAddr};

    /// Allocate a frame.
    extern fn alloc_frame() -> Option<PhysAddr>;
    /// Deallocate a frame.
    extern fn dealloc_frame(addr: PhysAddr);
}

// 实现 API 模块
#[api_mod_impl(axvisor_api::memory)]
mod memory_impl {
    use crate_interface::memory::{alloc_frame, dealloc_frame, PhysAddr};

    extern fn alloc_frame() -> Option<PhysAddr> {
        // 调用 ArceOS 的内存分配函数
        arceos_memory_alloc()
    }

    extern fn dealloc_frame(addr: PhysAddr) {
        // 调用 ArceOS 的内存释放函数
        arceos_memory_dealloc(addr);
    }
}

// 使用 API 模块
use axvisor_api::memory::{alloc_frame, dealloc_frame, PhysAddr};
pub fn main() {
    let frame = alloc_frame().expect("Failed to allocate frame");
    println!("Allocated frame at address: {:?}", frame);
    dealloc_frame(frame);
}

可以说,这是通过一个统一且按功能分类的 API 集合取代了之前的所有 Trait,并且通过 crate_interface 取消了对 Trait 的显式依赖。这样的实现的优势在于:

  1. API 函数的调用方式与普通函数一致,使用起来更简单,降低了使用者的心智负担;
  2. 调用者无需关系其依赖项需要哪些 API;依赖项的修改不会影响调用者;
  3. API 模块可以包含类型定义、常量和其他函数等,提供了更好的组织方式;

同样地,这样的设计也有一些缺点:

  1. 虽然 crate_interface 背后使用 Trait 实现了一定的编译期检查,但是相比于之前的 Trait 方式,编译期检查的能力有所下降;例如如果一个 api_mod 没有被实现,只有在链接时才能发现,而不是编译期;
  2. 这样设计本质上不允许通过不同的 Trait 实现,在同一个程序中为同一个组件提供两种不同的 API 实现;这在一定程度上损失了灵活性;不过这种情况在 Axvisor 中并不常见,目前也没有造成实际问题;
  3. 降低了单个组件直接复用的能力;例如某个组件原本可以直接被其他组件复用,现在需要额外引入 axvisor_api 的依赖;虽然实际上可以通过 feature 来关闭 axvisor_api 中一部分没有用到的 API(目前未实现),但这仍然是一个缺点;

当前仍存在的问题

除了以上提到的缺点,axvisor_api 目前的具体实现仍然存在一些问题:

  1. 不能支持非内联模块:对于最常见的,将模块放置在单独的文件中的方式,axvisor_api 目前还不支持;即定义 #[api_mod] mod x; 然后在 x.rs 中定义 API 模块的方式是无法工作的;这是 Rust 过程宏的能力所限;
  2. 对 IDE 的功能有轻微干扰:由于 axvisor_api 使用了过程宏,可能会导致某些 IDE 的代码补全和跳转功能不如预期;不过目前测试 rust-analyzer 工作比较正常;
  3. extern fn 语法和 rustfmt 的冲突:使用 extern fn 标记 API 函数是出于可读性和语法一致性的考虑,但是 rustfmt 会将其格式化为 extern "C" fn,这会导致编译错误;可能的解决办法包括直接使用 extern "C" fn,但这会和真正的外部 C 函数声明冲突;
  4. API 函数不够醒目:由于 API 函数和普通函数的区别在于 extern 关键字和没有函数体,在大段代码中可能不够醒目;目前通过在生成的文档中给出详细列表的方式,试图弥补这一点;未来可以考虑使用 #[api] 属性来标记 API 函数,以提高可读性;

此外,还有一些问题和 axvisor_api 的设计无关和实现无关,属于无论怎样设计 API 接口都会遇到的问题,此处也列出一条作者能想到的:

  • 平台相关 API:某些 API 是平台强相关,甚至具体设备强相关的,但又是非常必要的。例如,ARM 架构下 GIC 的半虚拟化实现,就与物理 GIC 驱动的实现密切相关,前者需要调用很多后者的功能;将这些功能全部放置于 axvisor_api 中会导致 API 模块过于臃肿;但不统一在一起又可能造成可读性和可维护性下降,容易出错等等问题;