@zephyr
Articles8
Tags10
Categories0
cf-workers-wasm

cf-workers-wasm

在cloudflare workers上体验一下wasm:)

这两天闲来无事,用rust重写了一下飞机订阅程序,托管于cf workers. 程序功能比较简单,利用cf-kv存储节点配置,收到请求以后再返回分享连接。

网上关于wasm的资料已经比较丰富,rustwasm本身就有文档。下面稍微记录一下我的摸索过程:

接口

相比单独写js或rust, 用wasm还需要解决rust和js的交互问题。比如由rust导出方法,供js调用:

1
2
#[wasm_bidgen]
pub fn func() { }

如果要在rust中调用js方法,则需要先声明:

1
2
3
4
#[wasm_bidgen]
extern "C" {
pub fn func()
}

一般都会导入js-sys, web-sys这两个crate,它们封装了js中常用的类型和方法,比如js-sys::ArrayBuffer web-sys::Request, web-sys::Response (rust-analyzer有时推断不出这些类型,需要手动标记,不然没有提示)

数据

为了方便使用,还要把JsValue转换成合适的rust类型。基本类型可以直接互换,Option<T>::None对应js中的null; 另外JsValue还提供了from_serde()into_serde()这两个方法(需要启用wasm-bidgenserde-serializefeature),配合serde的宏,可以很方便的实现类型转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#[derive(Serialize, Deserialize)]
pub struct Example {
pub field1: HashMap<u32, String>,
pub field2: Vec<Vec<f32>>,
pub field3: [f32; 4],
}

pub fn to_js(x: &example) -> JsValue {
JsValue::from_serde(x).unwrap()
}

pub fn from_js(x: &JsValue) -> Example {
x.into_serde().unwrap()
}

异步

rust中的Future和js的Promise也是可以相互转换的,需要导入wasm_bidgen_futures,里面有future_to_promisespawn_local方法。此外,wasm_bidgen的宏也可以直接用在async fn上,会自动实现FuturePromise的转换。Future返回值为Result<JsValue, JsValue>,分别对应resolvereject。如果想要实现Promise.all的效果,只要对着多个Futurejoin就行了

使用kv

官网的教程已经给出了getput的调用方法,可以自己如法炮制一个list&delete

首先声明方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#[wasm_bindgen]
#[wasm_bindgen]
extern "C" {
pub type WorkersKv;

#[wasm_bindgen(structural, method, catch)]
pub async fn get(
this: &WorkersKv,
key: JsValue,
options: JsValue,
) -> Result<JsValue, JsValue>;

#[wasm_bindgen(structural, method, catch)]
pub async fn put(
this: &WorkersKv,
key: JsValue,
val: JsValue,
options: JsValue,
) -> Result<JsValue, JsValue>;

#[wasm_bindgen(structural, method, catch)]
pub async fn delete(
this: &WorkersKv,
key: JsValue,
) -> Result<JsValue, JsValue>;

#[wasm_bindgen(structural, method, catch)]
pub async fn list(
this: &WorkersKv,
prefix: JsValue,
limit: JsValue,
cursor: JsValue,
) -> Result<JsValue, JsValue>;
}

wasm_bidgen必须添加structural参数,因为WorkersKv是外部js传入的kv binding,类型未知(不是JsValue),见The structural flag

定义返回值类型(get返回String, 而JsValue已经实现了From<String>, 因此不必再定义一个GetResult)

1
2
3
4
5
6
7
8
9
10
11
12
13
#[derive(Serialize, Deserialize)]
pub struct ListKey {
pub name: String,
pub expiration: Option<u64>,
pub metadata: Option<Metadata>,
}

#[derive(Serialize, Deserialize)]
pub struct ListResult {
pub keys: Vec<ListKey>,
pub complete: Option<bool>,
pub cursor: Option<String>,
}

再封装一下方法,方便调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
pub async fn get<T: Into<JsValue>>(kv: &WorkersKv, key: T) -> Result<String> {
let res: JsValue = kv.get(key.into(), JsValue::NULL).await?;
if res.is_undefined() || res.is_null() {
return Err(Error::InvalidKey(String::from("invalid key: null")));
}
let res: String = res.into_serde()?;
Ok(res)
}

pub async fn put<U, T>(kv: &WorkersKv, key: U, value: T) -> Result<()>
where
U: Into<JsValue>,
T: Into<JsValue>,
{
kv.put(key.into(), value.into(), JsValue::NULL).await?;
Ok(())
}

pub async fn delete<T: Into<JsValue>>(kv: &WorkersKv, key: T) -> Result<()> {
kv.delete(key.into()).await?;
Ok(())
}

pub async fn list(kv: &WorkersKv) -> Result<ListResult> {
let res: JsValue =
kv.list(JsValue::NULL, JsValue::NULL, JsValue::NULL).await?;
let res: ListResult = res.into_serde()?;
Ok(res)
}

上面的四个函数都用了自定义的Error和Result, 配合?会很方便。这里没有实现DisplayErrortrait, 而是转换成JsValue,供外部js来try...catch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#[derive(Debug)]
pub enum Error {
Js(JsValue),
Serde(SerdeError),
InvalidKey(String),
}

pub type Result<T> = std::result::Result<T, Error>;

impl From<JsValue> for Error {
fn from(e: JsValue) -> Self { Error::Js(e) }
}

impl From<SerdeError> for Error {
fn from(e: SerdeError) -> Self { Error::Serde(e) }
}

impl From<Error> for JsValue {
fn from(e: Error) -> Self {
use Error::*;
match e {
Js(e) => e,
Serde(e) => e.to_string().into(),
InvalidKey(e) => e.into(),
}
}
}

调用list和get, 并发地获取数据

1
2
3
4
5
6
7
let list = kv::list(kv).await?;

let all_data = try_join_all(
list.keys.into_iter().map(|key| async move {
kv::get(kv, key.name).await?
})).await?

Author:@zephyr
Link:https://zephyr.moe/2021/08/21/cf-workers-wasm/
License:CC BY-NC-SA 3.0 CN