基本用法
声明
(module
(memory 1)
)
代码memory 1
声明了一个memory内存区域,其初始尺寸为1页(page size),即64k字节,也即64 × 1024 = 65536个字节。
可使用第2个立即数来声明内存的最大尺寸。
(module
(memory 1 5)
)
上面所声明的内存,其容量最小1页,最大5页。最大容量的字节数不能超过4G,因此最大值应小于等于65536页。
查看memory的状态
可使用memory的size指令返回内存区域的大小。
let watSrc = `
(module
(memory 2)
(func $sum (export "memsize") (result i32)
memory.size
)
)
`;
const { memsize } = await WabtUtils.RunWat(watSrc);
pc.log(memsize());
也可直接导出memory至JavaScript读写其内容。
let watSrc = `
(module
(memory $mem1 1 3)
(export "memory" (memory $mem1))
)
`;
const { memory } = await WabtUtils.RunWat(watSrc);
pc.log(memory);
let typeObj = memory.type();
pc.log(typeObj);
let buffer = memory.buffer;
let byteOffset = 0;
let length = 20;
let tarr = new Uint8Array(buffer, byteOffset, length);
for (let i = byteOffset; i < length; i++) {
tarr[i] = i * 2;
}
pc.log(tarr);
Wasm的memory对应于JavaScript的WebAssembly.Memory类,其buffer属性的类型为ArrayBuffer,因此可通过相应的TypedArray或DataView等视图来读写其数据。
由于Wasm的memory的字长为65536个字节,比较大,故上面代码只设置前20个字节的内容。
存储与读取单个数值
const { getHexAddrStrFromTypedArray } = await import('/js/esm/BinUtils.js');
let watSrc = `
(module
(memory $mem1 (export "memory") 1)
(func $store
(i32.store (i32.const 0) (i32.const 15))
)
(func (export "load") (result i32)
(i32.load (i32.const 0))
)
(start $store)
)
`;
const { memory, load } = await WabtUtils.RunWat(watSrc);
let num = load();
pc.log(num);
let tarr = new Uint8Array(memory.buffer, 0, 8);
pc.log('%s', getHexAddrStrFromTypedArray(tarr));
函数$store将$mem1内存区域第0个字节的值设置为常数15。函数$load取出该内存区域第0个字节的数值。
start指令在Wasm模块完成初始化、且内存区域也已初始化后,自动调用$store函数,用于设置其值。
而在客户端中,调用所导出的load函数来加载该内存区域第0个字节的数值并打印出来,并打印内存前8个字节的状态。
alignment
操作数在内存中对齐仅是一种优化机制,而不是强制性的。
当在store中指定align=n
时,编译器将确保内存地址的边界满足该需求。但如果指定不当,则可能会导致抛出异常,或导致应用效能损失。
当在load中指定align=n
时,不会影响实际加载的结果。但如果指定不当,则可能会导致触发陷阱,或导致应用效能损失。
对齐参数为2n次幂,值为1, 2, 4, 8, 16等等。
对齐参数不能大于操作数的位数。例如,i64的字长为8字节,则对齐参数可以为1, 2, 4, 8。
i32的默认对齐参数为align=4
,i64的默认对齐参数为align=8
,v128的默认对齐参数为align=16
。
const { getHexAddrStrFromTypedArray, getBinStr } = await import('/js/esm/BinUtils.js');
let watSrc = `
(module
(memory (export "memory") 1)
(func $store
(v128.store offset=4 align=16 (i32.const 0) (v128.const i32x4 1 2 3 4))
(v128.store offset=4 align=16 (i32.const 16) (v128.const i32x4 5 6 7 8))
)
(func (export "load") (result i32)
(i32.load (i32.const 4))
)
(start $store)
)
`;
const { memory, load } = await WabtUtils.RunWat(watSrc);
let tarr = new Uint8Array(memory.buffer, 0, 64);
pc.log(tarr);
let str = getHexAddrStrFromTypedArray(tarr, 16);
pc.log('%s', str);
let num = load();
pc.log(num);
pc.log('%s', getBinStr(num));
最佳实践是使用默认对齐。但有时在强调地址边界时,可使用align=n
来明示边界对齐的宽度。
初始化内存
可以使用data段将内存内容使用字符串进行初始化。
以字符串字面符初始化
(module
(memory 1)
(data (i32.const 0) "Hello")
)
因为目前的实现只支持1个memory,因此i32.const 0
并非指定使用第几个内存,而是指定从默认唯一的内存的第0个字节的偏移处开始,将其内容设置为Hello
的内容。此时memory的大小不能为0。
在 JS 中查看字符串
let watSrc = `
(module
(memory (export "memory") 1)
(data (i32.const 0) "Hello")
)
`;
const { memory } = await WabtUtils.RunWat(watSrc);
let tarr = new Uint8Array(memory.buffer, 0, 5);
pc.log(tarr);
let str = new TextDecoder("utf-8").decode(tarr);
pc.log(str);
在JavaScript环境中查看memory的内容时,由于memory的尺寸过大,因此我们需要知道在该内存中有内容的起始字节处,以及终止字节处。Wasm并未提供自动计算在内存中有实际内容大小的指令。因此上面的代码以手工的方式计算了Hello
共计5个字节。
我们可以在JavaScript中寻求解决方案。思路是通过类型化数组的findIndex方法来找到第一个NUL字符的位置,根据此位置,再调用slice方法来切割出有实际内容的区域。
let watSrc = `
(module
(memory (export "memory") 1)
(data (i32.const 0) "Hello")
)
`;
const { memory } = await WabtUtils.RunWat(watSrc);
let tarr = new Uint8Array(memory.buffer);
let nulCharIndex = tarr.findIndex(value => value === 0);
pc.log(nulCharIndex);
tarr = tarr.slice(0, nulCharIndex);
pc.log(tarr);
let str = new TextDecoder("utf-8").decode(tarr);
pc.log(str);
findIndex方法一旦找到符合条件的第一个元素,将立即返回该元素的索引值,因此这种算法较为高效。
同时初始化多个字节处的内容
可以同时声明多个data段来初始化不同偏移处的内容。
let watSrc = `
(module
(memory (export "memory") 1)
(data (i32.const 0) "JavaScript")
(data (i32.const 15) "Python")
(data (i32.const 30) "Wasm")
)
`;
const { memory } = await WabtUtils.RunWat(watSrc);
let tarr = new Uint8Array(memory.buffer);
let lastCharIndex = tarr.findLastIndex(value => value !== 0);
tarr = tarr.slice(0, lastCharIndex + 1);
pc.log(tarr);
let str = new TextDecoder("utf-8").decode(tarr);
pc.log(str);
使用3个data段,在不同偏移值的地方分别声明了3个字符串。
在自动计算有效内容大小时,因多个字符串之间可能夹有NUL字符,因此只能调用findLastIndex方法来找到最后一个不为NUL字符的字符位置。
理论上讲,这种算法在实际内容较少时,迭代次数将较多,效率也较为低下。此时只能寄希望于JavaScript的findLastIndex方法在其内部有优化算法。
但从实际运行效果来看,速度还是较快的。
此外,内存中间所混杂的NUL字符,经TextDecoder解码后均消失了。
被动数据段的初始化
在声明data段时,如果不指定偏移值,则属于被动 (passive) 的数据段,其数据不会自动填充memory。此时,可调用memory的init指令,将指定数据段的相应数据,复制到memory中。
let watSrc = `
(module
(memory (export "memory") 1)
(data "Hello, World!") ;; the 0th data segment
(func $init_memory
(memory.init 0 ;; from 0th data segment
(i32.const 2) ;; memory target offset
(i32.const 7) ;; data segment offset
(i32.const 5) ;; bytes
)
)
(start $init_memory)
)
`;
const { memory } = await WabtUtils.RunWat(watSrc);
let tarr = new Uint8Array(memory.buffer, 0, 10);
pc.log(tarr);
let str = new TextDecoder("utf-8").decode(tarr);
pc.log(str);
memory.init指令的第1个参数为立即数,指定填充数据来自哪个数据段;第2个参数指定要复制到memory的偏移值;第3个参数指定从数据段开始复制的偏移值;第4个参数指定要复制的字节数量。
最后,取出memory最前面的10个字节,按utf-8
来解码为并打印出来。
可以同时使用多个被动数据段的数据来填充同一默认内存区域。
let watSrc = `
(module
(memory (export "memory") 1)
(data "Hello, World!") ;; the 0th data segment
(data "Python is good.") ;; the 1st data segment
(func $init_memory
(memory.init 0 ;; from 0th data segment
(i32.const 0) ;; memory target offset
(i32.const 0) ;; data segment offset
(i32.const 7) ;; bytes
)
(memory.init 1 ;; from 1st data segment
(i32.const 8) ;; memory target offset
(i32.const 0) ;; data segment offset
(i32.const 6) ;; bytes
)
)
(start $init_memory)
)
`;
const { memory } = await WabtUtils.RunWat(watSrc);
let tarr = new Uint8Array(memory.buffer, 0, 14);
pc.log(tarr);
let str = new TextDecoder("utf-8").decode(tarr);
pc.log(str);
这种方式,可灵活、自由地组合使用多个数据段中的数据。
被动数据段数据的舍弃
let watSrc = `
(module
(memory (export "memory") 1)
(data "Hello") ;; the 0th data segment
(func $init_memory
(memory.init 0 ;; from 0th data segment
(i32.const 0) ;; memory target offset
(i32.const 0) ;; data segment offset
(i32.const 5) ;; bytes
)
data.drop 0 ;; drop the data
;; of the 0th data segment
)
(start $init_memory)
)
`;
const { memory } = await WabtUtils.RunWat(watSrc);
let tarr = new Uint8Array(memory.buffer);
let str = new TextDecoder("utf-8").decode(tarr);
pc.log(str);
第0个data段是被动数据段,其内容用以初始化memory后,可显式调用data.drop 0
,将第0个data段的数据舍弃。
其作用有二:一是其后该段数据不能再被使用。二是起到优化作用。在内存紧张时,Wasm可随时清理该数据段的内存。
数值的初始化
当memory的内容为数值类型时,可通过相应数据类型的store指令予以初始化。
const { getHexAddrStrFromTypedArray, getHexStr, getBinStr } = await import('/js/esm/BinUtils.js');
let watSrc = `
(module
(memory (export "memory") 1)
(func $store
(i32.store (i32.const 0) (i32.const 1))
(i32.store (i32.const 1) (i32.const 2))
(i32.store (i32.const 2) (i32.const 3))
(i32.store (i32.const 3) (i32.const 4))
)
(func (export "load") (result i32)
(i32.load (i32.const 0))
)
(start $store)
)
`;
const { memory, load } = await WabtUtils.RunWat(watSrc);
let tarr = new Uint8Array(memory.buffer, 0, 8);
pc.log(tarr);
let str = getHexAddrStrFromTypedArray(tarr, 8);
pc.log('%s', str);
let num = load();
pc.log(num);
pc.log('%s', getHexStr(num));
pc.log('%s', getBinStr(num));
在4个连续的字节处,分别存储了4个数值;读取数据时,则连续读取了4个字节的内容,并解读为一个整数值。
上例可看到,Wasm的memory的存储及解读是按小尾来进行的。
i32虽为4个字节的指令,但由于其store指令需指定一个字节偏移处,而上面的代码每次只偏移了一个字节,因此实际上均覆盖了前一个i32在内存中的高位内容。
下面代码偏移2个字节再存储另一个数值,则得到了不同的数值。
const { getHexAddrStrFromTypedArray, getHexStr, getBinStr } = await import('/js/esm/BinUtils.js');
let watSrc = `
(module
(memory (export "memory") 1)
(func $store
(i32.store (i32.const 0) (i32.const 1))
(i32.store (i32.const 2) (i32.const 2))
)
(func (export "load") (result i32)
(i32.load (i32.const 0))
)
(start $store)
)
`;
const { memory, load } = await WabtUtils.RunWat(watSrc);
let tarr = new Uint8Array(memory.buffer, 0, 4);
pc.log(tarr);
let str = getHexAddrStrFromTypedArray(tarr, 4);
pc.log('%s', str);
let num = load();
pc.log(num);
pc.log('%s', getHexStr(num));
pc.log('%s', getBinStr(num));
整数数值存储
整数的存储
调用各个数据类型的store指令以在内存中存储数据。
const { getHexAddrStrFromTypedArray, getHexStr } = await import('/js/esm/BinUtils.js');
let watSrc = `
(module
(memory (export "memory") 1)
(func $store
(i32.store (i32.const 0) (i32.const 0x10)) ;; 4 bytes
(i32.store (i32.const 4) (i32.const 0x20)) ;; 4 bytes
(i64.store (i32.const 8) (i64.const 0x30)) ;; 8 bytes
(i64.store (i32.const 16) (i64.const 0x40)) ;; 8 bytes
)
(func (export "load32") (param $idx i32) (result i32)
(i32.load (local.get $idx))
)
(func (export "load64") (param $idx i32) (result i64)
(i64.load (local.get $idx))
)
(start $store)
)
`;
const { memory, load32, load64 } = await WabtUtils.RunWat(watSrc);
let tarr = new Uint8Array(memory.buffer, 0, 32);
let str = getHexAddrStrFromTypedArray(tarr, 8);
pc.log('%s', str);
let num1 = load32(0);
pc.log('%d, %s', num1, getHexStr(num1));
let num2 = load32(4);
pc.log('%d, %s', num2, getHexStr(num2));
let num3 = load64(8);
pc.log('%d, %s', num3, getHexStr(num3));
let num4 = load64(16);
pc.log('%d, %s', num4, getHexStr(num4));
首先,与设置各种数据类型单个值不同,由于是在一连串的内存区域中设置值,因此store指令的参数需同时指定在内存中的偏移值以及数值。
其次,从内存存储状态来看,Wasm按小尾方式来存储数据。
第三,在加载数据时,load指令也需指定在内存中的偏移值。
未正确指定偏移值的存储
在存储内存数值时,如果未能正确指定偏移值,则会引发不易觉察的错误。
const { getHexAddrStrFromTypedArray, getHexStr } = await import('/js/esm/BinUtils.js');
let watSrc = `
(module
(memory (export "memory") 1)
(func $store
(i32.store (i32.const 0) (i32.const 0x10)) ;; 4 bytes
(i32.store (i32.const 1) (i32.const 0x20)) ;; 4 bytes
)
(func (export "load32") (param $idx i32) (result i32)
(i32.load (local.get $idx))
)
(start $store)
)
`;
const { memory, load32} = await WabtUtils.RunWat(watSrc);
let tarr = new Uint8Array(memory.buffer, 0, 16);
let str = getHexAddrStrFromTypedArray(tarr, 8);
pc.log('%s', str);
let num1 = load32(0);
pc.log('%d, %s', num1, getHexStr(num1));
let num2 = load32(1);
pc.log('%d, %s', num2, getHexStr(num2));
let num3 = load32(4);
pc.log('%d, %s', num3, getHexStr(num3));
先在偏移值为0的地方存储了一个32位的数值0x10,而在存储第2个32位的数值时,本应在偏移值为4的地方存储,实际上却在偏移值为1的地方存储,结果第1个数值的内存区域被覆盖了,从而导致加载第1个数值时,其值不再是0x10。
而第2个数值若在偏移值为1的地方加载,则没有问题。但如果在偏移值本应为4的地方加载,也会出现错误。
只存储部分位域的数值
i32的总位数为32位,但也可调用其store8指令来存储8位的整数, 调用其store16指令来存储16位的整数。
const { getHexAddrStrFromTypedArray, getHexStr } = await import('/js/esm/BinUtils.js');
let watSrc = `
(module
(memory (export "memory") 1)
(func $store
(i32.store8 (i32.const 0) (i32.const 0x11223344))
(i32.store16 (i32.const 4) (i32.const 0x11223344))
(i32.store (i32.const 8) (i32.const 0x11223344))
)
(func (export "load32") (param $idx i32) (result i32)
(i32.load (local.get $idx))
)
(start $store)
)
`;
const { memory, load32} = await WabtUtils.RunWat(watSrc);
let tarr = new Uint8Array(memory.buffer, 0, 12);
let str = getHexAddrStrFromTypedArray(tarr, 4);
pc.log('%s', str);
let num1 = load32(0);
pc.log('%d, %s', num1, getHexStr(num1));
let num2 = load32(4);
pc.log('%d, %s', num2, getHexStr(num2));
let num3 = load32(8);
pc.log('%d, %s', num3, getHexStr(num3));
这种只存储部分位域的指令,只保留相应低权重位部分的数值,高权重位的部分直接被舍弃。
i64的情况也类似于此。可调用其store8指令来存储8位的整数, 调用其store16指令来存储16位的整数, 调用其store32指令来存储32位的整数。
const { getHexAddrStrFromTypedArray, getHexStr } = await import('/js/esm/BinUtils.js');
let watSrc = `
(module
(memory (export "memory") 1)
(func $store
(i64.store8 (i32.const 0) (i64.const 0x1122334455667788))
(i64.store16 (i32.const 8) (i64.const 0x1122334455667788))
(i64.store32 (i32.const 16) (i64.const 0x1122334455667788))
(i64.store (i32.const 24) (i64.const 0x1122334455667788))
)
(func (export "load64") (param $idx i32) (result i64)
(i64.load (local.get $idx))
)
(start $store)
)
`;
const { memory, load64} = await WabtUtils.RunWat(watSrc);
let tarr = new Uint8Array(memory.buffer, 0, 32);
let str = getHexAddrStrFromTypedArray(tarr, 8);
pc.log('%s', str);
let num1 = load64(0);
pc.log('%d, %s', num1, getHexStr(num1));
let num2 = load64(8);
pc.log('%d, %s', num2, getHexStr(num2));
let num3 = load64(16);
pc.log('%d, %s', num3, getHexStr(num3));
let num4 = load64(24);
pc.log('%d, %s', num4, getHexStr(num4));
memory的指令
fill
fill指令在指定的内存区域填充指定的单个字节的数值。
const { getHexAddrStrFromTypedArray } = await import('/js/esm/BinUtils.js');
let watSrc = `
(module
(memory (export "memory") 1)
(func $store
(memory.fill
(i32.const 2) ;; start offset
(i32.const 3) ;; byte value
(i32.const 5) ;; bytes length
)
)
(start $store)
)
`;
const { memory } = await WabtUtils.RunWat(watSrc);
let tarr = new Uint8Array(memory.buffer, 0, 16);
let str = getHexAddrStrFromTypedArray(tarr, 8);
pc.log('%s', str);
fill指令共有3个参数,第1个参数指定内存区域偏移值,第2个参数指定要填充的数值,第3个参数指定共填充多少个字节。这3个参数的类型都只能为i32。
上面代码从第2个字节开始,将各字节的值填充为3,共填充5个字节。
第2个参数要填充的数值中,如果有效位域超过1个字节,则只取最低权重位的1个字节的数值。
const { getHexAddrStrFromTypedArray } = await import('/js/esm/BinUtils.js');
let watSrc = `
(module
(memory (export "memory") 1)
(func $store
(memory.fill
(i32.const 2)
(i32.const 0x66778899) ;; take only the byte value
;; from the lowest significant byte
(i32.const 5)
)
)
(start $store)
)
`;
const { memory } = await WabtUtils.RunWat(watSrc);
let tarr = new Uint8Array(memory.buffer, 0, 16);
let str = getHexAddrStrFromTypedArray(tarr, 8);
pc.log('%s', str);
copy
copy指令将内存特定区域中的数据复制到另一区域。
const { getHexAddrStrFromTypedArray } = await import('/js/esm/BinUtils.js');
let watSrc = `
(module
(memory (export "memory") 1)
(func $store
(memory.fill (i32.const 0) (i32.const 0x11) (i32.const 8))
(memory.copy
(i32.const 9) ;; target offset
(i32.const 0) ;; source offset
(i32.const 5) ;; byte length
)
)
(start $store)
)
`;
const { memory } = await WabtUtils.RunWat(watSrc);
let tarr = new Uint8Array(memory.buffer, 0, 16);
let str = getHexAddrStrFromTypedArray(tarr, 8);
pc.log('%s', str);
copy指令共有3个参数,第1个参数指定目标偏移值,第2个参数指定源偏移值,第3个参数指定要复制字节数量。这3个参数的类型都只能为i32。
目标区域与源区域可以相互重叠。
const { getHexAddrStrFromTypedArray } = await import('/js/esm/BinUtils.js');
let watSrc = `
(module
(memory (export "memory") 1)
(func $store
(i64.store (i32.const 0) (i64.const 0x1122334455667788))
(i64.store (i32.const 8) (i64.const 0x1122334455667788))
(memory.copy
(i32.const 0) ;; target offset
(i32.const 3) ;; source offset
(i32.const 5) ;; byte length
)
)
(start $store)
)
`;
const { memory } = await WabtUtils.RunWat(watSrc);
let tarr = new Uint8Array(memory.buffer, 0, 16);
let str = getHexAddrStrFromTypedArray(tarr, 8);
pc.log('%s', str);
从第3个字节开始,复制5个字节至第0个字节处,目标区域与源区域有部分重叠。复制完成后,重叠的源区域的内容被覆盖。
size
size指令返回表示内存大小的数值。
let watSrc = `
(module
(memory (export "memory") 1)
(func (export "memsize") (result i32)
memory.size
)
)
`;
const { memory, memsize } = await WabtUtils.RunWat(watSrc);
pc.log('Memory size: %d', memsize());
在JavaScript中,可通过导出的memory来查看其字节总数。
let watSrc = `
(module
(memory (export "memory") 1)
(func (export "memsize") (result i32)
memory.size
)
)
`;
const { memory, memsize } = await WabtUtils.RunWat(watSrc);
pc.log('Memory size: %d', memsize());
pc.log('Buffer bytes length: %d', memory.buffer.byteLength);
pc.assert(memsize() * 64 * 1024 === memory.buffer.byteLength);
grow
grow指令将内存大小按指定的页数予以扩展,返回表示原来内存大小的数值。
let watSrc = `
(module
(memory (export "memory") 1)
(func (export "grow") (param i32) (result i32)
(memory.grow (local.get 0))
)
(func (export "memsize") (result i32)
memory.size
)
)
`;
const { memory, grow, memsize } = await WabtUtils.RunWat(watSrc);
pc.log('Memory size before growing: %d', memsize());
let result = grow(5);
pc.log('Value returned from grow(): %d', result);
pc.log('Memory size after growing: %d', memsize());