Programming WebAssembly with Rust 译 构建WebAssembly Checkers (第二章)未完待续

样本应用程序小到足以适应快速博客文章,非常擅长为您提供快速,轻松的一些新语法介绍。它们向您展示如何打印到控制台,它们向您显示给定代码行通常需要多少个括号,只要您不介意缺少周围应用程序的上下文就可以看到工作代码在帖子中显示。

即使在创建完整的应用程序时,易于使用的介质的性质意味着这些说明性的应用程序通常看起来不像真实世界的应用程序,并且它们与您可能部署到生产中的任何内容几乎没有任何相似之处。

在本章中,您将不会创建一个人为的“Hello,World”WebAssembly模块。相反,您将创建一个可用于运行棋子游戏的模块(也称为草稿,取决于您来自世界的哪个部分)。

您将通过创建一系列小功能来构建此模块,这些功能一旦完成,将协同工作以提供工作跳棋游戏的基础知识。在实际应用程序的复杂性和保持示例简单以用作学习工具的需要之间总是存在权衡,因此我们在评估一些游戏规则和边缘情况时削减了一些角落,但代码将是可玩的当你完成了。

玩跳棋,棋盘游戏

如果您玩过跳棋,那么您可以跳过此部分。如果您需要复习,那么这仍然是一个快速阅读。 Checkers是一款在8×8游戏板上玩的相当简单的游戏。电路板的正方形通常是交替颜色(美国最常见的是黑色和红色电路板)。

然后每个玩家将12个棋子放在棋盘上的固定方格中,彼此间隔均匀一个方格。一个玩家控制黑色棋子,另一个玩家控制白色(或红色)棋子。控制黑色棋子的玩家进行第一步。

最简单的举动是,如果没有其他玩家占据该位置,则允许棋子在棋盘上对角滑动。如果有,你可能能够跳跃并捕获该玩家的作品。玩家轮流移动或跳跃(如果棋子位于右侧,则可以包括每回合多次跳跃),直到一个玩家到达对手的主行。这一行称为国王行或冠头。一旦进入对手的国王队,玩家的棋子就会加冕并获得向后或向前移动的能力。

当一名玩家捕获了所有对手的棋子,或者让对手没有更多的合法动作时,游戏就结束了。当双方都无法取得胜利时,比赛以平局结束。

应对数据结构约束

如果你使用Java,Rust,Python甚至C ++等高级编程语言构建这个游戏,那么你可能会有一个非常不同的方法来设置游戏所需的数据结构而不是你将要使用的在WebAssembly中。

根据上一节中描述的规则,您将需要保持8×8游戏板的状态。这个游戏板上的位置可以是空的,也可以是黑色或白色的。在本节中,您将学习如何使用该语言可用的有限数据结构在WebAssembly中管理此类状态。

如果你正在玩主游戏,那么当你接触到更基本的WebAssembly语言原语和概念时,你将在整个章节中创建函数。首先,让我们为我们的项目创建一个目录。我打电话给我的是wasmcheckers,但你可以选择任何东西。在这个低级别编写代码的一个优点是我们最终只会得到一个文本文件 - 没有项目文件,没有Makefile,只有文本。

在项目目录中创建checkers.wat文件并定义一个空模块:

(module
(memory $mem 1)
Chapter 2. Building WebAssembly Checkers • 20
d exclusively for luming discuss)

内存声明中的1表示名为$ mem的内存必须至少分配一个64KB的内存页。 内存可以根据Wasm模块或主机的请求增长。

您可以使用wat2wasm编译它,然后使用wasmobjdump检查内容,然后再继续查看状态管理的详细信息。 您应该看到一些如下所示的输出:

$ wasm-objdump checkers.wasm -x
checkers.wasm: file format wasm 0x1
Section Details:
Memory:
- memory[0] pages: initial=1

当您继续阅读本章时,如果您对事情感到好奇,您可以随时编译,然后使用这些工具来检查已编译模块中的内容。

管理游戏板状态

在管理游戏板状态时,首先要解决的问题显然是董事会本身。如上所述,棋盘是8×8网格。这可能会触发程序员大脑中想要声明二维数组的部分。在Rust中,可能看起来像这样:

let mut checkerboard: [[GamePiece; 8]; 8];

这就是大多数开发人员倾向于将这个问题可视化的方法,除了语法上的差异。但这就是问题:WebAssembly没有array-singledimension或其他。它也没有复杂的类型,因此您无法创建结构或元组甚至是名为GamePiece的哈希映射。

WebAssembly确实拥有的一件事是线性内存。正如我们在前一章中讨论的那样,WebAssembly可以具有可以写入,读取,导入或导出的命名的,连续的内存块。因此,如果您要使用线性内存块,那么如何在该空间中表示二维数组呢?

解决方案是线性化二维阵列。许多您喜欢的编程语言可能已经进行了线性化以提高效率,而您却没有注意到。线性化的技巧是找出将(x,y)坐标对转换为内存偏移的数学运算。

在此图中,您可以看到一些块表示内存中各个位置的值。 其中一些是游戏板上相应的(x,y)坐标,下面你会看到那块内存的单位偏移量。 例如,板上的坐标(0,0)是单位偏移0.游戏板上的坐标(7,0)是单位偏移7,坐标(7,1)是单位偏移15,所以 上:

在这里插入图片描述

您可能检测到了一个模式,该模式的等式为offset =(x + y * 8),其中8是一行中的平方数。如果您只是将二维数组线性化为索引为数组的一维空间,那么这样就可以了,但WebAssembly的内存不像数组那样被索引。它是按字节索引的。

这意味着如果你要在游戏板上的每个点存储一个32位整数(4个字节),你需要调整方程为offset =(x + y * 8)* 4其中4是数字每单位偏移的字节数。

以这种方式思考数据存储激活了我大脑的一部分,我早就认为它死了,并且被蜘蛛网覆盖。这需要一点点习惯,与你习惯的相比,它可能感觉像是一个严格的约束,但如果你坚持这一点直到本章的结尾,你会看到在这些约束条件下的操作如何得到回报。

整个检查器游戏的基础将是一个确定给定X和Y坐标的字节偏移的函数,因为这将是您的代码每次更新电路板时需要做的事情(这是在模块声明中) ,在内存声明后):

wasmcheckers/checkers.wat
(func $indexForPosition (param $x i32) (param $y i32) (result i32)
(i32.add
(i32.mul
(i32.const 8)
(get_local $y)
)
(get_local $x)
)
)
;; Offset = ( x + y * 8 ) * 4
(func $offsetForPosition (param $x i32) (param $y i32) (result i32)
(i32.mul
(call $indexForPosition (get_local $x) (get_local $y))
(i32.const 4)
)
)

这里执行的数学归结为以下内容:

offsetForPosition(1, 2)
= (1 + 2 * 8) * 4
= 68

乘法和加法运算符的嵌套有点难以阅读,但它仍然可以管理。 有了确定游戏状态的位置的能力,下一步就是弄清楚如何表示该状态。

有趣的比特标志

在线性存储器的每个位置,您可以访问32位(或4个字节)。 您没有复杂结构或任何语言基元来存储列表,字段或元组。 你刚刚得到原始位,那么你将如何表示游戏状态?

这使得来自模式库的另一种技术称为位标志。 位标志是一种用于为数字内的各个位分配含义的技术。当多个连续位组合以存储某些其他含义时,通常称为位屏蔽。 我们要做的大部分工作都是位标志,尽管我们会简单地介绍屏蔽。

在这里插入图片描述

你可以看到8位数字00101101,但不是简单地表示数值常数45,这实际上是一组布尔值。 如图所示,值45在虚构视频游戏的上下文中具有以下所有含义:
•此游戏对象是玩家
•游戏对象未亮起
•教程已完成
•治疗师已被发现
•精灵王没有被杀
•玩家可以使用火魔法
•玩家还没有解开噩梦的洞穴
•玩家不是游戏大师

当您以这种方式描述某些数据结构时,最终会传递简单的原始数值来表示玩家和游戏对象,而不是像结构或枚举这样的高级构造。

鉴于比特标志或屏蔽等策略,您可以将多少信息打包到一个数字中,这有点令人惊讶。在所有更高级别的语言之下,许多结构都被密集地打包成数字,就像这样。工具和编译器代表您完成所有这些工作是一个反复出现的主题,您现在正在查看您熟悉的机器中的各个齿轮。

您可以对数字使用按位逻辑运算符来查询和操作这些位值。例如,按位AND运算将一个数字的每个位与第二个数字的相应位进行比较,如果相同相对位置的两个位都为1,则结果数中的位将为1.否则,输出位当两个比较位中的任何一个为1时,按位OR产生1,而按位XOR执行异或或比较位。

使用位掩码或“打包”值时,以下快速参考表将非常方便:
在这里插入图片描述

到目前为止,您已经编写了一个函数,可以将笛卡尔棋盘坐标转换为内存地址,并且您已经了解了如何将大量数据打包成简单的整数以进行存储和检索。 现在,您可以在棋盘上为整数位分配含义:

在这里插入图片描述

使用这些位标志,你知道一个加冠的黑色部分的值为5(加冠+黑色),加冠的白色部分的值为6(加冠+白色),并且板上的所有空白空间都将被表示 按0。 如果您有敏锐的眼光,您可能已经注意到这个位打包的数据结构在技术上允许一块同时是白色和黑色(值为3)。 保持作品颜色互斥必须是你用代码强制执行的东西,尽管你也可以使用不同的位标记含义来避免这种情况。

按位与常规数学 您可能已经注意到,要在位掩码值中激活多个布尔标志,您可以简单地将两个标志一起添加,如Black(1)和Crowned(4)等于5的情况。这是一个坏主意,因为此操作 不是幂等的。 如果将Crown标志添加到数字两次,则基本上已损坏您的状态。当你使用i32.or激活一个标志时,这是幂等的,你可以一遍又一遍地做,而不会破坏其余的数字。

一旦确定了每个位的含义,就可以将一些函数写入查询集,并从一个片段中删除这些值。 在这种情况下,“piece”实际上只不过是一个4字节的整数值:

wasmcheckers/checkers.wat
;; Determine if a piece has been crowned
(func $isCrowned (param $piece i32) (result i32)
(i32.eq
(i32.and (get_local $piece) (get_global $CROWN))
(get_global $CROWN)
)
)
;; Determine if a piece is white
(func $isWhite (param $piece i32) (result i32)
(i32.eq
(i32.and (get_local $piece) (get_global $WHITE))
(get_global $WHITE)
)
)
;; Determine if a piece is black
(func $isBlack (param $piece i32) (result i32)
(i32.eq
(i32.and (get_local $piece) (get_global $BLACK))
(get_global $BLACK)
)
)
;; Adds a crown to a given piece (no mutation)
(func $withCrown (param $piece i32) (result i32)
(i32.or (get_local $piece) (get_global $CROWN))
)
;; Removes a crown from a given piece (no mutation)
(func $withoutCrown (param $piece i32) (result i32)
(i32.and (get_local $piece) (i32.const 3))
)

这段代码依赖于不可变的全局值,如$ CROWN,$ BLACK和$ WHITE,它们就像其他语言中的常量一样。 这些在模块的顶部定义,如下所示:

(global $WHITE i32 (i32.const 2))
(global $BLACK i32 (i32.const 1))
(global $CROWN i32 (i32.const 4))

withoutCrown函数有点棘手。 我们不能使用带冠部位(4)的XOR来强制它为零。 相反,它会切换它,设置一个尚未加冕的部分。 此功能的目的是比较两件,无论其表冠状态如何。 这样做的安全方法是使用带有掩码的AND,该掩码仅返回黑色和白色位并忽略所有其他位。

对我来说,作为一个没有全天思考二进制和按位操作的人,这很难想象。 看看下面的真值表,说明了它的工作原理:

在这里插入图片描述

$ withCrown在相同的bitmasking主体上工作,只是使用不同的运算符。这段代码调用$ piece变量中的位和32位常量4中的位。参考我们方便的位掩码参考图表,我们知道OR位操作用于设置位。因此,右边第三个位(22)中的位将设置为1并返回新值。

重要的是要记住,你还没有(还)影响存储状态,你只是在玩数字内的位。此时,您已经创建了一些函数,这些函数对简单整数执行位屏蔽操作,以便在跳棋游戏中赋予它们上下文含义。

在我们继续编写更多代码之前,能够测试和试验我们正在编写的函数会很不错。由于您使用的是原始的wast语法,因此没有简单的单元测试框架或代码生成可用。最简单的方法就是导出我们正在处理的所有函数,这样我们就可以从JavaScript中调用它们,直到我们满意它们按预期工作。

首先,将$ offsetForPosition,$ indexForPosition和所有位掩码函数放入名为func_test.wat的测试模块中:

wasmcheckers/func_test.wat
(module
(memory $mem 1)
(global $WHITE i32 (i32.const 2))
(global $BLACK i32 (i32.const 1))
(global $CROWN i32 (i32.const 4))
(func $indexForPosition (param $x i32) (param $y i32) (result i32)
(i32.add
(i32.mul
(i32.const 8)
(get_local $y)
)
(get_local $x)

)
)
;; Offset = ( x + y * 8 ) * 4
(func $offsetForPosition (param $x i32) (param $y i32) (resul
(i32.mul
(call $indexForPosition (get_local $x) (get_local $y))
(i32.const 4)
)
)
;; Determine if a piece has been crowned
(func $isCrowned (param $piece i32) (result i32)
(i32.eq
(i32.and (get_local $piece) (get_global $CROWN))
(get_global $CROWN)
)
)
;; Determine if a piece is white
(func $isWhite (param $piece i32) (result i32)
(i32.eq
(i32.and (get_local $piece) (get_global $WHITE))
(get_global $WHITE)
)
)
;; Determine if a piece is black
(func $isBlack (param $piece i32) (result i32)
(i32.eq
(i32.and (get_local $piece) (get_global $BLACK))
(get_global $BLACK)
)
)
;; Adds a crown to a given piece (no mutation)
(func $withCrown (param $piece i32) (result i32)
(i32.or (get_local $piece) (get_global $CROWN))
)
;; Removes a crown from a given piece (no mutation)
(func $withoutCrown (param $piece i32) (result i32)
(i32.and (get_local $piece) (i32.const 3))
)
(export "offsetForPosition" (func $offsetForPosition))
(export "isCrowned" (func $isCrowned))
(export "isWhite" (func $isWhite))
(export "isBlack" (func $isBlack))
(export "withCrown" (func $withCrown))
(export "withoutCrown" (func $withoutCrown))
)

使用wat2wasm将其编译成func_test.wasm,然后创建一个JavaScript包装器,它将加载WebAssembly模块并执行导出的函数,这样我们就可以确保它们按预期工作:

wasmcheckers/func_test.js
fetch('./func_test.wasm').then(response =>
response.arrayBuffer()
).then(bytes => WebAssembly.instantiate(bytes)).then(results => {
console.log("Loaded wasm module");
instance = results.instance;
console.log("instance", instance);
var white = 2;
var black = 1;
var crowned_white = 6;
var crowned_black = 5;
console.log("Calling offset");
var offset = instance.exports.offsetForPosition(3,4);
console.log("Offset for 3,4 is ",offset);
console.debug("White is white?", instance.exports.isWhite(white));
console.debug("Black is black?", instance.exports.isBlack(black));
console.debug("Black is white?", instance.exports.isWhite(black));
console.debug("Uncrowned white",
instance.exports.isWhite(instance.exports.withoutCrown(crowned_white)));
console.debug("Uncrowned black",
instance.exports.isBlack(instance.exports.withoutCrown(crowned_black)));
console.debug("Crowned is crowned", instance.exports.isCrowned(crowned_black));
console.debug("Crowned is crowned (b)", instance.exports.isCrowned(crowned_white));
});

最后,我们可以创建一个启动func_test.js脚本的index.html文件:

wasmcheckers/func_test.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {
background-color: rgb(255, 255, 255);
}
</style>
</head>
<body>
<span id="container"></span>
<script src="./func_test.js"></script>
</body>
</html>

由于浏览器中的跨站点脚本规则,它可能不允许您在没有HTTP服务器的情况下从文件系统打开文件(我的Ubuntu上的Firefox不会,但显然是Arch Linux上的Firefox),所以启动你的 最喜欢的一个,或者你可以使用Python在与HTML文件相同的目录中启动这个轻量级的:

$ python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

现在打开浏览器localhost:8000 / func_test.html。 打开JavaScript调试控制台,您应该看到类似以下输出的内容,验证我们是否获得了我们期望的值:

Loaded wasm module
instance WebAssembly.Instance { exports: {…} }
Calling offset
Offset for 3,4 is 140
White is white 1
Black is black 1
Black is not white 0
Uncrowned white 1
Uncrowned black 1
Crowned is crowned 1
Crowned is crowned (b) 1

虽然让人放心(我们的代码可以工作),但这也是一套非常枯燥乏味的控制台输出。 随着我们在本书中的进一步发展,您将能够生成真实的,面向用户的状态,游戏板和其他模块内部的Web可视化。

在本章的过程中,您只需将您喜欢的任何功能复制并粘贴到此工具中,确保将其导出,然后使用JavaScript进行测试。 在下一节中,您将构建到目前为止在checkers.wat中编码的内容,以开始阅读和编写游戏状态以管理棋盘。

操纵董事会

您要编写的下几个函数涉及在线性存储器中存储和检索游戏板网格中的值,并在允许状态变化之前对参数和数据执行一些验证检查:

wasmcheckers/checkers.wat
;; Sets a piece on the board.
(func $setPiece (param $x i32) (param $y i32) (param $piece i32)
(i32.store
(call $offsetForPosition
(get_local $x)
(get_local $y)
)
(get_local $piece)
)
)
;; Gets a piece from the board. Out of range causes a trap
(func $getPiece (param $x i32) (param $y i32) (result i32)
(if (result i32)
(block (result i32)
(i32.and
(call $inRange
(i32.const 0)
(i32.const 7)
(get_local $x)
)
(call $inRange
(i32.const 0)
(i32.const 7)
(get_local $y)
)
)
)
(then
(i32.load
(call $offsetForPosition
(get_local $x)
(get_local $y))
)
)
(else
(unreachable)
)
)
)
;; Detect if values are within range (inclusive high and low)
(func $inRange (param $low i32) (param $high i32) (param $value i32) (result i32)
(i32.and
(i32.ge_s (get_local $value) (get_local $low))
(i32.le_s (get_local $value) (get_local $high))
)
)

$ setPiece函数有一些新的语法,这是您的代码首先将一个函数的返回值用作另一个函数的参数。 $ setPiece调用i32.store,它在内存地址中存储一个32位整数。 内存地址将是通过调用$ offsetForPosition返回的值,存储的值将是参数$ piece。 例如,要在网格位置(5,5)设置白色部分,可以调用(i32.store 200 2)。

$ getPiece说明了第一次使用带有函数调用的if块作为谓词。 如果谓词执行函数,则需要指定将从谓词返回的值的类型。 block关键字简单地包装一个或多个语句并指示该块的返回类型,类似于匿名函数。 if / then / else块的一般结构也在谓词子句中执行代码,如下所示:

(if (result i32) (block (result i32) ...)
(then ... )
(else ... )
)

最后,$ inRange函数用于防止超出游戏板边缘的查询。 大多数高级编程语言都具有内置功能,可以防止这种情况发生,或者会抛出越界运行时异常。 如果没有这些奢侈品,在WebAssembly中你必须手动确保永远不会访问超出给定数据结构范围的内存。

然而,值得指出的是,即使WebAssembly中的索引越界错误也永远不会超出其线性内存的限制。 这使堆损坏的可能性非常低。

检查失误的边界 在整个计算历史中,未能在汇编级别正确检查边界导致了一些灾难性的错误,恶意病毒和一些惊人的视频游戏漏洞。 我最喜欢的一个是来自塞尔达传说的原始Famicom版本,它展示了超越隐含边界的阅读如何将原始数据转换为可执行指令,直接传送到游戏结束。

Keeping Track of the Current Turn

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值