82

I'm using WebGL to render a binary encoded mesh file. The binary file is written out in big-endian format (I can verify this by opening the file in a hex editor, or viewing the network traffic using fiddler). When I try to read the binary response using a Float32Array or Int32Array, the binary is interpreted as little-endian and my values are wrong:

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

I can't find any references to the default endianness of typed arrays in http://www.khronos.org/registry/typedarray/specs/latest/ so I'm wondering what's the deal? Should I assume that all binary data should be little-endian when reading using typed arrays?

To get around the problem I can use a DataView object (discussed in the previous link) and call:

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

The DataView functions such as "getInt32" read big-endian values by default.

(Note: I've tested using Google Chrome 15 and Firefox 8 and they both behave the same way)

Bob
  • 909
  • 1
  • 8
  • 8

7 Answers7

83

The current behaviour, is determined by the endianness of the underlying hardware. As almost all desktop computers are x86, this means little-endian. Most ARM OSes use little-endian mode (ARM processors are bi-endian and thus can operate in either).

The reason why this is somewhat sad is the fact that it means almost nobody will test whether their code works on big-endian hardware, hurting what does, and the fact that the entire web platform was designed around code working uniformly across implementations and platforms, which this breaks.

Samuel Jenks
  • 1,137
  • 4
  • 21
  • 34
gsnedders
  • 5,532
  • 2
  • 30
  • 41
  • 9
    Somehow I thought that would be the case. – Bob Nov 12 '11 at 02:30
  • 5
    It's not unfortunate at all. Typed arrays follow the platform's endianness because we use them to interoperate with with native APIs, which work in the platform's endianness. If typed arrays had a set endianness, we'd lose a huge amount of the benefit of using them (on platforms that didn't happen to match the endianness that was chosen). For situations like the OP's, where there's a file involved (or for interacting with various protocols that define a specific endian order, such as TCP; etc.), that's what `DataView` is for. – T.J. Crowder Sep 13 '18 at 15:56
  • 2
    @T.J.Crowder There's definitely a use for machine-endianness, but the bigger problem is the majority of use we're seeing of typed arrays on the web don't need to worry about the underlying machine endianness, and if you do rely on machine endianness it's highly likely to broken on big endian systems (given approximately nobody will have tested their JS on one). (Note I was working at Opera at the time I wrote the above, who probably to this day account for the majority of browsers shipped on big-endian systems.) – gsnedders Sep 13 '18 at 19:11
  • Well, I can't claim deep familiarity with the issue, but the claim of those working on this from the early development of this stuff through implementation was that using machine endianness in typed arrays was important for interop with native APIs, which sounds solid to me. I'm going to trust that the many and varied people involved who have that deep familiarity didn't just collectively get it wrong. :-) – T.J. Crowder Sep 13 '18 at 19:48
  • 1
    @T.J.Crowder Remember typed arrays grew out of WebGL (where yes, machine endianness is useful), rather than a separate proposal. By the time it started getting used outside of WebGL, almost entirely in places where endianness doesn't matter, the cat was out of the bag with defaulting to machine endianness. Basically, given nobody tests on big-endian systems, you either break most WebGL cases (or swap endianness when passing to the GL implementation, which I believe is what browsers *actually do*), or break most non-WebGL cases. – gsnedders Sep 13 '18 at 22:49
  • @gsnedders - Citation for "what browsers actually do"? Although I suppose it doesn't really matter, it is what it is, and people will think it fortunate or not based on what they use them for... – T.J. Crowder Sep 14 '18 at 06:43
44

FYI you can use the following javascript function to determine the endianness of the machine, after which you can pass an appropriately formatted file to the client (you can store two versions of the file on server, big endian and little endian):

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");
}

In your case you will probably have to either recreate the file in little endian, or run through the entire data structure to make it little endian. Using a twist of the above method you can swap endianness on the fly (not really recommended and only makes sense if the entire structure is the same tightly packed types, in reality you can create a stub function that swaps bytes as needed):

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;
        }
    }
}
Stefan Rein
  • 8,084
  • 3
  • 37
  • 37
Ryan
  • 2,755
  • 16
  • 30
  • Nice one! I have just added `new` and `return bytes;` to your code. These helped make the code run for me. Thanks. – Theo Mar 21 '14 at 21:14
  • Actually the return was not necessary as the buffer itself is swapped. – Theo Mar 21 '14 at 21:47
  • filler text just to do this: :-D – Ryan Mar 24 '14 at 11:05
  • @Ryan, why do you use 4 bytes instead of 2? – Max Koretskyi Sep 30 '16 at 09:08
  • @Maximus this is due to 32bit, for example an `Uint32ArrayBuffer` – Stefan Rein Dec 07 '16 at 20:44
  • @StefanRein, yeah, but `Uint16Array` could have been used – Max Koretskyi Dec 07 '16 at 20:51
  • @Maximus Depends on the purpose. In my case I did need the buffer swapped for an Uint32ArrayBuffer. Because Uint32Array will use your machines endianness, like mine is Little Endian and I did need to have it in Big Endian, thus PNG is encoded as BE. If you would have used `WORD`, you would have mess up the buffer. Because you would swap every two bytes, instead of every four bytes reversing the order – Stefan Rein Dec 07 '16 at 20:58
  • @StefanRein, no, I was talking about `checkEndian` function. Endianess operates on bytes, so two bytes should be sufficient to determine if the order is the same, right? Hence the question why use 4 to determine when 2 could have been used? – Max Koretskyi Dec 08 '16 at 07:59
  • @Maximus jep, you're completely right on this! An Uint16Array is completely enough – Stefan Rein Dec 08 '16 at 08:06
  • @StefanRein, great, just wanted to confirm that. Thanks for you feedback :) – Max Koretskyi Dec 08 '16 at 08:08
  • Just wanted to say, could very easily make the checkEndian function a bit more concise & efficient: `const systemIsLE = ()=>(new Uint16Array((Uint8Array.of(0x1, 0x0)).buffer))[0] === 0x1;` – BAM5 Oct 07 '20 at 04:09
34

Taken from here http://www.khronos.org/registry/typedarray/specs/latest/ (when that spec is fully implemented) you can use:

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

However, if you can't use those method because they aren't implemented, you can always check the file's magic value (almost every format has a magic value) on the header to see if you need to invert it according to your endiannes.

Also, you can save endiannes-specific files on your server and use them accordingly to the detected host endiannes.

Chiguireitor
  • 3,186
  • 1
  • 20
  • 26
  • Hm that's a good idea! I was using DataView before, but only Chrome supports it at the moment. – Bob Nov 12 '11 at 02:26
  • Just as a follow up, i'm implementing my own binary writer on JavaScript, and it seems to be working on both firefox and chrome. – Chiguireitor Dec 13 '11 at 20:29
17

The other answers seem a bit outdated to me, so here's a link to the latest spec:

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

In particular:

The typed array view types operate with the endianness of the host computer.

The DataView type operates upon data with a specified endianness (big-endian or little-endian).

So if you want to read/write data in Big Endian (Network Byte Order), see: 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.
user1338062
  • 11,939
  • 3
  • 73
  • 67
16

Quick way to check endianness

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

How it works:

  • an array of 4 bytes is created;
  • a 32-bit view wraps that array;
  • view[0] = 1 sets the array to hold 32-bit value 1;
  • now comes the important part: if system is big endian, that 1 is being hold by the rightmost byte (little comes last); if it is little endian, it is the leftmost byte that stores it (little comes first). So doing a bitwise AND with the leftmost byte returns false if the machine is big endian;
  • the function finally converts it to a boolean by applying the ! operator to the result of the & operation, while also inverting it so that it returns true for big endian.

One nice tweak is to turn it into an IIFE, that way you can run the check only once and then cache it, then your application can check it as many times as it needs:

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
}
Lucio Paiva
  • 19,015
  • 11
  • 82
  • 104
0

Yet Another QUICK WAY to CHECK Endianness:

Just adding my 2Cents here, but, my preferred method below is something I’ve found useful; especially when stored statically in a Singleton and made available across classes:

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

If each 8Bits is not stored in the same order as the hex was input, then it’s using little endian. The result is then stored, and is useful for further reference. The reason the result is accurate, is because data is stored in the buffer, the same way as it is stored Natively on the device, according to the ECMA script spec.

The fact it calls only once, and then stores it, is very useful; especially with a million+ Itterations, all needing to know which endianness to use, including the most obvious one, rendering.

To illustrate this:

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");
  }
}

It’s similar to the isBigEndian version already posted, just done a the other way round; which is in the spirit of EndianNess.

Anthony Pace
  • 451
  • 4
  • 9
0

this should return true on little-endian and false on big-endian:

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

because little-endian sets [0] to 1 and [1] to 0, conversely big endian set [0] to 0 and [1] to 1... i think? don't actually have a big-endian system available to test on.

hanshenrik
  • 19,904
  • 4
  • 43
  • 89