CovScript CNI 杂谈

随着 CovScript 的逐渐发展,其生态目前也已经基本成型,有了作为一门生产级编程语言的潜力。但也有一些朋友在完成一些需求时需要动态加载某些功能,CNI 在这个时候就显得非常有用,因此我决定将 CNI 独立成一个模块,供有特殊需求的朋友使用。

CNI Standalone Edition

CovScript CNI SE:https://github.com/covscript/cni

SE 的意思是 Standalone Edition(独立版),去除了庞大的 CovScript Runtime 和 Interpreter 相关代码,编译出的静态库不到 100k 大小

CovScript SE 包含了两部分功能,分别是 CNI 本身以及 CovScript 扩展系统和 CNI 组成宏。下面分别介绍这三者以及它们之间的关系。

CNI 在最初是独立于 CovScript 的一个部分,其主要作用是通过 C++ 模版元编程相关的技巧在编译期解析一个 C++ 函数的各种信息,比如返回类型、形参列表等,再根据这些信息将这个函数包装为一个 CovScript Callable 对象,其本质是一个返回值为 cs::var,参数为 cs::vector => std::vector<cs::var>。也就是说,CovScript Callable 对象和很多动态语言的 Foreign Function 很相似,只不过从指针满天飞的形式变成相对安全、易读的方式(cs::var 能保证类型安全和资源安全)。

实际上,利用这个特性,CNI 能够用于实现类型擦除,从而能够将各式各样的函数以类型安全、资源安全的形式传递到各处。CNI 本身是个 C++ Callable Class(重载了 operator() 的类),其构造函数能接受任意形式的 C++ 函数。利用这个特性,可以编写如下的代码:

#include <covscript/covscript.hpp>

cs::cni func(foo1);
cs::vector args{1, 2, 3};
func(args);

至于 CovScript 扩展系统和 CNI 组成宏,可以参考这篇博客:https://unicov.cn/2019/05/16/cni-composer-macro/

总的来说,CNI 用于实现类型安全、资源安全的类型擦除,将任意 C++ 函数转换成 CovScript 能识别的类型;CovScript 扩展系统是 CovScript Runtime 能够动态加载其他功能的关键,基于动态链接库实现;CNI 组成宏则是在前两者基础上的一种 API 无关的标准,能用简单易用的方式编写 CovScript 扩展。

目前来说,CovScript 生态中大多数扩展都是基于上面三个系统构建的。也就是说,CovScript CNI SE 能兼容大多数 CovScript 现有扩展,只需要使用 CovScript CNI SE 重新编译就能很方便的引入到自己的应用中,将 CovScript 的能力转换为自己的能力。反之亦然,使用 CovScript CNI SE 打造的扩展也能以极低的成本移植到 CovScript 生态中。

使用 CNI SE

那么就只剩一个问题,如何使用 CovScript CNI SE 加载扩展呢?

下面是一个示例程序:

#include <covscript/covscript.hpp>

int main(int argc, const char **args)
{
    if (argc != 2)
    return -1;
    cs::extension dll(args[1]);
    cs::function_invoker<void(std::string)>
        func1(dll.get_var("print"));
    func1("Hello");
    cs::var func2 = dll.get_var("print");
    cs::invoke(func2, "Hello");
    return 0;
}

在这段程序中展示了两种调用扩展中函数的方式。但在调用之前是将变量提取出来,方法也非常简单,即 dll.get_var,关于这个函数的详细注解如下:

  • dll 是一个 cs::extension 对象,在构造时传入路径,在失败时抛出 cs::runtime_error 异常
  • get_var 方法接受一个 std::string ID,返回 cs::var,在 ID 不存在时抛出 cs::runtime_error 异常
  • cs::runtime_error 异常是 std::exception 的子类

获取到扩展中的函数 func 后,分别可以用 cs::function_invoker 或 cs::invoke 调用。

cs::function_invoker 是一个模版类,需要手动传入目标函数的返回类型和形参列表,能够将一个 cs::var 包装为适合在 C++ 中使用的强类型对象

cs::invoke 是一个模版函数,可以动态的调用 cs::var 形式的 cs::callable 对象,返回 cs::var

一般来说,cs::invoke 更常用,但可能存在 C++ 中类型和 CovScript 中类型不能完全兼容的问题;cs::function_invoker 是 CovScript SDK 提供的基础设施,能对不兼容的函数进行基础的转换,兼容性更好

若要对 cs::var 进行手动操作,可以直接赋值,提取其中的变量可使用 val<T>() 和 const_val<T>() 两种形式。当类型不同或权限错误(比如使用 val<T>() 提取受保护的变量)都会抛出 cov::error 异常

编译 & 兼容性

上面提到,CNI SE 仅是代码级兼容,本质上还是需要重新编译,不算完全兼容。这是因为 CovScript SDK 中有很多 CNI 之外的功能,CNI SE 为了轻量化将这部分全部剥离了,而这部分功能又是 CovScript Runtime 必须的功能,因此没办法做到两者的互相兼容。

那读者可能就要问了,如果想直接二进制兼容呢?

其实这个问题的答案已经浮现在各位眼前了:直接使用 CovScript SDK。

CovScript SDK 和 CovScript CNI SE 形式是一样的,都是通过头文件 + 静态库的方式分发。不同的是 CovScript SDK 不能直接添加为 CMake 的子文件夹来引入到项目中,只能随 CovScript Runtime 一同下载 & 编译,原因是 CovScript SDK 的依赖更复杂,没办法直接通过 CMake 编译。

使用 CovScript SDK 最简单的方式是从官网(https://covscript.org.cn/)上下载,然后将运行时环境所在的位置(Windows:C:\\Program Files (x86)\\CovScript,macOS:/Applications/CovScript.app/Contents/MacOS/covscript,Linux 无需单独设置)设置为环境变量 CS_DEV_PATH,并在 CMakeLists.txt 头部加入以下内容:

if (DEFINED ENV{CS_DEV_PATH})
    include_directories($ENV{CS_DEV_PATH}/include)
    link_directories($ENV{CS_DEV_PATH}/lib)
endif ()

最后再将目标链接至 libcovscript 即可,使用 CNI SE 时也是一样。

至于 CNI SE 本身的编译,可以参考 CNI SE 的 GitHub Repo 中的介绍,可以使用 csbuild 脚本编译,也可以通过将 CNI SE 设置为 CMake 子文件夹来直接引用。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注