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 子文件夹来直接引用。

CovScript 4来啦!从3.4版本开始,ECS将随运行时一同发行

经过了近两年的Preview后,CovScript 4(Extended Covariant Script, ECS)终于逐渐成体系了。目前Cov4主要是由三部分组成,分别为编译器(ecs)、语法扩展(*.ecsx)和支持库(ecs, ecs_parser, ecs_generator, ecs_bootstrap)。事实上,整个CovScript 4的生态都构建在CovScript 3的基础上。

由于CovScript 4并不是刚需,因此为了降低运行时安装包的体积,我们采用了在运行时中附带一个简单的ECS启动器,这个启动器将扫描当前环境中是否存在ECS Bootstrap环境,只有通过CSPKG安装了这个环境才会启动真正的ECS编译器。

安装ECS Bootstrap环境的方法也很简单,只需要在命令行中运行:

> cspkg install ecs_bootstrap --yes

通过CSPKG部署ECS的好处有很多。除了上面提到的原因外,由于ECS目前依旧处于Preview状态,通过CSPKG部署还能大大降低用户更新的复杂度,当我们发布补丁时,用户只需要在命令行中运行下面命令即可更新本地的支持库:

> cspkg upgrade --yes

希望大家喜欢~

关于Modern C++中Universal Reference的一些理解和谬误

Universal Reference只是一种技巧,并不是一种C++中内建的语法糖。实际上所谓Universal Reference本身只是模板推导、引用折叠以及类型退化共同构成的一种编程范式。

通用引用是否就是形如T&&的引用?

不是。通用引用成立的非常重要的条件之一就是模板类型推导。T必须是一个独立的、构成推导条件的模板类型参数。比如:

通用引用正确示例

template<typename T>
void uniref(T &&dat) {
// TODO
}

通用引用错误示例

template<typename T>
struct test {
void uniref(T &&dat) {
// TODO
}
};

通用引用的推导规则是怎样的?

首先需要介绍的概念是引用折叠。引用折叠本身也是存在于模板类型推导的过程中

引用折叠仅存在于使用通用引用承载左值时。大家都知道左值引用的形式为T&,若套用在通用引用的形式中,就会变成(T&)&&,这时外侧的&&会被自动忽略掉。这个行为被称为引用折叠。

其次是类型退化。退化的具体规则比较复杂,我们在这里仅介绍在通用引用中的类型退化。事实上之所以推荐在传递参数时使用通用引用,就是因为通用引用中发生的类型退化几乎是所有传参方式中最小的。

当通用引用承载右值时,注意这里是右值而不是右值引用,T&&会退化为平凡参数,或者说是退化为传值(Pass by value)。

最后是最简单的类型匹配。对于本身就是右值引用的参数,这时发生的是最简单的类型匹配。如传入int&&,通用引用的T&&将构成对这个类型的全匹配,因此不会发生任何多余的修饰。

也就是说,通用引用的推导规则可总结为下表:

参数左值左值引用右值右值引用
修饰折叠折叠退化全匹配
T&&T&T&TT&&

常见谬误

通用引用能保留参数的原有类型

并不能,具体可见上表中的推导规则。通用引用常用于实现完美转发,而完美转发依赖std::forward对类型进行还原。

若不进行还原,对于传入的右值很容易被判定为左值。这是传值带来的附加效果。事实上这里在函数返回之前的确是一个左值。

右值引用就是通用引用

长得像罢了

更多问题,可以在评论中回复。

使用CNI组成宏编写CovScript扩展

概念介绍

CNI是什么?

CNI,全称为C++ Native Interface,是Covariant Script与C++之间进行交互的抽象接口,几乎支持所有能被调用的东西

CNI组成宏是什么?

CNI组成宏是CNI标准扩展的一部分,旨在用简单易用的宏定义代替繁杂的 CNI 声明

CNI标准扩展又是什么?

C/C++ Native Interface Standard Extension,即CNI标准扩展,是Covariant Script解释器扩展的一部分,旨在降低中低复杂度的Covariant Script语言扩展的编写难度

让我们开始吧

必需头文件

#include <covscript/dll.hpp>

Covariant Script扩展头文件

#include <covscript/cni.hpp>

CNI标准扩展头文件

CNI根名称空间(CNI Root Namespace)

在CNI标准扩展中,我们首先需要接触到的第一个概念就是CNI根名称空间

Covariant Script在引入扩展时,会将扩展中的内容映射为Covariant Script语言中的名称空间,因此CNI根名称空间就是这种映射在C++中的表现

事实上,由于需要建立C++与Covariant Script扩展系统之间的联系,CNI根名称空间会自动完成一些必需工作。因此,我们几乎所有的操作,都必须在CNI根名称空间中进行

声明CNI根名称空间

CNI_ROOT_NAMESPACE {
    // C++代码
}

类似于C++的名称空间声明,在关键的CNI_ROOT_NAMESPACE宏后只需紧跟花括号组成的语句块即可

CNI根名称空间对应着C++中的名称空间名cni_root_namespace

CNI函数(CNI Function)

CNI函数名字叫做函数,实际上是一种C++函数到Covariant Script函数的映射

CNI函数是CNI组成宏的一部分,可以说是整个CNI标准扩展中唯一一个真正和CNI产生联系的部分,其他部分只能说是在配合CNI的工作

CNI系统是Covariant Script扩展系统的多个子系统之一,其功能为构建CNI抽象层以完成Covariant Script函数到C++函数调用的转换工作,具体原理我们这里就不再深究

声明CNI函数

// 声明C++函数
CNI(函数名)

我们只需要先在当前名称空间(必须是CNI名称空间)中声明C++函数,然后再在其后追加一句CNI组成宏,即可完成CNI函数的声明

Hello,world!

到这里我们已经介绍了最基础的CNI组成宏用法,下面就是喜闻乐见的实践环节了

示例代码

// CovScript头文件
#include <covscript/dll.hpp>
#include <covscript/cni.hpp>
// STL输入输出库
#include <iostream>
// CNI根名称空间
CNI_ROOT_NAMESPACE {
    // C++函数声明
    void test() {
        std::cout << "Hello,world!" << std::endl;
    }
    // CNI函数声明
    CNI(test)
}

结合我们之前介绍的知识,能不能看懂这段代码呢?

编译为Covariant Script扩展

由于开发环境的多样,我们这里不再阐述Covariant Script SDK的安装方法

本教程基于基于Debian Linux的发行版,使用Windows的朋友推荐先使用Windows Subsystem Linux进行开发

首先是安装SDK,请在root环境中运行以下代码或使用sudo执行

# wget http://mirrors.covariant.cn/covscript/covscript-amd64.deb
# dpkg -i ./covscript-amd64.deb

然后,切换到工作目录,即你的代码所在目录,执行以下指令

# g++ -std=c++14 --shared -fPIC -O3 源代码.cpp -o 扩展名.cse -lcovscript

执行完毕后,会在当前目录生成一个文件名为”扩展名.cse”的Covariant Script扩展,接下来执行Covariant Script REPL验证

# cs_repl
Covariant Script Programming Language Interpreter REPL
Version: 3.2.0 Psephurus gladius(Stable) Build 8
Copyright (C) 2019 Michael Lee. All rights reserved.
Please visit http://covscript.org/ for more information.
> import test
> test.test()

程序应在终端输出“Hello,world!”,如下图

这便代表我们的扩展编写正确,Covariant Script调用了扩展中的C++函数

模板元编程之类型萃取(Type Traits)中匹配规则的推导方法

现我们希望在编译期判定

  1. 一个类型是否为常量类型
  2. 一个类型是否为引用类型
  3. 一个类型是否为模板类型

那么如何去做呢?

首先是写出泛匹配,泛匹配的结果一定为失败(false)。在本例中的三个需求都是针对单一类型的萃取,所以泛匹配只需要写一个参数即可:

template<typename> struct is_xxxx {
static constexpr bool result = false;
};

在这里,模板参数的参数名被省略,只需把xxxx改成对应的易读名即可,如is_constant_type、is_reference_type、is_template_class。所有的单一类型萃取都可以使用这个泛匹配,只需根据需要更改result的形式即可。比如在这里我们只需要判断是不是某种类型,所以我们使用bool,相应的特化匹配也只需要写一个即可。

接下来就要开始写特化匹配了。特化匹配一般比较难写,我们在这里分成四步:

  1. 写出最终要匹配的类型的一阶推导式
  2. 补齐对应的参数名和相关语法
  3. 写出对应的模板参数列表
  4. 完成特化匹配

一:写出最终要匹配的类型的一阶推导式

推导式的形式一般是:

元素 -> 语法描述

那么推导式的作用就是描述使用“语法描述”分解“元素”的方式,我们将这种方式成为文法。编译器会根据推导式描述的文法对程序进行解析,这种解析的过程称为匹配。

一阶推导式仅涉及到一次匹配,一般比较好写,但以后为了解决更复杂的问题我们可能要写高阶推导式甚至递归推导式,其中递归推导式涉及到消除无限递归的问题,我们以后会讲。

在这里,对应三个例子,有三个一阶推导式:

  1. ConstT -> const T
  2. RefT -> T& | T&&
  3. TemplateClass -> T<…>

注意,“…”指的是任意参数。后两个一阶推导式理论上是递归推导式,但我们不关心接下来的递归结果,所以也可以看作一阶推导。

问题:请列出下面几个类型符合的推导式以及推导结果

  1. const int&
  2. const std::map<std::string, std::size_t>

答案:

  1. 符合ConstT和RefT两个推导式,推导结果分别是T=int&、T=const int
  2. 符合ConstT和TemplateClass两个推导式,推导结果分别是T=std::map<std::string, std::size_t>、T=const std::map

二:补齐对应的参数名和相关语法

一般来说,需要补齐的有以下几个情况:

  1. 任意参数,需要用模板参数包表示出来
  2. 嵌套匹配,需要加上传递给嵌套匹配的参数

在这里需要补齐的只有第三个,补齐后的形式为:

TemplateClass  ->  T<X...>

三:写出对应的模板参数列表

这一步就比较简单,只需按照模板参数的语法规则对照写就可以了:

  1. template<typename T>
  2. template<typename T>
  3. template<template<typename…ArgsT>class T, typename…X>

其中,第三个模板参数列表还可以简化:

template<template<typename...>class T, typename...X>

为什么可以简化?因为这里ArgsT并没有实际意义,仅为了标记模板类参数的模板参数列表。

四:完成特化匹配

到这里我们只需要将我们完成的部分填入下面的模板:

模板参数列表 struct is_xxxx<推导式语法描述> {
static constexpr bool result = true;
};

在这里,既然是特化匹配,匹配结果必然是成功(true);除了填入完成的部分,还要把xxxx改成与泛匹配对应的易读名;像第二个判断类型是否为引用的特化匹配因为存在两个情况所以需要写两个特化匹配。

所以我们最终完成的代码如下:

// 一个类型是否为常量类型
template <typename T>
struct is_constant_type
{
static constexpr bool result = false;
};
template <typename T>
struct is_constant_type<const T>
{
static constexpr bool result = true;
};
// 一个类型是否为引用类型
template <typename T>
struct is_reference_type
{
static constexpr bool result = false;
};
template <typename T>
struct is_reference_type<T &>
{
static constexpr bool result = true;
};
template <typename T>
struct is_reference_type<T &&>
{
static constexpr bool result = true;
};
// 一个类型是否为模板类型
template <typename T>
struct is_template_class
{
static constexpr bool result = false;
};
template <template <typename... ArgsT> class T, typename... X>
struct is_template_class<T<X...>>
{
static constexpr bool result = true;
};

此代码符合C++11标准