【问题标题】:Javascript Typed Arrays and EndiannessJavascript 类型数组和字节序
【发布时间】:2011-12-13 18:15:43
【问题描述】:

我正在使用 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 进行了测试,它们的行为方式相同)

【问题讨论】:

  • 如果有人想知道,我认为答案是我应该使用 little-endian 编写文件。
  • 这就是避免问题而不是解决问题。

标签: javascript endianness webgl typed-arrays arraybuffer


【解决方案1】:

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

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

【讨论】:

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

检查字节序的快速方法

/** @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
}

【讨论】:

    【解决方案3】:

    仅供参考,您可以使用以下 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;
            }
        }
    }
    

    【讨论】:

    • 不错的一个!我刚刚在您的代码中添加了newreturn bytes;。这些有助于使代码为我运行。谢谢。
    • 实际上没有必要返回,因为缓冲区本身已被交换。
    • 填充文本只是为了做到这一点::-D
    • @Ryan,你为什么用 4 个字节而不是 2 个字节?
    • @Maximus 这是由于 32 位,例如 Uint32ArrayBuffer
    【解决方案4】:

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

    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 或未定义,则读取大端值。" - 只花了我几个小时或我的生命。
    【解决方案5】:

    取自这里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 支持。
    • 作为后续,我正在用 JavaScript 实现我自己的二进制编写器,它似乎可以在 firefox 和 chrome 上运行。
    猜你喜欢
    • 2015-10-15
    • 2012-07-14
    • 2022-06-10
    • 2010-12-27
    • 1970-01-01
    • 2016-09-10
    • 2011-11-26
    • 2023-03-21
    • 1970-01-01
    相关资源
    最近更新 更多