`DataView` no TypeScript

`DataView` no TypeScript

Este artigo explica o DataView no TypeScript.

Explicaremos o DataView no TypeScript, desde os conceitos básicos até o uso prático, passo a passo.

YouTube Video

DataView no TypeScript

Ao usar o DataView no TypeScript, é possível realizar operações de leitura e escrita em nível de byte em um ArrayBuffer com grande precisão. O DataView é extremamente útil para o processamento binário de baixo nível, como implementação de protocolos, análise de arquivos binários e envio/recebimento de dados binários via WebSocket.

Conceitos Básicos: Diferenças entre ArrayBuffer, TypedArray e DataView

ArrayBuffer é uma estrutura de dados fundamental utilizada para armazenar uma sequência de bytes de comprimento fixo. TypedArrays como Uint8Array são visualizações que tratam o buffer como um array de um tipo específico, onde cada elemento possui um tipo fixo.

Por outro lado, o DataView é uma visualização flexível que permite ler e escrever valores de qualquer tipo de dado em qualquer posição de deslocamento. Como não pressupõe tipos fixos e permite um controle detalhado em nível de byte, ele é adequado para analisar protocolos binários e processamento de baixo nível.

A seguir está um exemplo de criação de um ArrayBuffer e, em seguida, de um Uint8Array e DataView a partir dele.

 1// Create an ArrayBuffer (8 bytes)
 2const buffer = new ArrayBuffer(8);
 3
 4// Create a Uint8Array view (easy to manipulate individual bytes)
 5const u8 = new Uint8Array(buffer);
 6u8[0] = 0x12;
 7u8[1] = 0x34;
 8
 9// Create a DataView (allows reading/writing any type at any offset)
10const dv = new DataView(buffer);
11
12// Read a 16-bit unsigned integer (big-endian assumed if second argument is omitted)
13console.log(dv.getUint16(0)); // 0x1234
  • Neste código, dois tipos diferentes de visualizações—Uint8Array e DataView—são usados simultaneamente em um único buffer. Usando o DataView, você pode ler e escrever valores de forma flexível, especificando deslocamentos e endianness arbitrários.

Principais Métodos do DataView

DataView é uma interface para manipular um ArrayBuffer em nível de byte, e fornece métodos para ler e escrever vários tipos de dados, como inteiros e números de ponto flutuante.

Os principais métodos são os seguintes:.

  • Leitura: getUint8, getInt8, getUint16, getInt16, getUint32, getInt32, getFloat32, getFloat64
  • Escrita: setUint8, setInt8, setUint16, setInt16, setUint32, setInt32, setFloat32, setFloat64

Todos esses métodos recebem um 'deslocamento em bytes' como primeiro argumento: get lê o valor nessa posição e set escreve o valor nessa posição. Você também pode especificar o endianness como segundo argumento ao lidar com dados de 16, 32 ou 64 bits. Na prática, é mais seguro sempre especificar o endianness de acordo com a especificação dos dados.

O exemplo a seguir mostra como gravar um inteiro de 32 bits e um número de ponto flutuante de 32 bits em formato little endian em um buffer e depois lê-los de volta no mesmo formato.

 1const buf = new ArrayBuffer(12); // Enough space for integer + float
 2const view = new DataView(buf);
 3
 4// Write a 32-bit unsigned integer at offset 0 (little-endian)
 5view.setUint32(0, 305419896, true); // 305419896 = 0x12345678
 6
 7// Read the same 32-bit unsigned integer back from offset 0 (little-endian)
 8const intVal = view.getUint32(0, true);
 9console.log(intVal); // 305419896
10
11// Write a 32-bit floating number at offset 4 (little-endian)
12view.setFloat32(4, 3.1415926, true);
13
14// Read the 32-bit floating number back from offset 4 (little-endian)
15const floatVal = view.getFloat32(4, true);
16console.log(floatVal); // 3.1415926 (approx)
  • Ao especificar explicitamente o endianness, você pode garantir compatibilidade com diferentes plataformas e especificações binárias.

Sobre Endianness (Ordem dos Bytes)

Alguns protocolos, como os utilizados em redes, usam big-endian, enquanto muitos CPUs e formatos de arquivos utilizam majoritariamente little-endian. No DataView, se o segundo argumento for definido como true, os dados são tratados como little endian; se for false ou omitido, o tratamento será big endian.

No exemplo a seguir, você pode ver como o array de bytes na memória muda ao gravar o mesmo número usando os formatos big-endian e little-endian.

 1const b = new ArrayBuffer(4);
 2const a = new Uint8Array(b);
 3const dv = new DataView(b);
 4
 5// Write in big-endian order
 6dv.setUint32(0, 0x0A0B0C0D, false); // big-endian
 7console.log([...a]); // [10, 11, 12, 13]
 8
 9// Write the same value in little-endian order
10dv.setUint32(0, 0x0A0B0C0D, true); // little-endian
11console.log([...a]); // [13, 12, 11, 10]
  • Compreender como a ordem dos bytes muda entre big endian e little endian facilita muito a análise de dados de comunicação ou formatos binários.

Entrada/Saída de Strings (Usando TextEncoder e Decoder Juntos)

DataView é excelente para ler e escrever números, mas não lida diretamente com strings. É comum codificar as strings usando TextEncoder ou TextDecoder (como para UTF-8) e, em seguida, copiá-las para o buffer via Uint8Array. A seguir está um exemplo de como escrever uma string UTF-8 em um buffer e depois lê-la de volta.

 1const encoder = new TextEncoder();
 2const decoder = new TextDecoder();
 3
 4const str = "\u3053\u3093\u306b\u3061\u306f";
 5const encoded = encoder.encode(str); // Uint8Array (UTF-8 encoded bytes)
 6
 7// Create a buffer and write the encoded string bytes into it
 8const buf2 = new ArrayBuffer(encoded.length);
 9const u8v = new Uint8Array(buf2);
10u8v.set(encoded);
11
12// Read the bytes and decode them back into a string
13const decodedStr = decoder.decode(new Uint8Array(buf2));
14console.log(decodedStr);
  • Convertendo strings para binário e, se necessário, prefixando o comprimento delas, é possível armazenar strings de comprimento variável.

Exemplo Prático: Codificação/Decodificação de um Formato Binário Personalizado

Abaixo, definimos um formato de mensagem simples contendo um número de versão, ID e nome. Implementamos um processo de codificação para converter mensagens em dados binários e um processo de decodificação para restaurar o objeto original a partir do binário. Neste formato de mensagem, o primeiro byte armazena a versão, os próximos 4 bytes armazenam o ID no formato little-endian, o byte seguinte contém o comprimento do nome e, por fim, o nome codificado em UTF-8 é armazenado.

 1// Encode an object into a binary message
 2function encodeMessage(msg: { version: number; id: number; name: string }): ArrayBuffer {
 3  const encoder = new TextEncoder();
 4  const nameBytes = encoder.encode(msg.name);
 5  const total = 1 + 4 + 1 + nameBytes.length;
 6  const buf = new ArrayBuffer(total);
 7  const dv = new DataView(buf);
 8  let offset = 0;
 9
10  dv.setUint8(offset, msg.version); offset += 1;      // write version
11  dv.setUint32(offset, msg.id, true); offset += 4;    // write ID (little-endian)
12  dv.setUint8(offset, nameBytes.length); offset += 1; // write name length
13
14  // write name bytes
15  new Uint8Array(buf, offset).set(nameBytes);
16  return buf;
17}
18
19// Decode a binary message back into an object
20function decodeMessage(buf: ArrayBuffer) {
21  const dv = new DataView(buf);
22  let offset = 0;
23
24  const version = dv.getUint8(offset); offset += 1;   // read version
25  const id = dv.getUint32(offset, true); offset += 4; // read ID (little-endian)
26  const nameLen = dv.getUint8(offset); offset += 1;   // read name length
27
28  // extract name bytes
29  const nameBytes = new Uint8Array(buf, offset, nameLen);
30  const decoder = new TextDecoder();
31  // decode UTF-8 string
32  const name = decoder.decode(nameBytes);
33
34  return { version, id, name };
35}
36
37// Example usage
38const packed = encodeMessage({ version: 1, id: 42, name: "Alice" });
39const unpacked = decodeMessage(packed);
40console.log(unpacked); // { version: 1, id: 42, name: "Alice" }
  • Esta é uma implementação típica para lidar com um formato de mensagem básico contendo uma string de comprimento variável, podendo ser aplicada em muitas situações práticas como comunicação de rede ou projetos de arquivos binários personalizados.

Cuidados e Melhores Práticas

Ao trabalhar com dados binários usando DataView, há vários pontos práticos a considerar além da simples leitura e escrita de valores—como segurança, uso consistente de endianness e tratamento apropriado de tipos. Em particular, ao lidar com dados binários recebidos de fontes externas ou ao trabalhar com inteiros grandes, é importante projetar o código para evitar leituras incorretas e estouros de buffer. Abaixo estão alguns pontos úteis para lembrar no uso prático.

  • Verificação de Limite DataView lança uma exceção se um deslocamento ou tamanho exceder o limite do buffer. Sempre verifique os comprimentos ao lidar com dados binários não confiáveis.

  • Sempre Especifique o Endianness Sempre especifique explicitamente e de forma consistente little endian ou big endian no seu código.

  • Consistência de Tipos Os números em JavaScript são valores de ponto flutuante IEEE-754 de 64 bits. Métodos como getUint32 são tratados corretamente, mas não há getUint64, por isso é necessário um tratamento especial para inteiros de 64 bits.

  • Manipulação de Inteiros de 64 bits Para manipular inteiros de 64 bits, é necessário usar BigInt ou dividir manualmente o valor em partes alta e baixa de 32 bits. Aqui está um exemplo simples de leitura de um inteiro sem sinal de 64 bits.

1function getUint64(dv: DataView, offset: number, littleEndian = true): bigint {
2  const low = BigInt(dv.getUint32(offset, littleEndian));
3  const high = BigInt(dv.getUint32(offset + 4, littleEndian));
4  return littleEndian ? (high << 32n) | low : (low << 32n) | high;
5}
  • Usando BigInt, você pode manipular com segurança inteiros maiores que 64 bits também.

Uso no Node.js (Interoperando com Buffer)

Embora o Buffer seja comumente usado no Node.js, também é fácil converter entre ArrayBuffer e DataView. Use a propriedade buffer dos objetos Buffer ou o construtor Uint8Array para conversão.

1// Node.js: Buffer -> ArrayBuffer
2const nodeBuf = Buffer.from([1,2,3,4]);
3const arrBuf = nodeBuf.buffer.slice(nodeBuf.byteOffset, nodeBuf.byteOffset + nodeBuf.byteLength);
4const dvNode = new DataView(arrBuf);
5console.log(dvNode.getUint16(0));
  • Isso pode ser usado como uma ferramenta para trocar dados binários entre Node.js e navegadores.

Resumo

DataView é um mecanismo poderoso para leitura e escrita livre de dados binários—especialmente útil em situações que exigem controle flexível, como especificação de endianness e acesso a posições arbitrárias. Combinando ArrayBuffer, TypedArray e DataView, você pode manipular dados binários com flexibilidade e precisão no TypeScript, permitindo uma ampla gama de casos de uso, desde implementação de protocolos até análise de arquivos. Ao também incorporar a codificação de strings e o tratamento de inteiros de 64 bits conforme necessário, é possível realizar operações binárias ainda mais práticas.

Você pode acompanhar o artigo acima usando o Visual Studio Code em nosso canal do YouTube. Por favor, confira também o canal do YouTube.

YouTube Video