Javascript 类型数组和字节序

IT技术 javascript endianness webgl typed-arrays arraybuffer
2021-02-10 16:13:47

我正在使用 WebGL 来呈现二进制编码的网格文件。二进制文件以大端格式写出(我可以通过在十六进制编辑器中打开文件或使用 fiddler 查看网络流量来验证这一点)。当我尝试使用 Float32Array 或 Int32Array 读取二进制响应时,二进制被解释为 little-endian 并且我的值是错误的:

// Interpret first 32bits in buffer as an int
var wrongValue = new Int32Array(binaryArrayBuffer)[0];

我在http://www.khronos.org/registry/typedarray/specs/latest/ 中找不到任何对类型化数组的默认字节序的引用,所以我想知道这是怎么回事?在使用类型化数组读取时,我是否应该假设所有二进制数据都应该是小端的?

为了解决这个问题,我可以使用 DataView 对象(在上一个链接中讨论过)并调用:

// Interpret first 32bits in buffer as an int
var correctValue = new DataView(binaryArrayBuffer).getInt32(0);

默认情况下,诸如“getInt32”之类的 DataView 函数读取大端值。

(注意:我已经使用 Google Chrome 15 和 Firefox 8 进行了测试,它们的行为方式相同)

5个回答

当前行为由底层硬件的字节序决定。由于几乎所有台式计算机都是 x86,这意味着小端。大多数 ARM 操作系统使用小端模式(ARM 处理器是双端模式,因此可以在任一模式下运行)。

这有点令人难过的原因是,这意味着几乎没有人会测试他们的代码是否可以在大端硬件上运行,从而损害了它的功能,而且整个 Web 平台是围绕代码在跨实现和平台上统一工作而设计的,这打破了。

好吧,我不能声称对这个问题非常熟悉,但是从这些东西的早期开发到实现的那些人声称,在类型化数组中使用机器字节序对于与本机 API 的互操作很重要,这听起来很可靠我。我将相信,参与其中的许多不同的人,他们都非常熟悉,他们不会只是集体弄错了。:-)
2021-03-21 16:13:47
@TJCrowder 请记住,类型化数组源自 WebGL(是的,机器字节序很有用),而不是单独的提案。当它开始在 WebGL 之外使用时,几乎完全在字节序无关紧要的地方,猫已经摆脱了默认机器字节序的束缚。基本上,鉴于没有人对 big-endian 系统进行测试,您要么破坏大多数 WebGL 情况(或在传递给 GL 实现时交换字节序,我相信这是浏览器实际所做的),要么破坏大多数非 WebGL 情况。
2021-03-21 16:13:47
不知何故,我认为情况会如此。
2021-04-02 16:13:47
@TJCrowder 肯定有机器字节序的用途,但更大的问题是我们在网络上看到的类型化数组的大部分使用不需要担心底层机器字节序,如果你确实依赖机器字节序它很可能在大端系统上被破坏(假设几乎没有人会在一个系统上测试他们的 JS)。(请注意,我在撰写上述内容时在 Opera 工作,时至今日,他可能占大端系统上发布的大部分浏览器。)
2021-04-07 16:13:47
这一点也不不幸。类型化数组遵循平台的字节顺序,因为我们使用它们与本机 API 进行互操作,这些 API 以平台的字节顺序工作。如果类型化数组具有固定的字节序,我们将失去使用它们的大量好处(在与所选字节序不匹配的平台上)。对于像 OP 这样涉及文件的情况(或用于与定义特定字节序的各种协议(例如 TCP 等)进行交互),这就是DataView目的。
2021-04-11 16:13:47

仅供参考,您可以使用以下 javascript 函数来确定机器的字节序,然后您可以将适当格式的文件传递给客户端(您可以在服务器上存储文件的两个版本,大端和小端):

function checkEndian() {
    var arrayBuffer = new ArrayBuffer(2);
    var uint8Array = new Uint8Array(arrayBuffer);
    var uint16array = new Uint16Array(arrayBuffer);
    uint8Array[0] = 0xAA; // set first byte
    uint8Array[1] = 0xBB; // set second byte
    if(uint16array[0] === 0xBBAA) return "little endian";
    if(uint16array[0] === 0xAABB) return "big endian";
    else throw new Error("Something crazy just happened");
}

在您的情况下,您可能必须以小端重新创建文件,或者遍历整个数据结构以使其成为小端。使用上述方法的扭曲,您可以即时交换字节序(不是真正推荐的,只有在整个结构是相同的紧密包装类型时才有意义,实际上您可以创建一个根据需要交换字节的存根函数):

function swapBytes(buf, size) {
    var bytes = new Uint8Array(buf);
    var len = bytes.length;
    var holder;

    if (size == 'WORD') {
        // 16 bit
        for (var i = 0; i<len; i+=2) {
            holder = bytes[i];
            bytes[i] = bytes[i+1];
            bytes[i+1] = holder;
        }
    } else if (size == 'DWORD') {
        // 32 bit
        for (var i = 0; i<len; i+=4) {
            holder = bytes[i];
            bytes[i] = bytes[i+3];
            bytes[i+3] = holder;
            holder = bytes[i+1];
            bytes[i+1] = bytes[i+2];
            bytes[i+2] = holder;
        }
    }
}
@Ryan,你为什么使用 4 个字节而不是 2 个?
2021-03-28 16:13:47
好东西!我刚刚将new添加return bytes;到您的代码中。这些有助于使代码为我运行。谢谢。
2021-03-29 16:13:47
填充文本只是为了做到这一点::-D
2021-04-06 16:13:47
实际上,由于缓冲区本身已交换,因此不需要返回。
2021-04-07 16:13:47
@Maximus 这是由于 32 位,例如 Uint32ArrayBuffer
2021-04-12 16:13:47

取自这里http://www.khronos.org/registry/typedarray/specs/latest/(当该规范完全实施时)您可以使用:

new DataView(binaryArrayBuffer).getInt32(0, true) // For little endian
new DataView(binaryArrayBuffer).getInt32(0, false) // For big endian

但是,如果您无法使用这些方法,因为它们没有实现,您可以随时检查文件头上的魔法值(几乎每种格式都有一个魔法值),看看是否需要根据字节顺序反转它。

此外,您可以在服务器上保存特定于字节序的文件,并根据检测到的主机字节序使用它们。

嗯,这是个好主意!我之前使用过 DataView,但目前只有 Chrome 支持它。
2021-04-06 16:13:47
作为跟进,我正在用 JavaScript 实现我自己的二进制编写器,它似乎在 Firefox 和 chrome 上都可以使用。
2021-04-09 16:13:47

其他答案对我来说似乎有点过时,所以这里是最新规范的链接:

http://www.khronos.org/registry/typedarray/specs/latest/#2.1

特别是:

类型化数组视图类型以主机的字节序操作。

DataView 类型对具有指定字节序(big-endian 或 little-endian)的数据进行操作。

因此,如果您想以 Big Endian(网络字节顺序)读取/写入数据,请参阅:http : //www.khronos.org/registry/typedarray/specs/latest/#DATAVIEW

// For multi-byte values, the optional littleEndian argument
// indicates whether a big-endian or little-endian value should be
// read. If false or undefined, a big-endian value is read.
“如果为 false 或未定义,则读取大端值。” - 只花了我几个小时或我的生命。
2021-03-19 16:13:47

检查字节序的快速方法

/** @returns {Boolean} true if system is big endian */
function isBigEndian() {
    const array = new Uint8Array(4);
    const view = new Uint32Array(array.buffer);
    return !((view[0] = 1) & array[0]);
}

怎么运行的:

  • 创建了一个 4 个字节的数组;
  • 一个 32 位视图包装了该数组;
  • view[0] = 1 设置数组以保存 32 位值 1;
  • 现在是重要的部分:如果系统是大端,则 1 由最右边的字节保存(小到最后);如果是小端,则存储它的是最左边的字节(小字节在前)。因此,如果机器是大端的,那么对最左边的字节进行按位与运算会返回 false;
  • 该函数最终通过将!运算符应用于运算结果将其转换为布尔值&,同时还反转它以便它为大端返回 true。

一个很好的调整是把它变成一个IIFE,这样你只能运行一次检查然后缓存它,然后你的应用程序可以根据需要多次检查它:

const isBigEndian = (() => {
    const array = new Uint8Array(4);
    const view = new Uint32Array(array.buffer);
    return !((view[0] = 1) & array[0]);
})();

// then in your application...
if (isBigEndian) {
    // do something
}