Inspirer

Rust 实现动态库加载和基于此功能实现的插件管理

最近开发后端 UCenter 服务,考虑到该服务核心逻辑变动相对少,稳定性要求高,单点处理能力要强,且 IO 瓶颈较少(大多缓存),有较多的校验和加解密工作,因此需要使用性能更为强劲的语言,最终考虑使用 Rust(无 GC、内存安全、性能与 C/C++ 差距不明显)以及其最近风头正劲的基于 Actor 模型的 Web 框架:Actix-web。对于框架和语言的介绍我会另起文章,先说说这个用户中心服务。

用户中心服务提供平台下属多个应用的统一授权工作,无论是第三方登录授权(如基于 OAuth 的授权或对外部以 OAuth 支持)、SSO(Single Sign-On)以及解决跨应用用户数据共享、互通等。虽然服务核心逻辑十分稳定,但对于各类子应用接入会有较多的拓展需求,例如不同应用的专用数据表的访问、多协议适配等。对于动态语言或存在虚拟机的语言而言,动态库加载相对简单,但对于 Rust 这种静态和无(或极小)运行时语言,拓展则相对困难。不过 Rust 提供了 FFI(Foreign Function Interface)支持,我们则利用这个实现我们的需求。

一个关键的库

虽然 Rust 提供了 FFI,拥有调用 ABI(Application Binary Interface)的能力,但我们肯定不是在这里仅仅引入一个确定的动态链接库,而是一个能够动态根据配置或自动读取目录列表自动加载链接库的功能,因此,我们需要使用这个库:libloading(https://crates.io/crates/libloading) ,这个库提供了动态加载动态链接库(Dynamical Library)的能力,其原理是利用了不同系统提供的 API 实现的,例如 Windows 环境下,则通过 Win32 API GetProcAddress 实现动态加载。

该库提供的文档很清晰地展示了其用法,十分简单直接:

extern crate libloading as lib;

fn call_dynamic() -> lib::Result<u32> {
    let lib = lib::Library::new("/path/to/liblibrary.so")?;
    unsafe {
        let func: lib::Symbol<unsafe extern fn() -> u32> = lib.get(b"my_func")?;
        Ok(func())
    }
}

知道了如何动态加载库,我们就可以进行下一步。

定义 Trait 和插件拓展接口函数

由于 ABI(Application Binary Interface) 支持的类型有限,我们不能将整个拓展所有细节定义于 ABI 上,例如我们难免在系统中需要表现力丰富的枚举类型(例如及其常见的 Result<T, E> 以及 Option<T>)和一些必要的结构体,更重要的是,我们不希望有太多地方脱离了 Rust 的安全检查,除了引入外部接口必要的 unsafe 代码以外,最好一切都在掌控之下。

因此我们最好的做法就是通过一个 拓展接口函数 创建并返回一个实例(指针),该实例是一个已知 Rust Trait 的实现,在主服务拓展管理器中将实例指针引入转换为提供安全检查的容器里后,最终我们只与这个实例打交道,唯一和外部接触的,就是那个 拓展接口函数

我们需要先定义一个 Trait(下述代码示例包括一些私有定义,若希望自己实现可参考调整为自己的实现):

use std::any::Any;
use ucenter::UcenterResult;
use ucenter::socialite::SocialUserInfo;
use ucenter::database::Conn;

pub trait UcenterApp: Any + Send + Sync {
    /// 获取扩展名称
    fn name(&self) -> &'static str;

    /// 创建或更新用户
    fn create_or_update_user(&self, conn: &Conn, guid: u32, userinfo: SocialUserInfo) -> UcenterResult<u32>;

    /// 当拓展被加载时触发该事件
    fn on_extend_load(&self) {}
}

上述代码中,我们定义了三个方法,所有的拓展都必须要实现这第一个和第二个方法(on_extend_load 包含默认实现)。此时我们就可以实现一个拓展了,以下是拓展的实现代码(不保证正确,仅参考):

#[macro_use] extern crate serde_derive;
#[macro_use] extern crate ucenter;
#[macro_use] extern crate diesel;

use ucenter::UcenterResult;
use ucenter::socialite::SocialUserInfo;
use ucenter::database::Conn;

#[derive(Serialize, Deserialize, Debug, Clone, Insertable)]
#[table_name = "app_qa_system_users"]
pub struct CreateUserFromSocialite {
    pub guid: u32,
    pub wechat_openid: String,
    pub wechat_unionid: Option<String>,
    pub nickname: Option<String>,
    pub avatar: Option<String>,
}

#[derive(Deserialize, Serialize, Debug, Clone, Queryable)]
pub struct UserBaseDisplay {
    pub id: u32,
    pub global_user_id: u32,
    pub internal_user_id: Option<u32>,
}

/// 实现了 Default Trait,可以通过 QASystemExtend::default() 创建
#[derive(Default, Debug)]
pub struct QASystemExtend;

impl UcenterApp for QASystemAppExtend {
    fn name(&self) -> &'static str {
        "qa-system"
    }

    fn create_or_update_user(&self, conn: &Conn, g_uid: u32, userinfo: SocialUserInfo) -> UcenterResult<u32> {
        use ucenter::schema::app_qa_system_users::dsl::*;

        let result: bool = select(exists(app_qa_system_users.filter(guid.eq(g_uid))))
            .get_result(conn)
            .map_err(map_database_error("app_qa_system_users"))?;

        if !result {
            let create = CreateUserFromSocialite {
                guid: g_uid,
                openid: userinfo.id,
                unionid: Some(userinfo.unionid),
                nickname: userinfo.nickname,
                avatar: userinfo.avatar
            };

            diesel::insert_into(app_choujiang_users)
                .values(&create)
                .execute(conn)
                .map_err(map_database_error("app_qa_system_users"))?;

            let generate_id = last_insert_id!(conn, "app_qa_system_users") as u32;

            return Ok(generate_id);
        }

        let user = find_by_id!(conn => (
            app_qa_system_users((id, global_user_id, internal_user_id)) global_user_id = g_uid => UserBaseDisplay
        ))?;

        Ok(user.id)
    }
}

Ok,完工,现在我们需要定义 拓展接口函数 以供扩展管理器加载这个扩展:

pub extern "C" fn _app_extend_create() -> *mut ucenter::UcenterApp {
    // 创建对象
    let object = QASystemAppExtend::default();
    // 通过 Box 在堆上存储该对象实例
    let boxed: Box<ucenter::UcenterApp> = Box::new(object);
    // 返回原始指针(这是个 unsafe 调用,不过在 extern "C" 这种 ABI 定义处整个代码段都处于 unsafe 下,所以不用额外写 unsafe)
    Box::into_raw(boxed)
}

最后,我们需要在 Cargo.toml 处告知编译器,将其编译为动态链接库而非静态库,在 Cargo.toml 添加下述内容:

[lib]
crate-type = ["cdylib", "rlib"]

编译后,我们就得到了一个动态链接库(.so 文件或 .dll)。

拓展管理器的实现

拓展管理器我们要做的其实不多,加载的部分我们已经能够通过开头的 libloading 示例代码看出。不过我们需要一个整体的实例作为拓展管理器,我们在这个实例中需要记录以下内容:

  • 拓展所在目录 ,便于提供批量自动加载
  • 已加载的 Library 实例,用以后续管理操作
  • 已加载的拓展实例,这个不用说,这个数据应该是一个 HashMap<String, Arc<Box<T: Extend>>> 大致这样的结构。我们既可以通过 HashMapget 去获取指定拓展(由于需要通过 clone 获取所有权而非借用,且有跨线程需求,因此使用 Arc,若还有 Mutable 需求 mut 则还需要配合更多如 Mutex),亦可以遍历整个列表。
pub struct AppExtendManager {
    path: String,
    extends: HashMap<String, Arc<Box<UcenterApp>>>,
    loaded_libraries: Vec<Library>,
}

impl AppExtendManager {
    pub fn new(path: String) -> AppExtendManager {
        AppExtendManager {
            path,
            extends: HashMap::new(),
            loaded_libraries: Vec::new(),
        }
    }

    /// 加载指定的扩展
    pub unsafe fn load_extend<P: AsRef<OsStr>>(&mut self, filename: P) -> UcenterResult<()> {
        // 构造函数(拓展接口函数)签名
        type ExtendCreator = unsafe fn() -> *mut UcenterApp;

        // 加载动态库
        let lib = Library::new(filename.as_ref())
            .or(Err(UcenterError::system_extend_dynamical_error(
                Some(UcenterErrorDetail::String("Cannot load extend.".into()))
            )))?;

        self.loaded_libraries.push(lib);

        let lib = self.loaded_libraries.last().unwrap();
        // 取得函数符号
        let constructor: Symbol<ExtendCreator> = lib.get(b"_app_extend_create")
            .or(Err(UcenterError::system_extend_dynamical_error(
                Some(UcenterErrorDetail::String("The `_app_extend_create` symbol wasn't found.".into()))
            )))?;

        // 调用该函数,取得 UcenterApp Trait 实例的原始指针
        let boxed_raw = constructor();

        // 通过原始指针构造 Box,至此逻辑重归安全区
        let extend = Box::from_raw(boxed_raw);

        // 触发一下扩展的 on load 事件,你也可以定制更多事件接口,在对应地方调用
        // 这块灵活运用即可构造强大的插件机制
        extend.on_extend_load();

        debug!("Extend {} loadded.", extend.name());

        // 加入 Hash map,便于后面再次获取
        self.extends.insert(extend.name().to_string(), Arc::new(extend));

        Ok(())
    }

    /// 选取指定 name 的拓展
    pub fn select<T: Into<String>>(&self, target: T) -> UcenterResult<Arc<Box<UcenterApp>>>
    {
        let key: String = target.into();

        self.extends.get(&key)
            .map(|v| {
                // v 实际是一个 Arc 实例,此处 clone 不会有什么性能开销
                // 且能拿到所有权(只读),Arc 是一个原子性的引用计数智能指针,
                // 因此可以安全跨线程使用。
                v.clone()
            })
            .ok_or(UcenterError::system_subsystem_error(None))
    }
}

我们已经实现了一个基本的拓展管理器,加载的实例对象我们通过 select 或其他方式从我们的 HashMap 中取得,当然也可以自行设计存储容器形式。取得实例即可调用其方法,由于实例是我们已经确定的 Trait 的实现,因此调用时可信的。通过这样的方式就可以将核心逻辑拆分出来,将剩余变动较大的部分交由拓展实现。基于这个原理,实现各种插件机制亦十分容易,仅需在对外部进行调用的阶段谨慎处理(毕竟只有此处存在 unsafe 代码块),剩余部分仍然依托 Rust 强大的静态检查而十分可靠。

由于这个接口是基于 C 的 ABI 实现,因此拓展不仅仅能够使用 Rust 写,还可以使用 C、C++ 开发,不过相对问题较多并不在本文的考虑范围内。

通过宏快速创建拓展接口函数

每次书写 拓展接口函数 是很繁琐的,通过宏则会十分简单,我们定义如下宏:

#[macro_export]
macro_rules! declare_app_extend {
    ($app:ty, $constructor:path) => {
        #[no_mangle]
        pub extern "C" fn _app_extend_create() -> *mut $crate::UcenterApp {
            // 确保构造器正确,所以做了这一步骤,来显示声明签名
            let constructor: fn() -> $app = $constructor;

            let object = constructor();
            let boxed: Box<$crate::UcenterApp> = Box::new(object);
            Box::into_raw(boxed)
        }
    };
}

有了这个宏,我们就可以这样快速创建一个拓展接口函数:

declare_app_extend!(QASystemAppExtend, QASystemAppExtend::default);

至此,我们的工作已经基本完成!

参考内容

Rust FFI Guide - Dynamic Loading & Plugins