bytes: 範囲、カーソル、ULEB128

長さを持つバイトviewと失敗時の状態を保存する読み書きを説明します。

Wave Foundation

文字列と異なる点

バイト データにはゼロを含めることができるため、長さとともにポインタを渡します。 Bytes と BytesMut は非所有ビューであり、基になるストレージが有効な間のみ有効です。 BytesMut には書き込み可能なストレージが必要です。

cursor作り

std::bytes::cursor
bytes_reader(data: ptr<u8>, len: i64) -> ByteReader
bytes_writer(data: ptr<u8>, len: i64) -> ByteWriter
bytes_reader_read_be_u16(reader: ptr<ByteReader>, out_value: ptr<u16>) -> i32
bytes_writer_write_be_u16(writer: ptr<ByteWriter>, value: u16) -> i32

std::bytes::types から ByteReader と ByteWriter をインポートします。それらの位置フィールドは、次の操作の場所を識別します。それらの長さは、アクセス可能な合計バイト数です。どちらのカーソルを構築しても、基礎となるメモリのコピーや割り当ては行われません。

beはbig-endian、leはlittle-endianです。ファイル形式がbig-endianの場合、ホストCPUのバイト順に関係なく、be関数を使用します。 16・32・64ビット signed/unsigned 読み出し・書き込みと1バイト関数があります。

エラーと状態の保存

std::bytes::errorsのBYTES_OKは0です。INVALIDは無効な範囲、EOFは入力が不十分、NO_SPACEは出力容量が不十分、OVERFLOWは表現可能な範囲外の値を示します。チェックされたカーソル操作は、操作全体が成功した後にのみ位置を進めます。失敗した読み取りでも出力値は保持されます。

ULEB128

std::bytes::leb128
bytes_reader_read_uleb128_u64(reader: ptr<ByteReader>, output_value: ptr<u64>) -> i32
bytes_writer_write_uleb128_u64(writer: ptr<ByteWriter>, value: u64) -> i32

ULEB128 は、最大 10 バイトを使用して、符号なし 64 ビット整数を可変バイト数で格納します。スペースが不十分な場合、ライターはその位置と宛先バイトの両方を保存します。リーダーは、不完全な入力とu64を超える値を区別します。終了した非最小エンコーディングは受け入れられます。

実際のメッセージを作成して短い入力失敗を確認するには、バイナリメッセージの実践に進みます。 0を含むバイト列をstrとして出力しないでください。

同じバイトを異なる順序で読み取る

バイト順序は数値の格納規則です。 1、2という2バイトをbig-endianで読むと1×256+2、little-endianで読むと2×256+1です。ネットワークまたはファイル形式の規則に従って選択します。

import("std::bytes::types")::{Bytes, bytes_view};
import("std::bytes::read")::{bytes_read_be_u16, bytes_read_le_u16};

fun main() -> i32 {
    var data: array<u8, 2> = [1, 2];
    var view: Bytes = bytes_view(&data[0], 2);
    var big: u16 = 0;
    var little: u16 = 0;

    if (bytes_read_be_u16(view, 0, &big) < 0) {
        return 1;
    }

    if (bytes_read_le_u16(view, 0, &little) < 0) {
        return 2;
    }

    println("be={} le={}", big, little);
    return 0;
}

実行結果:

be=258 le=513

viewは配列を借ります。個別の割り当てやコピーがないため、配列が有効な間のみ使用できます。 offsetはバイト単位で、16ビット読み取りにはその位置から2バイトが必要です。

offset APIとcursor API選択

指定されたフィールド位置を直接読み取る形式には、offset引数を受け取るread/write関数が楽です。前のフィールド長によって次の位置が変わるストリームには、positionを持つcursorが楽です。

2つを混ぜるときは、cursor.positionとは別にoffsetのどれが基準かを明確にします。同じ場所を2回追加したり、読み取りに成功しなかったため、次の場所に移動する間違いを避けてください。

短い入力でステータスを確認する

import("std::bytes::types")::{ByteReader};
import("std::bytes::cursor")::{bytes_reader, bytes_reader_read_be_u16};
import("std::bytes::errors")::{BYTES_ERROR_EOF};

fun main() {
    var data: array<u8, 1> = [1];
    var reader: ByteReader = bytes_reader(&data[0], 1);
    var value: u16 = 99;
    var status: i32 = bytes_reader_read_be_u16(&reader, &value);

    if (status == BYTES_ERROR_EOF) {
        println("position={} value={}", reader.position, value);
    }
}

実行結果:

position=0 value=99

必要な2バイトがないため、出力値と位置が維持されます。この性質は、入力をさらに取得した後に同じフィールドの読み取りを再試行する設計に役立ちます。ただし、viewが指すストレージスペースを再割り当てした場合は、アドレスも更新する必要があります。

ULEB128の境界

0〜127は1バイト、128からはより多くのバイトを使用します。各バイトの上位ビットは、後にデータが続くかどうかを示します。 u64を超える値または長すぎる連続入力はOVERFLOWであり、単に入力が少ないEOFとは異なります。

容量不足 writerが状態を保存しているか直接確認してください。

import("std::bytes::types")::{ByteWriter};
import("std::bytes::cursor")::{bytes_writer};
import("std::bytes::leb128")::{bytes_writer_write_uleb128_u64};
import("std::bytes::errors")::{BYTES_ERROR_NO_SPACE};

fun main() {
    var data: array<u8, 1> = [85];
    var writer: ByteWriter = bytes_writer(&data[0], 1);
    var status: i32 = bytes_writer_write_uleb128_u64(&writer, 128);

    if (status == BYTES_ERROR_NO_SPACE) {
        println("position={} byte={}", writer.position, data[0]);
    }
}

実行結果:

position=0 byte=85

128は2バイトが必要ですが、スペースは1つだけです。失敗後、最初のバイト85も残ります。エラーの種類と状態の保存を一緒にチェックすることは、単純なroundtrip成功チェックよりも境界をよく説明します。

メッセージパーサーの作成順序

  1. 固定ヘッダーを読み、種類とバージョンを確認します。
  2. 長さを読み、残りの入力範囲と比較します。
  3. 必要なデータのみをviewまたは別のバッファに渡します。
  4. メッセージ全体を要求する形式であれば、残りのバイトも調べます。
  5. EOFと誤った形式のエラーを区別して発信者に渡します。

バイナリメッセージの実践でフィールドを連結して1つのプログラムを作成できます。