写点什么

FinClip 小程序 +Rust(四):端到端融合

作者:Speedoooo
  • 2022 年 5 月 17 日
  • 本文字数:7606 字

    阅读完需:约 25 分钟

FinClip小程序+Rust(四):端到端融合

Rust 实现的算法逻辑,如何封装到 Objective-C 并依照 FinClip 自定义 API 的规范注入到 FinClip SDK,最终作为扩展接口供一个 FinClip 小程序去使用,端到端跑通!

前言

《FinClip小程序+Rust(二)》里我们已经试验成功:iOS native app 集成 FinClip SDK,以及 Rust 代码编译成 x86_64-apple-ios 及 aarch64-apple-ios 目标码;在《FinClip小程序+Rust(三)》我们用一个 Bitcoin 和 Ethereum 采用的 secp256k1 Ellipic Curve Cryptography(椭圆曲线加密)算法的 Rust 实现(感谢无所不有的 Rust 开源生态),极其简单(而取巧)的开发了一个加密货币钱包。

接下来是本系列的焦点:融合

我们的设想是,开发一个 native app,里面用 Rust 实现了一些加解密相关的基础计算功能 - 内存安全、性能不错、跨平台通用;然后打通 FinClip SDK,让各种 Web3 的应用 - DeFi、GameFi、任何需要涉及加密钱包的场景(没有加密钱包就无所谓 Web3 了),以小程序的前端形态“上架”。


回顾我们这个小项目的目录结构:

finclip-rust        |---- ios          |---- android         |---- desktop         |---- mini-app         |---- rust (加密钱包相关代码,已完成)
复制代码


Rust 部分告一段落。

Rust 函数注入到 FinClip SDK

接下来是按《FinClip小程序+Rust(二)》的方法生成一个 iOS 的 wrapper 应用,姑且命名为“clip”,在 iOS 目录里,一番操作猛如虎,生成的内容如下:

ios |---- clip |       |---- AppDelegate.h |       |---- AppDelegate.m |       |---- Assets.xcassets |       |---- Base.lproj       |       |---- FinApplet.framework  (<==== 在xcode的项目添加FinClip SDK后产生) |       |---- SceneDelegate.h |       |---- SceneDelegate.m |       |---- ViewController.h |       |---- ViewController.m |       |---- main.m | |---- clip.xcodeproj         |---- project.pbxproj         |---- project.xcworkspace         |---- xcuserdata
复制代码


在以下的步骤中,我们将主要编辑两处代码:AppDelegate 和 ViewController,以及增加一个 singleton 的实现。

注入自定义 API 扩展 FinClip SDK 的原理

和市场上的其他小程序技术不同,FinClip 的产品使命是把小程序技术能力赋能给任何设备的任何应用,所以它提供了一个嵌入式的小程序引擎(又称为运行沙箱)SDK,而使用这个 SDK 的应用,被称之为宿主。宿主可能有各种的原生功能,希望被注入到 SDK 中从而暴露给小程序的开发者用 JavaScript 接口调用,从而极大程度扩展、丰富标准 SDK 的能力,支持广泛的应用场景。


视乎你要注入的 API 是同步还是异步调用,在 FinClip SDK 的注入时采用相应的接口进行注册。官网此处有一定的说明,比较简单直观。这里特别要提一下同步接口的注册,开发者如果开始时没有注意到文档说明,可能会浪费一些不必要的时间去诊断为什么小程序侧的 JavaScript 接口调用拿不到数据:


如果要注入到 SDK 的 API 是同步类型,其入参必须是字典类型,返回值也必须是字典类型,且内部不能包含无法序列化成 JSON 的对象。 API 的函数返回值字典里必须包含  errMsg,如果返回成功则 errMsg 的内容值为"<API 函数名>:ok",如果返回失败,则内容值为"<API 函数名>:fail"

如果想加一些提示信息,可以将信息内容放在 ok 或 fail 的后面,例如 @"errMsg":@"finclipTestSync:fail invalid params"("finclipTestSync"为你要注入到 SDK 的自定义函数)。

把 Rust 输出的函数封装成 FinClip 接受的 API

FinClip 接受的同步型 API 签名,是字典类型入参和字典类型返回值。我们对需要暴露给小程序的 Rust 函数做一一对应的包装,这个过程比较繁琐机械,大致上可以总结如下:

- (NSDictionary *)yourSyncAPI:(NSDictionary *)param{    NSLog(@"%p, param:%@", __func__, param);        // 从入参param字典取出参数,就是把字典内容转换成Rust函数需要的对象或数据类型    YourParam1 param1 = ...;    YourParam2 param2 = ...;    YourData data = yourRustFuncExportToC(param1, param2, ...);        // 把获得的数据,以字符串方式塞回到字典中,返回    NSDictionary *resultDict = @{                                 @"errMsg":@"yourSyncAPI:ok",                                 @"data_item1":[NSString stringWithUTF8String:data.property1],                                 @"data_item2":@"[返回值字符串...]"    };    return resultDict;}
复制代码

就我们这个加密钱包例子而言, 在 Rust 侧通过 FFI 输出了四个 C-style 接口:

CWallet generate_cwallet(); // Rust生成的Wallet结构,“翻译”成Cfree_cwallet(CWallet); // C侧(也就是使用侧)使用完记得通知Rust释放内存save_wallet(CWallet *); // 把钱包密钥持久化CWallet fetch_cwallet(); // 从存储把钱包密钥读出
复制代码

注:上述最后两个关于钱包存取的功能,在 iOS 上需要基于 Core Data 之类的方案实现,暂未支持。


我们需要把上述函数逐一映射到 Objective-C 的接口封装,才能注册到 FinClip SDK 中。

封装发生在哪里?

上述机械性的映射、封装、数据转换(字典入参转换成 Rust/C 函数入参,Rust/C 函数返回值转换成字典),代码应该写在哪里呢?


简单粗暴的方式,可以写在 AppDelegate 里,因为我们准备把自定义 API 的注册跟 FinClip SDK 初始化放在一起(见下节)。但这不是一个很优雅的做法。


我们决定在 Xcode 项目中,增加一个 class,姑且叫做 FinClipExt(FinClipExt.h/.m)。我们把它实现成一个 singleton,把所有要注册到 FinClip SDK 的 API 都挂在它下面。头文件:

////  FinClipExt.h//  clip////
#ifndef FinClipExt_h#define FinClipExt_h
#import <FinApplet/FinApplet.h>#import "rustywallet.h"
@interface FinClipExt : NSObject { CWallet cwallet;}
+(FinClipExt*)singleton;
@end#endif /* FinClipExt_h */
复制代码

具体的实现,节选片段(Singleton 的实现代码不是重点):

- (NSDictionary *)generate_wallet:(NSDictionary *)param{    NSLog(@"OC: generate_wallet ============>");    NSLog(@"%p, param:%@", __func__, param);    cwallet = generate_cwallet();    char *addr = cwallet.public_addr;    char *pub_key = cwallet.public_key;    char *sec_key = cwallet.private_key;    NSLog(@"OC: generate_wallet addr ====> %@", [NSString stringWithUTF8String:addr]);
NSDictionary *resultDict = @{ @"errMsg":@"generate_wallet:ok", //in real case, check to return ok or fail @"public_address":[NSString stringWithUTF8String:addr], @"public_key":[NSString stringWithUTF8String:pub_key], @"private_key":[NSString stringWithUTF8String:sec_key] }; return resultDict;}
//remember to free the C memory allocation of the wallet structure//free_cwallet(cw);- (NSDictionary *)release_wallet:(NSDictionary *)param{ NSLog(@"OC: release_wallet ============>"); free_cwallet(cwallet);
NSDictionary *resultDict = @{ @"errMsg":@"release_wallet:ok" }; return resultDict;}
复制代码

我们把 FinClipExt.h 在 AppDelegate 进行 import 即可。

在哪里“登记”API?

本项目中,我们采用了 Objective-C 去开发 iOS wrapper app,FinClip SDK 的初始化,我们放在 AppDelegate 中,那么这里也是我们注入 Rust 所实现的功能的地方。


首先我们需要确认 Rust library 生成了对应的 C  头文件。这个文件的位置必须是 Xcode 编译我们这个 clip 项目时能找到的。例如:

cd finclip-rustcd rustcbindgen src/lib.rs -l c > rustywallet.h
复制代码

那么头文件生成在 finclip-rust/rust 下,Xcode 的项目需要在"Build Setting”中的“Header Search Path”把路径加上。


当然,我们在《FinClip小程序+Rust(三)》中成功构建的 librustywallet.a 也必须在 Xcode 项目的“Library Search Path”路径上。


接下来,我们在 AppDelegate.m 里进行初始化和注册:


其中,特别注意到 FinClip SDK 注册自定义 API 时所需提供的 target:

/** 注册同步扩展Api @param extApiName 扩展的api名称 @param target 实现同步api的类 @return 返回注册结果 */- (BOOL)registerSyncExtensionApi:(NSString *)syncExtApiName target:(id)target;
复制代码


这里的 target,是我们的 FinClipExt singleton,如:

    [[FATClient sharedClient] registerSyncExtensionApi:@"generate_wallet" target:[FinClipExt singleton]];
复制代码


到此为止,我们初步拥有了一个可以运行 FinClip 小程序并且能让它们调用我们用 Rust 写出来的原生功能的 App。

FinClip 小程序怎么调用自定义接口

现在我们准备开发一个接近于"paper wallet"的钱包“界面” - 一个 FinClip 小程序。它每次被打开的时候都会调用 Rust 实现的接口去生成一个钱包三要素:公开地址、公钥、私钥(当然为了让它真的可用,我们还需要给公开地址、私钥各自生成二维码,有时间我们再加上)。


实际上我们当然不应该每次打开小程序自动生成一个钱包,这里纯属为了 demo。这个小程序界面长这样:


创建一个小程序项目

《FinClip小程序+Rust(二)》所介绍,我们用 FinClip IDE 创建一个项目。

finclip-rust       |---- ios  (宿主集成FinClip SDK相关代码,已初步完成)       |---- android        |---- desktop        |---- mini-app        |---- rust (加密钱包相关代码,已完成)
复制代码


我们把这个小程序项目创建在 mini-app 之下:

mini-apps    |---- app.ftss    |---- app.js    |---- app.json    |---- fide.project.config.json    |---- package.json    |---- pages    |       |---- generate.ftss    |       |---- generate.fxml    |       |---- generate.js    |       |---- generate.json    |---- sitemap.json
复制代码


IDE 缺省生成的页面,我们把它改名为 generate,以示是负责生成密钥的页面,但无关重要。

告知小程序我们有自定义接口可用

在根目录下,增加一个 FinClipConf.js 文件:

module.exports = {  extApi:[    { //普通交互API      name: 'generate_wallet', //      sync: ture, //是否为同步api      params: { //扩展api 的参数格式,可以只列必须的属性      }    },    {       name: 'release_wallet',       sync: true,       params: {       }    }  ]}
复制代码


之前我们在 Xcode 的项目中用 Objective-C 实现的 FinClipExt singleton,所含有的 API 被注册至 FinClip SDK 中的,一一在此对应映射。

在小程序的生命周期某些函数中调用接口

上述注册成功的 API,小程序都可以通过“ft”这个系统全局变量去访问(就像微信小程序里需要调用平台 API 时用“wx”一样),从而让自定义的 API 像 FinClip 其他标准系统接口一样使用。


视乎具体的应用场景,我们可以在小程序的各生命周期函数中适当使用。在这个 demo 中,我们为了简单展现 Rust 所实现的加密算法可被小程序反复触发,选择在 onShow 中生成密钥,在 onHide 的时候则告知 Rust 侧释放内存(要求小程序开发者知道这个特点并配合调用,确实有点怪异。也许在宿主 App 里把它处理掉而无需暴露这样的设计给小程序开发者,更加合理。在此纯属演示目的)。


  onShow: function (options) {    const wallet = ft.generate_wallet();    console.log(wallet);    this.setData({       ['wallet.public_address']: wallet.public_address,       ['wallet.public_key']: wallet.public_key,       ['wallet.private_key']: wallet.private_key    })  },
onHide: function () { console.log("page unload"); ft.release_wallet(); },
复制代码

如何测试与上架

因为这个小程序极其简单,界面相关的代码就不赘述,可以在 Git 上取得。接下来是如何测试的问题。

首先,凡泰极客提供了 FinClip IDE 和 FinClip 助手两个工具帮助开发者调试和测试,其中前者配备运行小程序的模拟器供在电脑上开发调试:


IDE 并能生成预览的二维码:


此时用手机端的 FinClip App 扫二维码,即可在真机测试小程序。


但是,上述工具对于使用自定义 API 的小程序,无法测试涉及扩展部分的功能,因为桌面端 IDE 的模拟器中,并没有你之前自行开发的原生功能与接口,而 FinClip App 作为一个只集成了标准 FinClip SDK 的宿主,它同样仅供开发者调试标准功能。在这些标准环境下测试你使用了自定义接口的小程序,就会遇到自定义函数“undefined”的问题。所以你必须依赖 Xcode 启动 iOS 模拟器运行你所开发的宿主 App,再加载小程序进行测试。


完全在 IDE+FinClip 助手的环境中形成流畅的开发测试循环,这一闭环被打断,当我们频繁修改小程序进行调试的时候,怎么办呢?目前的方案是,你必须按《FinClip小程序+Rust(二)》中描述的那样,自行安装部署 FinClip 社区版,把你开发的小程序上架到这个本地服务中 - 你需要打开两个后台页面,一个企业端、一个运营端,你自己首先扮演开发者的角色把更新的小程序代码通过 IDE 的“上传”发布到社区版的小程序中心:


再扮演审核人员的角色,通过运营端去批准自己的上架申请。然后在 Xcode 中,运行 iOS 模拟器启动宿主 App 加载要测试的小程序(但 FinClip SDK 会自动检测小程序版本自动升级,所以如果宿主部分的代码稳定的话,实际上我们主要是在 FinClip IDE 开发,然后把代码包上传到 FinClip 社区版小程序中进行发布,iOS 模拟器中的宿主 App 就会通过 FinClip SDK 加载到最新版本的小程序)。FinClip 小程序中心的管理后台操作简单,用户在扮演开发者和管理员两个角色发版、上架小程序的过程中,也就是几个鼠标的点击。

用 FinClip 社区版本地部署形成调试闭环

以下是比较关键的一些 checkpoint,成功验证后,我们任何人在自己的开发机器上,都将拥有一套完整的小程序技术生态,我们将独自一人扮演小程序开发者、原生宿主 App 开发者、跨平台通用逻辑开发者、小程序应用服务端开发者、小程序运营管理者的角色。


在社区版企业端配置关联“合作应用”

也就是我们现在要测试的这个加密钱包小程序的宿主 App(命名为 rust-ios),它集成了 FinClip SDK。当成功在本地运行 FinClip 社区版的 docker 镜像后,访问 127.0.0.1 打开企业端,进行配置:


在我们的宿主 App 初始化 FinClip SDK 时,需要指向社区版:

    NSString *appKey = @"22LyZEib0gLTQdU3MUauARgvo5OK1UkzIY2eR+LFy28NAKxKlxHnzqdyifD+rGyG";    FATConfig *config = [FATConfig configWithAppSecret:@"8fe39ccd4c9862ae" appKey:appKey];    config.apiServer = @"http://127.0.0.1:8000";
复制代码


注意 rust-ios 的 Bundle ID (com.finogeeks.rustful.clip),和它在 Xcode 中的设置是同一个。


在社区版企业端创建小程序

我们这个测试加密钱包的小程序,如之前所命名,叫做“clip”:


它有一个 App ID,为该小程序在平台上的唯一标识:


该小程序需要被关联至上述宿主应用 rust-ios 中:


宿主应用 rust-ios 和 clip 小程序关联上了。



在宿主 App 加个打开小程序的入口

宿主 App 通过 FinClip SDK 可以远程加载无穷无尽的小程序,可是如何发现、定位这些小程序呢?在真实环境中,宿主 App 的用户可以通过 App 中提供的入口、或者他人的转发分享、或者内容网页/公众号的链接、或者搜索,发现而使用某个小程序。在我们这个小 demo 中,最简单直接的就是在宿主 App 加一个按钮,指向要测试的小程序。


界面长这样,Xcode Interface Builder 画一画,5 分钟完成:


在这里,我们要回过头去改进一下 App。改动的代码在 ViewController.m:

////  ViewController.m//  clip//
#import "ViewController.h"#import "rustywallet.h"#import <FinApplet/FinApplet.h>
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view.}-(IBAction)showLabel{
FATAppletRequest *request = [[FATAppletRequest alloc] init]; request.appletId = @"62621bba4fed110001781d9a"; request.apiServer = @"http://127.0.0.1:8000"; request.transitionStyle = FATTranstionStyleUp; //request.startParams = startParams;
[[FATClient sharedClient] startAppletWithRequest:request InParentViewController:self completion:^(BOOL result, FATError *error) { NSLog(@"打开小程序:%@", error); } closeCompletion:^{ NSLog(@"关闭小程序"); }];}
@end
复制代码

上述代码的关键,在于 request.appletId 直接指向了我们的目标小程序 clip 的 App ID(此前在 FinClip 社区版小程序中心的企业端创建生成,并被关联到了 rust-ios 宿主应用)。


IDE 连接到社区版本地

FinClip IDE 应该登录到社区版服务中,以便于在开发调试过程中能直接把小程序上传至社区版小程序中心:


小程序代码包上传后,我们将依此打开社区版的管理后台,自己扮演不同的角色去上架。

一个人的“联调”

到此,在这一个人的开发测试旅途里,独自戴上不同的帽子,扮演不同的角色,去完成这一试验:

  1. 戴上 DevOps 的帽子:启动 FinClip 社区版的 docker 镜像

  2. 戴上小程序开发者的帽子:IDE 登录 FinClip 社区版,开始开发测试。把 clip 代码包发布至社区版小程序中心

  3. 戴上小程序开发者的帽子:登录社区版企业端(缺省在 “http://127.0.0.1:8000/mop/mechanism/#/Home/index” ),把刚才上传的 clip 小程序版本提交审核,申请上架

  4. 戴上运营管理者的帽子:登录社区版运营端(缺省在 “http://127.0.0.1:8000/mop/operate/#/Home/index” ),审批让自己提交的 clip 小程序上架

  5. 戴上 iOS 开发者的帽子:到 Xcode 运行 rust-ios 宿主 App,加载 clip 小程序

  6. 重复第 2 至第 5 步


在这个过程中,可能会发现我们的 Rust 部分代码要改进或者修复,这个时候我们将要打开 vscode 或者其他环境去编译调试 Rust 代码,再构建成 iOS 的目标码,然后重复上述 2-5 步。


看上去有点复杂,涉及较多的工具、语言,尤其当把 Android 和其他平台引入的时候,又更加的繁复。但也许换个角度看,是不同的理解:这让任何个人作为独立开发者,逻辑上较为清晰的划分自己在一个端到端的技术链路上不同的角色,按部就班的把模块分而治之。


在现实中,我们更可能是团队作战,上述一个人的“手忙脚乱”可以避免;此外, FinClip 的开发测试工具链还在继续发展优化,应该会让“自研发宿主提供自定义 API 支持小程序运行”的开发测试闭环变得更加的平滑。

Source code on Github: GitHub - kornhill/finclip-rust-demo


本文首发于凡泰极客博客,作者:F1n0Geek

用户头像

Speedoooo

关注

还未添加个人签名 2021.10.08 加入

还未添加个人简介

评论

发布
暂无评论
FinClip小程序+Rust(四):端到端融合_rust_Speedoooo_InfoQ写作社区