问卷反馈收集, 前端脚手架安装向导, rust, gtk3, win32, dll

Overview

scaffold-wizard

这是一款加持了【图形用户界面】的npm - inquirer(名曰:问卷)。即,根据【问卷】配置文件,以人-机交互的形式,收集终端用户的【回答结果】。这里提到的【问卷配置】与【回答结果】都是*.json格式的字符串(或文件)。

【问卷】既能够作为.exe文件被双击运行,也支持作为.dll文件被链接和调用-间接运行。

  • 前者的输入与输出都是.json文件。
  • 后者对外开放了两个C ABI以备调用。
    • 同步接口:char* inquire(char* questions, char* bin_dir, char* log4rs_file)
      • 【问卷配置】以json字符串的形式从第一个形参questions传入。
      • 【回答结果】以json字符串的形式从函数返回值传出。
    • 异步接口:void inquireAsync(char* questions, char* bin_dir, char* log4rs_file, void (*callback)(char* answers))
      • 【问卷配置】以json字符串的形式从第一个形参questions传入。
      • 【回答结果】通过最后一个【回调函数】实参的输入形参,以json字符串的形式异步地传出。

在函数调用期间,会有gnome图形界面被弹出和提示用户输入问题答案。

制作这款工具的动机

我最近花了两个月的业余时间制作【问卷】这款工具的直接冲动来源于:将公司【前端-脚手架安装向导】从·命令行交互·升级为·图形界面互动·的构想。其路线图大约包括:

  • 首先,让整个人-机交互过程更具有表现力;
  • 其次,最好能将【安装向导】改造成为一个“原生GUI平台”,从而在未来添加更多辅助功能。
  • 最终,成为公司技术工具链中重要的一环 --- 目标远大,征程漫长。

后来,我越做这款工具,越是觉得它的·通用性·还是比较高的。其使用场景不应仅只局限于【脚手架-安装过程】的现场配置收集。相反,任何含有【意见咨询】类功能的使用场景都可以考虑使用这款(或这类)工具。而,工具链的后续处理环节,再根据被收集的反馈结果,做定制化的“裁剪”。比如,“裁剪”脚手架内置的工程原型,使其更符合项目要求。

于是,我将这款工具从“脚手架-安装向导”更名为“问卷”。同时,它“下一步”再“下一步”的使用风格真心地相像于传统的windows应用软件的【安装向导】。【情怀】--- 在我认知体系中的任何软件安装都应该是“下一步”再“下一步”...最后“完成”;并且,其步骤越多,越有仪式感。

另一方面,在【rust桌面应用】方向投入更多业余精力也符合我个人对掌握rust技术栈的成长规划。即,

  • rust + wasm入门。作为入门,这个“接入端”算是门槛比较低的了。
  • rust桌面编程领域进阶。毕竟,wasm是一个严重受限的技术平台,许多rust高级语言特性,还有rust生态一多半的crate都没有用武之地。这严重地制约了我对rust技术栈的想像力与领悟层次。而转向rust Iron则很不明智。因为,
    • 就诸多后端解决方案而言,rust相对于go并没有绝对优势,生存空间极为狭小。同时,rust还得受着来自javarubyphppython的冲击。
    • 愣头青地和既得利益【团体】正面抢生存空间不利于团队的团结,我的领导也不会对我满意的。
    • 我掌握新技术的初衷是提高个人岗位竞争力,不是找挨虐的
  • 最后我的愿景是:在IoT嵌入式设备上“开花结果”。这对rust技术栈本身来说真不是问题。它已经一次又一次地证明其实力。愿景的实现主要还是看我对rust的掌握能够达到什么水平。

综上所述,实践rust的务实路径:wasm -> Native GUI App -> IoT嵌入式编程。使用rust做一些GC类语言想做,而做不好的事。

即便作为是一名懒惰的程序员,我也得掌握两个计算机语言

  • GC类精通一门(一般说是“高级计算机语言”)
  • GC类掌握一门(通常认为是“系统计算机语言”)

前者中佼佼者䊨在:“铺得面广+无处不在”,解决“温饱”问题;后者中“剩者”的立足点是:“足够地快+内存安全”,解决“小康”问题。我要是能达到这个目标,那可真是:“中年危机远离我”。

技术

简单地讲,rust写业务逻辑 + gtk组件库画界面。

依赖说明

  • clap
    • 解析命令行参数input-fileoutput-filelog4rs-file
    • 用法还算是高级,给clapyaml配置文件,而不是在代码里攒【解析树】。
  • eval
    • 在运行时,根据上下文,求值【问卷配置】中when表达式。“给表达式求值”的功能真像javascript里的eval函数,但没那么强大。我也绝不想在这个小工具里集成一个JavascriptCore引擎。实在太重了
    • when表达式的求值结果决定了一个【问题】是否出现在图形界面的交互流程内。
  • loglog4rs
    • 日志记录
    • 大家对log4**家族里的其他成员一定很熟悉。比如,log4jlog4js
  • quick-xml
    • 解析SGML格式的Glade布局文件。将布局文件内,对外部资源(主要是图片)的相对引用地址都改成运行时计算得出的绝对路径。这样,无论你以何种方式启动.exe文件,被引用的外部文件都能够被正确地找到。
  • serde_json
    • 解析与输出JSON格式的【问卷配置】输入内容与【回答结果】输出内容。
  • gdk-pixbuf, gio, glib, gtk
    • 这些都是Gnome.gtk3rust binding。其功能可类似于C里的【头文件】。

毕竟,【问卷】功能单一,所以用到的第三方依赖项不多。此外,

  • 在类Linux操作系统上,需要GnomeGtK版本>= 3.24
  • windows操作系统上,绿色安装包需要自带gtk动态链接库与资源文件的“家什儿”。

开发环境搭建

不熟悉rust + gtk + win32技术栈的小伙伴儿请移步我的另一篇技术分享:为 Rust 原生 gui 编程,搭建 win32 开发环境

rustup工具链版本

鉴于之前使用rust + wasm完成【网络加密通讯】功能的踩坑经验,我这次显示地将package绑定了适用的rustup版本nightly-2021-03-25-x86_64-pc-windows-gnu。若你的本地rustup安装版本与之不匹配,请根据编译的报错信息,rustup install ***正确的rustup toolchain版本。就开发环境而言,对非windows用户不友好了,实在对不住。

构建

cargo buildcargo build --release

输出两个关键结果

  • bintarget\debug\scaffold-wizard.exe --- 可执行文件
  • libtarget\debug\scaffold_wizard.dll --- C动态链接库cdylib
    • 注意:不是默认的rust动态链接库dylib。在编译期间,它幼稚地试图将所有被链接到DLL文件都静态编译入一个结果DLL文件内。这“理想主义”作法直接造成了单个DLL导出public ABI数量超出上限的编译错误。

scaffold-wizardcargo new --bincargo new --lib的混合体。

cargo test

执行针对cdylib的单元测试。还没有添加【集成测试】与【基准测试】。

cargo run

  • 编译rust源码,和输出target\debug\scaffold-wizard.exe
  • msys2包管理器的环境下,运行target\debug\scaffold-wizard.exe

node build.jsnode build.js --release

这里执行js程序有点突兀。但,它是被用来攒“绿色安装包”的。安装包的目录结构如下

.
├─ bin    # 若 windows 发行包,此目录需要包括 41 个 dll/exe 文件。若 Linux 发行包,仅 1 个 exe 文件。
|  ├─ ...
│  ├─ scaffold-wizard.exe # 仅出现在 target/setup-bin 目录下
|  ├─ ...
│  └─ scaffold_wizard.dll # 仅出现在 target/setup-lib 目录下
├─ lib    # 仅 windows 发行包需要此目录
│  └─ gdk-pixbuf-2.0
├─ share  # 仅 windows 发行包需要此目录
│  ├─ glib-2.0
│  └─ icons
├─ assets
│  ├─ prompt-manifest.json # 【问卷配置】样板文件
│  ├─ log4rs.json          # 日志配置文件
│  └─ images               # 自定义组件的图片
└─ logs   # 运行时滚动日志输出目录。

如上所述,要攒这么复杂的目录结构,使用javascript编写构建程序绝对是省时省力的明智选择。

npm i -g archiver
node build.js

上面的命令执行之后,其会在target目录下,创建两个子文件夹和两个zip文件

  • setup-binscaffold-wizard.setup-bin.zip --- 独立执行程序和其绿色安装包
  • setup-libscaffold-wizard.setup-lib.zip --- 动态链接库和其绿色安装包

双击运行“绿色安装包”内的bin/scaffold-wizard.exe。便可,在msys2包管理器环境之外,运行应用程序。同理,“绿色安装包”内的scaffold_wizard.dll也能够脱离msys2地被链接调用。但要稍稍再复杂一些。

build.rs

每当执行cargo指令时,这个构建程序也都会被执行。在target目录下,它会创建若干指向msys2的符号链接。所以,强调:环境变量MSYS2_HOME需要被配置,编译才能被正常地执行。

  • 环境变量MSYS2_HOME保存了msys2的安装目录地址。

输入/输出说明

可执行文件的命令行参数

前端脚手架安装向导 1.0
张浩予 
以【问卷】的形式,收集开发者对前端工程原型的“裁剪”条件信息

USAGE:
    scaffold-wizard.exe [OPTIONS]

FLAGS:
    -h, --help       Prints help information
    -V, --version    Prints version information

OPTIONS:
    -i, --input-file    【问卷配置】 json 文件(包括:题面,选项,默认值)。
                                    缺省此参数会弹出【文件选择对话框】要求你临时选择一个 json 文件。
    -l, --log4rs-file     JSON 格式的 log4rs 配置文件。忽略此参数,程序会试从
                                    (1).exe 文件所在的同级目录
                                    (2)程序被执行的工作目录
                                    寻找 ../assets/log4rs.json 文件。若两处都没有配置文件,
                                    程序日志功将不会被开启。
    -o, --output-file  【问卷】的答案清单 json 文件。默认输出文件是 answers.json。
                                    并且,输出文件会被放置于与输入文件(--input-file)相同的目录里。

【问卷配置】json文件

它全方位地抄袭了Inquirer 的 Question 部分。但是,【回调函数钩子】那块,我是实在抄袭不来,原因包括:

  • 第一,我自己不会做定制而精简的“脚本程序”词法分析与执行器。
  • 第二,集成JavascriptCore引擎又太重了。

所以,现在阶段,我暂停点开这个方向的“科技树”。

另一方面,作为对缺失【回调函数钩子】的补偿,我在如下几处添加了新配置属性:

  1. "type": "input"类型(即,文本输入框)添加了"subType": "port"子类。其专门收集【数字类型】,取值范围在1000 ~ 99999的端口号。样板配置如下:

    {
        "appPort": { // 这个问题唯一标识字符串。相当于主键 ID。
            "when": "subprojects.app", // 条件表达式,当前问题是出现在交互流程中(true),还是被跳过(false)。
            "type": "input", // 文本输入框
            "subType": "port", // 之端口数字输入框
            "message": "请输入 移动端 webpack dev server 监听端口号", // 题面
            "required": true, // 是否必填
            "default": 9010 // 默认值
        },
    }
  2. "type": "list"类型(即,单选题)的每一个单选项添加了when(布尔)表达式。从而,根据上下文内容,动态地决定当前单选项是否被显示出来。样板配置如下:

    {
        "compUiLib": { // 这个问题唯一标识字符串。相当于主键 ID。
            "when": "subprojects.component", // 条件表达式,当前问题是出现在交互流程中(true),还是被跳过(false)。
            "type": "list", // 单选题
            "message": "请选择 基于哪款【UI 组件库】做二次开发实现组件", // 题面 - 标题
            "choices": [{ // 题面 - 单选项1
                "name": "不使用UI组件库", // 【显示用】完整名
                "short": "", // 【显示用】简称名 - 暂时尚未使用
                "value": "none" // 【程序引用】此选项的唯一标识字符串
            }, { // 题面 - 单选项2
                "when": "compWhichEnd == 'pcBrowser'", // 此选项是否出现的`when`表达式
                "name": "Element UI", // 【显示用】完整名
                "short": "Element", // 【显示用】简称名 - 暂时尚未使用
                "value": "elementUI" // 【程序引用】此选项的唯一标识字符串
            }, { // 题面 - 单选项3
                "when": "compWhichEnd == 'mobileBrowser'", // 此选项是否出现的`when`表达式
                "name": "Vant", // 【显示用】完整名
                "short": "vant", // 【显示用】简称名 - 暂时尚未使用
                "value": "vant" // 【程序引用】此选项的唯一标识字符串
            }]
        },
    }
  3. "type": "checkbox"类型(即,多选题)的每一个多选项添加了mutex: boolean属性。"mutex": true表示该选项具有排它性。若其被选中,则该选项只能被单选。样板配置如下:

    {
        "subprojects": { // 这个问题唯一标识字符串。相当于主键 ID。
            "type": "checkbox", // 多选题
            "message": "请选择 工程类型", // 题面 - 标题
            "required": true, // 是否必填
            "choices": [{ // 题面 - 多选项1
                "name": "PC浏览器-管理界面", // 【显示用】完整名
                "short": "中后台", // 【显示用】简称名 - 暂时尚未使用
                "value": "admin", // 【程序引用】此选项的唯一标识字符串。比如,subprojects.admin
                "checked": false // 初始选中状态
            }, { // 题面 - 多选项2
                "name": "本地 H5 插件", // 【显示用】完整名
                "short": "移动插件", // 【显示用】简称名 - 暂时尚未使用
                "value": "app", // 【程序引用】此选项的唯一标识字符串。比如,subprojects.app
                "checked": false // 初始选中状态
            }, { // 题面 - 多选项3
                "name": "组件/模块/微前端应用", // 【显示用】完整名
                "short": "组件/模块/微前端", // 【显示用】简称名 - 暂时尚未使用
                "value": "component", // 【程序引用】此选项的唯一标识字符串。比如,subprojects.component
                "checked": false, // 初始选中状态
                "mutex": true // 是否为单选
            }, { // 题面 - 多选项3
                "name": "RUST 语言 WEB 字节码 NPM 模块", // 【显示用】完整名
                "short": "RUST + WASM + NPM", // 【显示用】简称名 - 暂时尚未使用
                "value": "wasm", // 【程序引用】此选项的唯一标识字符串。比如,subprojects.wasm
                "checked": false, // 初始选中状态
                "mutex": true // 是否为单选
            }, { // 题面 - 多选项4
                "name": "RUST 语言原生 GUI 应用", // 【显示用】完整名
                "short": "RUST + GTK3 APP", // 【显示用】简称名 - 暂时尚未使用
                "value": "rust_gui", // 【程序引用】此选项的唯一标识字符串。比如,subprojects.rust_gui
                "checked": false, // 初始选中状态
                "mutex": true // 是否为单选
            }]
        },
    }

【回答结果】json文件

首先,它会被输出至和输入文件相同的文件夹内。

其次,它全方位地抄袭了Inquirer 的 Answers 部分

最后,补充说明:

  • "type": "checkbox"类型题面对应的答案类型是Map

调用·动态链接库

  • 直接贴nodejs代码
  • 在程序注释里,解释每个参数与返回值的用途

注意:

  • 在链接与调用DLL时,请保持target\setup-lib文件夹内的目录结构。
  • windows操作系统上,因为C:\Windows\System32目录下的zlib1.dllGnome.GTK3依赖的zlib1.dll名字冲突了。所以,为了让【问卷】DLL能够正常地运行,需要(无论是手动、还是程序自动)复制.boilerplate\bin\zlib1.dllnode安装目录的根目录(即,node.exe所在的文件夹)。

同步接口调用

const fs = require('fs');
const ffi = require('ffi');
const ref = require('ref');
const path = require('path');
const util = require('util');
// 准备【问卷配置】`json`文件
const homeDir = path.resolve('target/setup-lib');
const questionsFile = path.join(homeDir, 'assets/prompt-manifest.json');
const readFile = util.promisify(fs.readFile);
readFile(questionsFile, {encoding: 'utf8'}).then(questions => {
    // 加载 DLL
    const dllFile = path.join(homeDir, 'bin/scaffold_wizard.dll');
    const dllDir = path.dirname(dllFile);
    const scaffoldWizard = ffi.Library(dllFile, {
        inquire: ['string', ['string', 'string', 'string']],
        inquireAsync: ['void', ['string', 'string', 'string', 'pointer']]
    });
    // 调用 DLL
    // inquire(...) 一共有三个输入参数
    // (1) JSON 格式字符串,包括了【问卷配置】
    // (2) 被加载 DLL 文件所在的目录。以此,来寻找 assets\images 目录。
    // (3) log4rs 的配置文件路径。传一个空指针,表示关闭日志功能。
    // 输出返回值是 JSON 格式字符串,包括了【回答结果】
    const answers = scaffoldWizard.inquire(questions, dllDir, ref.NULL_POINTER);
    console.info('被收集的答案包括', answers);
});

异步接口调用

const fs = require('fs');
const ffi = require('ffi');
const ref = require('ref');
const path = require('path');
const util = require('util');
// 准备【问卷配置】`json`文件
const homeDir = path.resolve('target/setup-lib');
const questionsFile = path.join(homeDir, 'assets/prompt-manifest.json');
const readFile = util.promisify(fs.readFile);
readFile(questionsFile, {encoding: 'utf8'}).then(questions => {
    // 加载 DLL
    const dllFile = path.join(homeDir, 'bin/scaffold_wizard.dll');
    const dllDir = path.dirname(dllFile);
    const scaffoldWizard = ffi.Library(dllFile, {
        inquire: ['string', ['string', 'string', 'string']],
        inquireAsync: ['void', ['string', 'string', 'string', 'pointer']]
    });
    // 调用 DLL
    // inquire(...) 一共有三个输入参数
    // (1) JSON 格式字符串,包括了【问卷配置】
    // (2) 被加载 DLL 文件所在的目录。以此,来寻找 assets\images 目录。
    // (3) log4rs 的配置文件路径。传一个空指针,表示关闭日志功能。
    // 输出返回值是 JSON 格式字符串,包括了【回答结果】
    scaffoldWizard.inquireAsync(questions, dllDir, ref.NULL_POINTER, ffi.Callback('void', ['string'], answers => {
        console.info('被收集的答案包括', answers);
    }));
});

N-API封装

即将到来。

  • 正在阅读N-API相关文档(主要是Rust Binding的内容)。应该不难。
  • 但是,N-API也有不足,其对node 10之前的版本不兼容。

Neon封装

即将到来。

执行演示

运行这款工具分发包的最简单方式就是:

  1. 双击target\setup-bin\bin\scaffold-wizard.exe

  2. 直接弹出【文件选择对话框】,默认打开target\setup-bin\assets文件夹,要求你选择一个【问卷配置】json文件。

  3. 选择prompt-manifest.json文件,点击【打开】按钮。

    image

  4. 开始回答问题。

    image

  5. 期间,不能退出。

    image

  6. 完成所有问题之后,点击【完成】按钮。

  7. 程序退出。

  8. 【回答结果】json文件被输出到和输入文件相同的目录下,文件名为answers.json

我已经在windows 10x64windows 7x64亲自验证过了。

后继阶段的工作计划

  1. 完成N-API封装,让它更容易地与nodejs集成。node-ffi的集成方式还是太繁琐了。能够直接支持操作系统也有限。比如说,【中标麒麟】的国产操作系统就没有被明确地表示支持。
  2. 完成Neon封装
  3. ubuntu, MacOS操作系统交叉编译
  4. DLL, N-API, Neon试着支持异步回调函数。而不是在调用期间阻塞住node进程。
  5. DLLC node module【安装向导】组件这个业务场景,实现更高级的业务功能。即,
    1. 接收【调用端】传入的回调函数。
    2. 每完成一步【问题-收集】就调用回调函数向【调用端】通报进度,和暂停【交互流程】
    3. 【调用端】异步地执行一些工作,再借助回调函数的返回值通知【安装向导】继续【交互流程】
    4. 直到整个安装过程结束。
  6. 将此工程内一些通用的部分添加到【前端-脚手架】内的【rust工程原型】里。比如,
    1. build.js脚本与.boilerplate目录,生成【绿色安装包】
    2. build.rscargo run准备符号链接
  7. 考虑到WebkitGTK不兼容于windows操作系统。后续不可避免向QT组件库技术转向。

希望路过“大神”帮我看看

我这cargo build --release编译出来的dllexe都有点儿大(大约20MB)。这似乎有些不正常。路过的【神仙哥哥】与【神仙妹妹】是否可以帮我看看,我这是代码或编译配置,哪里有问题呀?

You might also like...
Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust.

Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust.

Starlight is a JS engine in Rust which focuses on performance rather than ensuring 100% safety of JS runtime.

starlight Starlight is a JS engine in Rust which focuses on performance rather than ensuring 100% safety of JS runtime. Features Bytecode interpreter

A rust web framework with safety and speed in mind.

darpi A web api framework with speed and safety in mind. One of the big goals is to catch all errors at compile time, if possible. The framework uses

A web framework for Rust.

Rocket Rocket is an async web framework for Rust with a focus on usability, security, extensibility, and speed. #[macro_use] extern crate rocket; #[g

Rust / Wasm framework for building client web apps
Rust / Wasm framework for building client web apps

Yew Rust / Wasm client web app framework Documentation (stable) | Documentation (latest) | Examples | Changelog | Roadmap | 简体中文文档 | 繁體中文文檔 | ドキュメント A

Moodle CMS Notifications in Rust
Moodle CMS Notifications in Rust

Moodle CMS Notifications View unread Moodle CMS notifications. Mark all notifications as read. Lightweight with no dependencies. Cross platform. Authe

Diana is a GraphQL system for Rust that's designed to work as simply as possible out of the box

Diana is a GraphQL system for Rust that's designed to work as simply as possible out of the box, without sacrificing configuration ability. Unlike other GraphQL systems, Diana fully supports serverless functions and automatically integrates them with a serverful subscriptions system as needed, and over an authenticated channel. GraphQL subscriptions are stateful, and so have to be run in a serverful way. Diana makes this process as simple as possible.

Thruster - An fast and intuitive rust web framework

A fast, middleware based, web framework written in Rust

🌱🦀🌱 Trillium is a composable toolkit for building web applications with async rust 🌱🦀🌱

🌱🦀🌱 Trillium is a composable toolkit for building web applications with async rust 🌱🦀🌱

Releases(basic3)
Owner
Stuart Zhang
angular, vue, webpack, rust, wasm 大前端
Stuart Zhang
Rust I18n is use Rust codegen for load YAML file storage translations on compile time, and give you a t! macro for simply get translation texts.

Rust I18n Rust I18n is use Rust codegen for load YAML file storage translations on compile time, and give you a t! macro for simply get translation te

Longbridge 73 Dec 27, 2022
lispr is a Rust macro that tries to implement a small subset of LISPs syntax in Rust

lispr lispr is a Rust macro that tries to implement a small subset of LISPs syntax in Rust. It is neither especially beautiful or efficient since it i

Jan Vaorin 0 Feb 4, 2022
Rust/Axum server implementation with PCR(Prisma Client Rust)

Realworld Rust Axum Prisma This project utilizes Rust with the Axum v0.7 framework along with the Prisma Client Rust to build a realworld application.

Neo 3 Dec 9, 2023
A Rust web framework

cargonauts - a Rust web framework Documentation cargonauts is a Rust web framework intended for building maintainable, well-factored web apps. This pr

null 179 Dec 25, 2022
A Rust library to extract useful data from HTML documents, suitable for web scraping.

select.rs A library to extract useful data from HTML documents, suitable for web scraping. NOTE: The following example only works in the upcoming rele

Utkarsh Kukreti 829 Dec 28, 2022
openapi schema serialization for rust

open api Rust crate for serializing and deserializing open api documents Documentation install add the following to your Cargo.toml file [dependencies

Doug Tangren 107 Dec 6, 2022
📮 An elegant Telegram bots framework for Rust

teloxide A full-featured framework that empowers you to easily build Telegram bots using the async/.await syntax in Rust. It handles all the difficult

teloxide 1.6k Jan 3, 2023
Sōzu HTTP reverse proxy, configurable at runtime, fast and safe, built in Rust. It is awesome! Ping us on gitter to know more

Sōzu · Sōzu is a lightweight, fast, always-up reverse proxy server. Why use Sōzu? Hot configurable: Sozu can receive configuration changes at runtime

sōzu 2k Dec 30, 2022
A Rust application which funnels external webhook event data to an Urbit chat.

Urbit Webhook Funnel This is a simple Rust application which funnels external webhook event data to an Urbit chat. This application is intended to be

Robert Kornacki 15 Jan 2, 2022
A html document syntax and operation library written in Rust, use APIs similar to jQuery.

Visdom A server-side html document syntax and operation library written in Rust, it uses apis similar to jQuery, left off the parts thoes only worked

轩子 80 Dec 21, 2022