JavaScript类型化数组和字节序问题

82

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

// 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);

DataView函数(如“getInt32”)默认读取大端存储的值。

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


如果有人在想,我认为答案是我应该使用小端方式编写我的文件。 - Bob
6
这被称为回避问题而不是解决它。 - Pacerier
7个回答

83

当前行为由底层硬件的字节序决定,由于几乎所有桌面电脑都是x86架构,这意味着使用小端字节序。大多数ARM操作系统使用小端模式(ARM处理器是双尾端的,因此可以在两种模式下运行)。

这种情况让人有些悲伤的原因是几乎没有人会测试他们的代码是否在大端硬件上工作,这会影响能够正常工作的代码,而整个Web平台都是围绕着代码在各种实现和平台上统一工作而设计的,但这种情况破坏了这一点。


9
我本以为会是这种情况。 - Bob
5
这并不不幸。类型化数组遵循平台的字节序,因为我们使用它们与本机 API 进行交互,本机 API 工作在平台的字节序下。如果类型化数组有一个固定的字节序,我们将失去使用它们的巨大好处(在没有匹配所选字节序的平台上)。对于像原帖中的情况,涉及到文件的情况(或者与定义特定字节序的各种协议进行交互,如 TCP 等等),就需要使用“DataView”。 - T.J. Crowder
2
@T.J.Crowder,机器字节序确实有用,但更大的问题是我们在Web上看到的大多数类型化数组的使用不需要担心底层机器字节序,如果您依赖于机器字节序,则很可能在大端系统上出现故障(因为几乎没有人会在此类系统上测试他们的JS)。 (请注意,我当时在Opera工作,他们可能仍然占大端系统上发货的浏览器的大部分份额。) - gsnedders
嗯,我不能声称对这个问题非常熟悉,但是从早期开发到实现中一直在处理这个问题的人们声称,在类型化数组中使用机器字节序对于与本地API的交互非常重要,这听起来很有道理。我会相信那些深入了解此事的众多人员并不是集体犯错的。 :-) - T.J. Crowder
1
@T.J.Crowder 记住,类型化数组起源于WebGL(在这里,机器尾数是有用的),而不是一个单独的提案。当它开始被用于WebGL以外的几乎所有地方,并且尾数无关紧要时,猫已经从袋子里跑出来了,并默认为机器尾数。基本上,由于没有人在大端系统上进行测试,你要么打破大多数WebGL案例(或在传递给GL实现时交换尾数,我相信这就是浏览器*实际执行的操作),要么打破大多数非WebGL案例。 - gsnedders
@gsnedders - “浏览器实际上做了什么”的引用?虽然我想这并不重要,它就是它的样子,人们会根据他们使用它们的目的来认为它是幸运的还是不幸的... - T.J. Crowder

44

顺便提一下,你可以使用以下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;。这些帮助我使代码运行起来了。谢谢。 - Theo
实际上返回并不必要,因为缓冲区本身已经被交换了。 - Theo
请翻译以下与编程有关的内容,只返回已翻译的文本:占位符文本只是为了完成这个任务::-D - Ryan
@Ryan,为什么你使用4个字节而不是2个? - Max Koretskyi
@Maximus,这是由于32位问题,例如Uint32ArrayBuffer - Stefan Rein
显示剩余6条评论

34

可以从这里获取: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支持它。 - Bob
作为跟进,我正在JavaScript上实现自己的二进制写入器,它似乎在Firefox和Chrome上都能正常工作。 - Chiguireitor

18

我认为其他答案有点过时,所以这里提供最新规范的链接:

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

具体而言:

类型化数组视图类型采用主机计算机的字节序。

DataView 类型根据指定的字节序(大端或小端)处理数据。

因此,如果您想以大端序(网络字节顺序)读取/写入数据,请参见: 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.

7
如果为 false 或未定义,则读取大端字节序的值。- 这可能会耗费我几个小时甚至是我的生命。 - Meirion Hughes

16

快速检查字节序的方法

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

1

这应该在小端序上返回真,在大端序上返回假:

function runtimeIsLittleEndian(){
    return (new Uint8Array(new Uint16Array([1]).buffer)[0] === 1);
}

因为小端序将 [0] 设置为 1,将 [1] 设置为 0,相反地大端序将 [0] 设置为 0,将 [1] 设置为 1... 我想是这样的?实际上没有一个可用的大端序系统来测试。

0

另一种快速检查字节序的方式:

这里只是添加我的2美分,但是我下面喜欢的方法是我发现有用的东西;特别是当它被静态存储在一个Singleton中,并且可以在类之间共享时:

static isLittleEndian = (function(){
            var a8 = new Uint8Array(4);
            var a32 = new Uint32Array(a8.buffer)[0]=0xFFcc0011;
            return !(a8[0]===0xff);
        })();

如果每个8位不按照输入的十六进制顺序存储,那么它就是使用小端字节序。然后将结果存储下来,以便进行进一步的参考。结果准确的原因是因为数据按照ECMA脚本规范在缓冲区中以本机设备相同的方式存储。

它仅调用一次并将其存储非常有用;特别是对于需要知道使用哪种字节序的数百万次迭代,包括最明显的渲染。

为了说明这一点:

const isLittleEndian = (function(){
    console.log("isLittleEndian function called");
    var a8 = new Uint8Array(4);
    var a32 = new Uint32Array(a8.buffer)[0]=0xFFcc0011;
    return !(a8[0]===0xff);
})();

for(let i = 0; i!=5; i++){
  if(isLittleEndian){
    console.log("Little Endian");
  }else{
    console.log("Big Endian");
  }
}

这与已发布的isBigEndian版本类似,只是以另一种方式完成;这符合字节序的精神。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接