Rust and WebAssembly 后篇 + 补充

2024-02-29 10:08
文章标签 rust 补充 webassembly 后篇

本文主要是介绍Rust and WebAssembly 后篇 + 补充,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

后来又发现了一篇很好的补充文章:https://blog.frankel.ch/start-rust/5/

Rust与JavaScript的接口

⚡ 这是本教程中需要理解和掌握的最重要的概念之一!

JavaScript 的垃圾收集机制作用的堆——分配对象、数组和 DOM 节点的地方——与 WebAssembly 的线性内存空间不同,我们的 Rust 值存在于其中。 WebAssembly 目前无法直接访问垃圾收集堆(截至 2018 年 4 月,这预计会随着“接口类型”提案而改变)。另一方面,JavaScript 可以读取和写入 WebAssembly 线性内存空间,但只能作为标量值(u8、i32、f64 等)的 ArrayBuffer。 WebAssembly 函数也接受和返回标量值。这些是构成所有 WebAssembly 和 JavaScript 通信的构建块。

wasm_bindgen 定义了如何跨此边界处理复合结构的共同理解。它涉及装箱 Rust 结构,并将指针包装在 JavaScript 类中以提高可用性,或者从 Rust 索引到 JavaScript 对象表。 wasm_bindgen 非常方便,但它并没有消除考虑我们的数据表示的需要,以及跨越这个边界传递的值和结构。相反,将其视为实现您选择的界面设计的工具。

在设计 WebAssembly 和 JavaScript 之间的接口时,我们希望针对以下属性进行优化:

  1. 最大限度地减少进出 WebAssembly 线性内存的复制。不必要的拷贝会带来不必要的开销。
  2. 最小化序列化和反序列化。与拷贝类似,序列化和反序列化也会产生开销,并且通常也会产生复制。如果我们可以将不透明的句柄传递给数据结构——而不是在一侧序列化它,将它复制到 WebAssembly 线性内存中的某个已知位置,然后在另一侧反序列化——我们通常可以减少很多开销。 wasm_bindgen 帮助我们定义和使用 JavaScript 对象或盒装 Rust 结构的不透明句柄。

作为一般的经验法则,一个好的 JavaScript↔WebAssembly 接口设计通常是这样一种设计:大型、长期存在的数据结构被实现为 Rust 类型,这些类型存在于 WebAssembly 线性内存中,并作为不透明句柄暴露给 JavaScript。 JavaScript 调用导出的 WebAssembly 函数,这些函数接受这些不透明的句柄、转换它们的数据、执行繁重的计算、查询数据,并最终返回一个小的、可复制的结果。通过只返回计算的小结果,我们避免了在 JavaScript 垃圾收集堆和 WebAssembly 线性内存之间来回复制和/或序列化所有内容。‘

生命游戏中的Rust+JavaScript接口设计

让我们首先列举一些要避免的错不良行为。我们不想在每一步变化中将整个宇宙复制到 WebAssembly 线性内存中。我们不想为宇宙中的每个单元格分配对象,也不想强加跨界调用来读取和写入每个单元格。

这让我们何去何从?我们可以将宇宙表示为一个平面数组,它存在于 WebAssembly 线性内存中,每个单元格都有一个字节。 0 是死细胞,1 是活细胞。

在这里插入图片描述

我们有多种方法可以将 Universe 的细胞暴露给 JavaScript。首先,我们将为 Universe 实现 std::fmt::Display,我们可以使用它来生成呈现为文本字符的单元格的 Rust 字符串。然后将这个 Rust String 从 WebAssembly 线性内存复制到 JavaScript 垃圾收集堆中的 JavaScript String,然后通过设置 HTML textContent 显示。在本章的后面,我们将改进此实现以避免在堆之间复制 Universe 的单元并渲染到 <canvas>

Rust实现

这里说实话按照官方的教程一步步来就行了,但我还是想大致解释一下思路。

  1. 定义枚举Cell,里面有两种状态 Alive 和 Dead。

  2. 定义结构体Universe,里面有属性 宽度、高度、和一个扁平数组cells,它是矩形细胞网络的扁平化后的一维向量。这个上面有提到过。、

  3. 既然是扁平化数组,我们需要一个映射函数,来通过实际的位置x、y来找到在扁平化后的向量中的下标index。所以实现函数 get_index

  4. 有了映射函数,接下来就是计算每一个点,它的周围有几个活着的点,所以实现live_neighbor_count函数,这个函数可能看不太明白,我解释一下:

    构造 [self.height, 0 ,1][self.width, 0 ,1] 两个长度为3的数组,然后嵌套遍历,就能得到9种组合对吧,这个想不明白的建议转行。然后其实就对应了9个位置,就是围绕自己一圈的点,包括自身的位置。所以如果都是0,先countinue,自身不算,那么为什么要取模呢,因为如果自身的位置本身在边界,它的周围根本就没有细胞,因此考虑循环,如果一个细胞在最上面,它上面的细胞,我们认为是在最下面,以此类推。

  5. 有了这个以后基本就差不多了。接下来实现最重要的 next_tick 函数,该函数用于计算下一轮宇宙整体的状态。

  6. 最后我们要把宇宙的状态交给JavaScript,因此需要一个能够可视化宇宙状态的函数,我们这里实现了to_string 方法,使用了 Display Trait 的实现。

  7. 最最后,实现Universe的构造函数。

mod utils;use std::fmt;
use wasm_bindgen::prelude::*;// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;#[wasm_bindgen]
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cell {Dead = 0,Alive = 1,
}#[wasm_bindgen]
pub struct Universe {width: u32,height: u32,cells: Vec<Cell>,
}#[wasm_bindgen]
impl Universe {pub fn new(len: u32) -> Universe {let width: u32 = len;let height: u32 = len;let cells = (0..width * height).map(|i| {if i % 5 == 0 || i % 11 == 0 {Cell::Alive} else {Cell::Dead}}).collect();Universe {width,height,cells,}}pub fn render (&self) -> String {self.to_string()}fn get_index(&self, row: u32, column: u32) -> usize {(row * self.width + column) as usize}fn live_neighbor_count(&self, row: u32, column: u32) -> u8 {let mut count = 0;for delta_row in [self.height - 1, 0, 1].iter().cloned() {for delta_col in [self.width - 1, 0 ,1].iter().cloned() {if delta_row == 0 && delta_col == 0 {continue;}let neighbor_x = (row + delta_row) % self.height;let neighbor_y = (column + delta_col) % self.width;count += self.cells[self.get_index(neighbor_x, neighbor_y)] as u8;}}count}pub fn next_tick(&mut self) {// 模拟下一个状态整个宇宙的state// 采用暴力遍历枚举let mut next = self.cells.clone();for x in 0..self.height {for y in 0..self.width {let idx = self.get_index(x, y);let cell = self.cells[idx];let live_neighbors_count = self.live_neighbor_count(x, y);let next_cell = match (cell, live_neighbors_count) {// Rule 1: 当本身是存活状态,周围存活数量小于等于1// 则本体死亡(数量稀少)(Cell::Alive, count) if count <= 1 => Cell::Dead,// Rule 2: 当本身是存活状态,周围存活数量小于等于3// 则本体正常生存(Cell::Alive, count) if count <= 3 => Cell::Alive,// Rule 3: 当本身是存活状态,周围存活数量大于等于4// 则本体死亡(数量过多)(Cell::Alive, count) if count >= 4 => Cell::Dead,// Rule 4: 当本身是死亡状态,周围存活数量大于等于3// 则本体死亡(Cell::Dead, count) if count >= 3 => Cell::Alive,// 其他情况保持原样(otherwise, _) => otherwise,};next[idx] = next_cell;}}self.cells = next;}
}impl fmt::Display for Universe {fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {for line in self.cells.as_slice().chunks(self.width as usize) {for &cell in line {let symbol = if cell == Cell::Dead {'◻'} else {'◼'};write!(f, "{}", symbol)?;}write!(f, "\n")?;}Ok(())}
}

结合JavaScript

最重要的时候来啦,就是结合JavaScript渲染到浏览器页面中。

这里我们为了快速测试,先使用pre标签,作为展示,并且使用 requestAnimationFrame 控制速率变化,这个函数会根据你的显示器帧率,自动在每一帧内执行一次函数。

我在这里自己用了一个gap来控制帧率,别太快了,不然眼睛还没反应过来,就趋于稳定了。

<body><pre id="game-of-life-canvas"></pre><script src="./bootstrap.js"></script>
</body>
import {Universe} from "wasm-game-of-life";const pre = document.querySelector("#game-of-life-canvas");
const universe = Universe.new(16);let num = 1;
let gap = 60;
const renderLoop = () => {pre.textContent = universe.render();num += 1;if (num % gap === 0) {universe.next_tick();}requestAnimationFrame(renderLoop);
}requestAnimationFrame(renderLoop);

使用线性内存 + canvas

请自行阅读 https://rustwasm.github.io/docs/book/game-of-life/implementing.html#rendering-to-canvas-directly-from-memory

最终完整代码

// lib.rs
mod utils;use std::fmt;
use js_sys::Math;
use wasm_bindgen::prelude::*;// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;#[wasm_bindgen]
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cell {Dead = 0,Alive = 1,
}#[wasm_bindgen]
pub struct Universe {width: u32,height: u32,cells: Vec<Cell>,
}#[wasm_bindgen]
impl Universe {pub fn new(len: u32) -> Universe {let width: u32 = len;let height: u32 = len;let cells = (0..width * height).map(|_| {if Math::random() < 0.03 {Cell::Alive} else {Cell::Dead}}).collect();Universe {width,height,cells,}}pub fn render (&self) -> String {self.to_string()}fn get_index(&self, row: u32, column: u32) -> usize {(row * self.width + column) as usize}fn live_neighbor_count(&self, row: u32, column: u32) -> u8 {let mut count = 0;for delta_row in [self.height - 1, 0, 1].iter().cloned() {for delta_col in [self.width - 1, 0 ,1].iter().cloned() {if delta_row == 0 && delta_col == 0 {continue;}let neighbor_x = (row + delta_row) % self.height;let neighbor_y = (column + delta_col) % self.width;count += self.cells[self.get_index(neighbor_x, neighbor_y)] as u8;}}count}pub fn next_tick(&mut self) {// 模拟下一个状态整个宇宙的state// 采用暴力遍历枚举let mut next = self.cells.clone();for x in 0..self.height {for y in 0..self.width {let idx = self.get_index(x, y);let cell = self.cells[idx];let live_neighbors_count = self.live_neighbor_count(x, y);let next_cell = match (cell, live_neighbors_count) {// Rule 1: 当本身是存活状态,周围存活数量小于等于1// 则本体死亡(数量稀少)(Cell::Alive, count) if count <= 1 => Cell::Dead,// Rule 2: 当本身是存活状态,周围存活数量小于等于3// 则本体正常生存(Cell::Alive, count) if count <= 3 => Cell::Alive,// Rule 3: 当本身是存活状态,周围存活数量大于等于4// 则本体死亡(数量过多)(Cell::Alive, count) if count >= 4 => Cell::Dead,// Rule 4: 当本身是死亡状态,周围存活数量大于等于3// 则本体死亡(Cell::Dead, count) if count >= 3 => Cell::Alive,// 其他情况保持原样(otherwise, _) => otherwise,};next[idx] = next_cell;}}self.cells = next;}pub fn width(&self) ->u32 {self.width}pub fn height(&self) ->u32 {self.height}pub fn cells(&self) ->*const Cell {self.cells.as_ptr()}
}impl fmt::Display for Universe {fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {for line in self.cells.as_slice().chunks(self.width as usize) {for &cell in line {let symbol = if cell == Cell::Dead {'◻'} else {'◼'};write!(f, "{}", symbol)?;}write!(f, "\n")?;}Ok(())}
}
import {Universe, Cell} from "wasm-game-of-life";
import {memory} from "wasm-game-of-life/wasm_game_of_life_bg";const CELL_SIZE = 10; // px
const GRID_COLOR = "#CCCCCC";
const DEAD_COLOR = "#FFFFFF";
const ALIVE_COLOR = "#000000";const universe = Universe.new(100);
const width = universe.width();
const height = universe.height();const canvas = document.querySelector("#game-of-life-canvas");
const ctx = canvas.getContext('2d');
canvas.height = (CELL_SIZE + 1) * height + 1;
canvas.width = (CELL_SIZE + 1) * width + 1;let num = 1;
let gap = 45;const renderLoop = () => {if (num % gap === 0) {drawGrid();drawCells();universe.next_tick();num = 1;}num += 1;requestAnimationFrame(renderLoop);
};requestAnimationFrame(renderLoop);const drawGrid = () => {ctx.beginPath();ctx.strokeStyle = GRID_COLOR;// Vertical lines.for (let i = 0; i <= width; i++) {ctx.moveTo(i * (CELL_SIZE + 1) + 1, 0);ctx.lineTo(i * (CELL_SIZE + 1) + 1, (CELL_SIZE + 1) * height + 1);}// Horizontal lines.for (let j = 0; j <= height; j++) {ctx.moveTo(0, j * (CELL_SIZE + 1) + 1);ctx.lineTo((CELL_SIZE + 1) * width + 1, j * (CELL_SIZE + 1) + 1);}ctx.stroke();
};const getIndex = (row, column) => {return row * width + column;
};const drawCells = () => {const cellsPtr = universe.cells();const cells = new Uint8Array(memory.buffer, cellsPtr, width * height);ctx.beginPath();for (let row = 0; row < height; row++) {for (let col = 0; col < width; col++) {const idx = getIndex(row, col);ctx.fillStyle = cells[idx] === Cell.Dead? DEAD_COLOR: ALIVE_COLOR;ctx.fillRect(col * (CELL_SIZE + 1) + 1,row * (CELL_SIZE + 1) + 1,CELL_SIZE,CELL_SIZE);}}ctx.stroke();
};

这篇关于Rust and WebAssembly 后篇 + 补充的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/758405

相关文章

Rust中的注释使用解读

《Rust中的注释使用解读》本文介绍了Rust中的行注释、块注释和文档注释的使用方法,通过示例展示了如何在实际代码中应用这些注释,以提高代码的可读性和可维护性... 目录Rust 中的注释使用指南1. 行注释示例:行注释2. 块注释示例:块注释3. 文档注释示例:文档注释4. 综合示例总结Rust 中的注释

Rust格式化输出方式总结

《Rust格式化输出方式总结》Rust提供了强大的格式化输出功能,通过std::fmt模块和相关的宏来实现,主要的输出宏包括println!和format!,它们支持多种格式化占位符,如{}、{:?}... 目录Rust格式化输出方式基本的格式化输出格式化占位符Format 特性总结Rust格式化输出方式

Rust中的Drop特性之解读自动化资源清理的魔法

《Rust中的Drop特性之解读自动化资源清理的魔法》Rust通过Drop特性实现了自动清理机制,确保资源在对象超出作用域时自动释放,避免了手动管理资源时可能出现的内存泄漏或双重释放问题,智能指针如B... 目录自动清理机制:Rust 的析构函数提前释放资源:std::mem::drop android的妙

Rust中的BoxT之堆上的数据与递归类型详解

《Rust中的BoxT之堆上的数据与递归类型详解》本文介绍了Rust中的BoxT类型,包括其在堆与栈之间的内存分配,性能优势,以及如何利用BoxT来实现递归类型和处理大小未知类型,通过BoxT,Rus... 目录1. Box<T> 的基础知识1.1 堆与栈的分工1.2 性能优势2.1 递归类型的问题2.2

在Rust中要用Struct和Enum组织数据的原因解析

《在Rust中要用Struct和Enum组织数据的原因解析》在Rust中,Struct和Enum是组织数据的核心工具,Struct用于将相关字段封装为单一实体,便于管理和扩展,Enum用于明确定义所有... 目录为什么在Rust中要用Struct和Enum组织数据?一、使用struct组织数据:将相关字段绑

浅析Rust多线程中如何安全的使用变量

《浅析Rust多线程中如何安全的使用变量》这篇文章主要为大家详细介绍了Rust如何在线程的闭包中安全的使用变量,包括共享变量和修改变量,文中的示例代码讲解详细,有需要的小伙伴可以参考下... 目录1. 向线程传递变量2. 多线程共享变量引用3. 多线程中修改变量4. 总结在Rust语言中,一个既引人入胜又可

Rust 数据类型详解

《Rust数据类型详解》本文介绍了Rust编程语言中的标量类型和复合类型,标量类型包括整数、浮点数、布尔和字符,而复合类型则包括元组和数组,标量类型用于表示单个值,具有不同的表示和范围,本文介绍的非... 目录一、标量类型(Scalar Types)1. 整数类型(Integer Types)1.1 整数字

Rust中的Option枚举快速入门教程

《Rust中的Option枚举快速入门教程》Rust中的Option枚举用于表示可能不存在的值,提供了多种方法来处理这些值,避免了空指针异常,文章介绍了Option的定义、常见方法、使用场景以及注意事... 目录引言Option介绍Option的常见方法Option使用场景场景一:函数返回可能不存在的值场景

【Rust练习】12.枚举

练习题来自:https://practice-zh.course.rs/compound-types/enum.html 1 // 修复错误enum Number {Zero,One,Two,}enum Number1 {Zero = 0,One,Two,}// C语言风格的枚举定义enum Number2 {Zero = 0.0,One = 1.0,Two = 2.0,}fn m

linux中使用rust语言在不同进程之间通信

第一种:使用mmap映射相同文件 fn main() {let pid = std::process::id();println!(