写点什么

FinClip 小程序 +Rust(五):用内联 SVG 实现二维码

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

    阅读完需:约 19 分钟

FinClip小程序+Rust(五):用内联SVG实现二维码

一旦试通了 Rust 通用逻辑功能和 FinClip 小程序的结合,可以玩的东西就很多了。我们先试试用 Rust 生成二维码,并以 inline SVG 方式提供给小程序渲染展示


前面一番操作,从小程序前端到 Rust 跨平台通用算法逻辑 library,基本搞通。再增加其他功能的话,照虎画猫也不难。

给加密钱包写个二维码实现,历时...20 分钟

就算是 Paper wallet,也还得有两个二维码才能用(总不能每次手敲几十位无法记忆的数字吧?) - 一个是钱包地址,接受支付或者查自己的资产时用;另一个是私钥,在交易时使用。


感谢 Rust 开源世界的大神们,要给我们的钱包加个生成二维码的功能,真是唾手可得。再把我们在前面已经玩熟了的 FinClip 小程序+Rust 的套路拿出来弄一次,顺便还能额外试试 SVG 在小程序里面怎么搞:

  • 首先我们找个能生成二维码的代码库。选择当然是很多了,但是我们希望它是跨平台的,二维码本身的生成是纯算法逻辑,哪都一样

  • 其次,二维码的渲染展示,最好不依赖于任何平台。二维码图形其实用 println 都能打印出来(也就是把'█'这个字符排列一下嘛)。我们选择 SVG,因为它的格式实际上就一 XML 文本。这样我们就不用去搞什么 PNG、JPG,引入一些不必要的库

  • 最后,我们要把生成的二维码 SVG 展示在 FinClip 小程序中


有了上述“指导思想”,我们选择了以下的方案:

  • 使用 qrcodegen 这个 Rust crate,帮助实现 QR code 生成

  • 按照 SVG 的规范,把生成的 QR code 输出为 XML

  • 使用 base64 这个 Rust crate,把 SVG 文本转化成一串 base64 编码的字符串

  • 把这串字符,在小程序中作为变量值绑定到页面渲染

下面我们把之前介绍的从 Rust 到 FinClip 小程序的流程又“机械”的走一次。

Rust 部分

首先,改一下 Rust 部分的代码:

finclip-rust    |---- ios    |---- mini-app    |---- rust   <==== 修订对象
复制代码

在 rust 子项目的根目录下,对 Cargo.toml 编辑,增加两个库:

    [dependencies]    ...    qrcodegen = "1.8.0"    base64 = "0.13.0"
复制代码


然后编辑修订 src/lib.rs,增加一个 QR Code 的生成函数,因为是打算给小程序调用的,所以也需要输出为 C 函数。QR code 生成部分特别简单,'QrCode::encode_text(&str, ECC-level)'即可生成,其中 ECC 为 Error correction,即当二维码损毁的时候,视乎损毁程度,二维码中浓缩的数据的程度(四级)以支持容错。我们选个 medium 即可:

#[no_mangle]pub unsafe extern "C" fn generate_qrcode_svg(input: *const c_char) -> *const c_char {    let s = CStr::from_ptr(input);    let sp = s.to_str().unwrap();
let qr = QrCode::encode_text(sp, QrCodeEcc::Medium).unwrap(); let svg = to_svg_string_base64(&qr, 4);
let result = CString::new(svg).unwrap(); let c_result: *mut c_char = result.into_raw();
c_result}
复制代码


注意这里比较重要的,是这个'to_svg_string_base64()'函数的实现,它实际上是“手工”生成一个 XML 文本。一个 SVG 的“图片”,例如以下这个 star.svg,用文本编辑器打开,实际上它是这个样子:

<svg height="210" width="500">  <polygon points="100,10 40,198 190,78 10,78 160,198" style="fill:lime;stroke:purple;stroke-width:5;fill-rule:nonzero;"/>  Sorry, your browser does not support inline SVG.</svg>
复制代码

to_svg_string_base64()的代码也没几行,详情可在项目代码库中查看,基本原理就是生成上述 XML 格式的数据,并调用'base64:encode'函数把它变成一串在小程序中可用的数据。SVG 原始数据,例如是这样:

<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 38 38" stroke="none">    <rect width="100%" height="100%" fill="#FFFFFF"/>    <path d="M4,4h1v1h-1z M5,4h1v1h-1z M6,4h1v1h-1z M7,4h1v1h-1z M8,    4h1v1h-1z M9,4h1v1h-1z M10,4h1v1h-1z M14,4h1v1h-1z M15,     ......     fill="#000000"/></svg>
复制代码

再经过 base64 的转换后,会变成这样(注:以下例子只是一个二维码 SVG 转换后很长的字符串的部分节选,实在太长无法完整粘贴在此):

        PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAwIDM3IDM3IiBzdHJva2U9Im5vbmUiPgogICAgICAgIDxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9IiNGRkZGRkYiLz4KICAgICAgICA8cGF0aCBkPSJNNCw0aDF2MWgtMXogTTUsNGgxdjFoLTF6IE02LDRoMXYxaC0xeiBNNyw0aDF2MWgtMXogTTgsNGgxdjFoLTF6IE05LDRoMXYxaC0xeiBNMTAsNGgxdjFoLTF6IE0xNCw0aDF2MWgtMXogT60F6IE0yOCwzMmgxdjFoLTF6IE0zMCwzMmgxdjFoLTF6IiBmaWxsPSIjMDAwMDAwIi8+Cjwvc3ZnPgo=
复制代码

按照老规矩,我们写在/src/lib.rs 的代码,到/examples 下修订一下 test.rs:

//test.rs//// 测试在Rust侧生成钱包密钥对,转换成C侧的数据结构。// 测试钱包在C侧调用接口存储和重新读出钱包密钥//
use std::ffi::{CStr};//use rustylib::gen::{CWallet};use rustywallet::{ CWallet, generate_cwallet, free_cwallet, save_wallet, fetch_cwallet, generate_qrcode_svg};
fn main() {
unsafe { let wallet: CWallet = generate_cwallet(); println!("---- generated a wallet to be used on C-side ----"); print_wallet(&wallet);
println!("---- saving the wallet to wallet.json ----"); save_wallet(&wallet); println!("---- saved! ----");
println!("---- fetching the saved wallet to be exposed to C-side ----"); let fetched = fetch_cwallet(); print_wallet(&fetched);
free_cwallet(wallet); // 对应 generate_cwallet() free_cwallet(fetched); // 对应 fetch_wallet() }}
unsafe fn print_wallet(wallet: &CWallet) { let a = CStr::from_ptr(wallet.public_addr); let pa = a.to_str().unwrap(); println!("public address=> {}", pa); let qrcode_pa = generate_qrcode_svg(wallet.public_addr); let c_qrcode_pa = CStr::from_ptr(qrcode_pa); println!("QR Code:\n {}", c_qrcode_pa.to_str().unwrap());
let pk = CStr::from_ptr(wallet.public_key); let ppk = pk.to_str().unwrap(); println!("public key=> {}", ppk);
let sk = CStr::from_ptr(wallet.private_key); let psk = sk.to_str().unwrap(); println!("private key=> {}", psk);
}
复制代码


运行测试一下无碍,就按老办法打包生成 iOS universal library:

cargo run --example testcargo lipo --release (或者直接用scripts/ios_build.sh,输出头文件和库到ios项目目录中,更加便利)
复制代码

Rust 部分宣告结束。

宿主 App 部分

接下来到了宿主部分,要把 Rust 的新功能给集成进去,也是非常的机械、简单。

finclip-rust    |---- ios     |      |---- clip.xcodeproj     |      |---- clip    |              |---- FinClipExt.m   <==== 修改一下    |---- mini-app    |---- rust   (done)
复制代码

也就是在FinClipExt,这个所有准备向小程序输出的自定义 API 的“挂靠单位”(一个 singleton),修改一下'generate_wallet'这个函数,在钱包中生成钱包地址后,把这个地址的字符串作为输入,让 Rust 部分的代码生成对应的 SVG 数据并转换成 base64 编码,然后再把结果放到一个叫"public_address_qrcode"的字段中,随着'generate_wallet'返回的 NSDictionary 返回。

FinClip 小程序部分:支持内联 SVG

终于来到了小程序的展现部分,修改也是几分钟的事情:

finclip-rust    |---- ios (done)    |---- mini-app      |         |---- pages    |                 |---- generate    |                           |---- generate.fxml  <==== 增加两行代码    |                           |---- generate.js    <==== 增加三行代码    |---- rust   (done)
复制代码


首先,自然是要在界面增加一个地方来展示二维码,我们采用最简单粗暴的办法,插入这么两行:

 <view style="width:148px;height:148px;background:url('{{ wallet.public_address_qrcode }}') no-repeat center"> </view>
复制代码


这是干嘛的呢?就是以 inline 方式,把本地生成的 SVG 通过 CSS 设为 background image。为什么不直接用 HTML5 所支持的 SVG 标签?因为... 这是当前互联网上小程序平台的通用做法,考虑到大家都这么干... FinClip 首先还是选择了兼容互联网主流技术,希望后续对 SVG 有更强大的支持吧。


然后,就是上面这个'{{wallet.public_address_qrcode}}'的变量的数据生成了。这是比较 tricky 的部分,涉及所谓“内联”的情况。


“内联”,就是把在当前网页内的数据(可能是静态,但更可能是动态生成)当作一个外部资源供页面渲染时使用。以图片为例,通常我们在网页里通过 img 的标签,让它的 src 指向图片所在的某个 URL 地址,从而网页渲染时会把图片资源加载进来。但如果该图片是一种当前页面内的数据呢?我们怎样告诉浏览器去加载它、渲染它?解决方案是通过一种叫做 Data URI scheme 的格式,例如:

<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" alt="Red dot" />
复制代码

Data URI scheme 的语法是这样:

data:[<media type>][;base64],<data>
复制代码

我们要内联式生成 SVG,它的 data URI scheme 是:

"data:image/svg+xml;base64,"
复制代码

所以,我们现在来到 mini-app/pages/generate/generate.js,在这里我们要生成一串数据是符合这个规范的,把它返回到 generate.fxml 的'{{wallet.public_address_qrcode}}'中。老办法,改一改小程序的 onShow 函数:

  onShow: function (options) {    const wallet = ft.generate_wallet();    const qrcode_svg = "data:image/svg+xml;base64," + wallet.public_address_qrcode;    console.log("qrcode");    console.log(qrcode_svg);
this.setData({ ['wallet.public_address']: wallet.public_address, ['wallet.public_address_qrcode']: qrcode_svg, ['wallet.public_key']: wallet.public_key, ['wallet.private_key']: wallet.private_key }) }
复制代码

也是足够的简单直观了。Rust 侧把脏活累活都干好了,到了小程序侧,也就是给数据加个明文的 data URI scheme 前缀,结束。


当然,要最后联调我们还得用FinClip社区版的企业端、运营端,自己带上开发者、管理者的帽子,自己提交一下新版本的小程序,再自己审批上架。然后用重新在 xcode 编译的宿主 App 刷一刷 - 带二维码的加密钱包马上呈现。



小结

从 Rust 到达FinClip 小程序,开发过程实质上是层层的跨语言的接口转换、数据转换。


只要把中间的转换机制弄明白弄熟练,其实技术上的实现是比较机械的。一旦成为“熟手技工”,熟练掌握写转换代码的技能,我们就可以把注意力、创新焦点放在 Rust 侧和小程序侧,一侧让我们实现算法型、逻辑型的跨平台通用的代码,另一侧让我们赋能其他前端开发者去创造应用。


如之前的几篇文章所介绍,我们需要经过三种语言、数据结构在各语言之间的适配:

  • Rust 代码,通过 FFI(Foreign Function Interface),输出 C 风格的 API 接口,以及 C 的数据类型、内存结构。这里的 tricky part,是在编译器跟前和 Rust 的 ownership 内存模型作“缠斗”,刚开始对这个语言不熟悉的时候,学习曲线比较陡峭,遭遇的困难往往让人气馁,但是一旦柳暗花明,又带来很大的满足感。虽然 Rust 是一个内存安全的语言,可是 C 不是,C 侧的使用过程,依然可能导致内存泄漏

  • 第一步产出的 library,需要被目标平台的原生代码集成,才能被注入至 FinClip SDK 中实现扩展。在 iOS 上,C library 在 Objective-C、Swift 的调用性能耗损几乎可以忽略不计,C 函数在 Objective-C 代码中基本是无缝使用,毕竟 Objective-C 可视为是 C 的超集。在 iOS 上,是 C 类型的函数入参和返回值需要被转换成 Objective-C 的 NSDictionary 之类的字典类型。在 Android 上,涉及 JNI 一层转换,变成 Java 对象。但 A ndroid 官方提供基于 Rust 开发 native component 的支持,看上去更值得深入探索。本文未及尝试,有待来者补充

  • 最后是原生部分的接口,经过 FinClip SDK 映射至 JavaScrip t 侧,供小程序的应用开发。这里涉及的是原生代码的字典类型数据转换成 JSON 数据

代码层层转换,是 Polyglot Programming(多语言混合编程)所难以避免 - 我们用最适合的语言去解决最对口的问题,但是最终每一个局部方案都需要互相连贯起来。实现了第一次,第二、第三次就难度按指数级别下降,也许最后就是“唯手熟尔”。

还可以尝试更多

受时间与篇幅限制,还有更多内容未及试验。以下是一些可以在现有代码上继续扩展的功能,是熟悉 FinClip+Rust 的很好练习:

  • 完成 crypto wallet 的本地存储和读取:把生成的密钥对和地址持久化存储在宿主 App 中。在 iOS 可能需要用 Core Data,但是 Rust+LevelDB 说不定也 OK。更进一步,还可以实现对密钥的加密存储

  • 实现私钥对交易的签名

  • 使用 Rust Web3 crate(Ethereum JSON-RPC 库),连接以太坊测试链,实现一下账户余额查询、转账支付等功能

  • 试试 Android、Linux/Mac/Windows 平台上实现同样的 demo

  • Rust 是最早支持编译生成 WebAssembly 的语言。WebAssembly 对小程序性能提高有一定的帮助。互联网主流平台的小程序技术的基础库对 WebAssembly 已经开始支持。在这里,Rust 同样有可观的发挥空间


欢迎读者对这个 demo 继续作出更多尝试,寻找实际应用场景、探索实用价值。


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


本系列文章

用户头像

Speedoooo

关注

还未添加个人签名 2021.10.08 加入

还未添加个人简介

评论

发布
暂无评论
FinClip小程序+Rust(五):用内联SVG实现二维码_rust_Speedoooo_InfoQ写作社区