オフィスアワーがそろそろ始まるよ!()

ポインタ

ポインタ変数はデータや関数のメモリ上のアドレスを格納するためのものです。ポインタを使用することで、メモリ上のデータやメモリ空間にマッピングされたハードウェアを間接的に参照、操作することが可能です。低レイヤで使用されるシステムプログラミング言語では、パフォーマンス上の理由やハードウェアを直接操作する必要があるため、ポインタを介してメモリ空間を操作します。

Zen言語にも、他のシステムプログラミング言語と同様にポインタがあります。Zenのポインタは大別すると4つに分類できます。ここでは、Zenのポインタについて解説します。

ポインタ操作の基礎

ポインタ変数の定義

ポインタ変数は、*i32のように参照する型の前にアスタリスク (*) を付けて定義します。

const pointer: *i32 = アドレス;

アドレスの取得

変数の前にアドレス (&) 演算子を置くことで、その変数のアドレスを取得します。

examples/ch02-primitives/src/pointers.zen:8:12

test "address-of operator" {
    const x: i32 = 42;
    const pointer = &x;
    std.debug.warn("address of x is: {}\n", pointer);
}

ポインタ型についてもコンパイラが型推論可能な場合には、型を省略することができます。pointerの型は、右辺値&xから*i32であると推論されます。

上記コードを実行するとstd.debug.warnは以下の文字列を出力します。

address of x is: i32@204d90

i32@に続く数字が、変数xのアドレスです。このアドレスはプログラムを実行するたびに異なる値になりますので、手元で確認できるxのアドレスは、上のアドレスとは異なるはずです。

デリファレンス

ポインタ変数から、参照している値を取得するには、デリファレンス (参照外し) 演算子 (.*) を使用します。

examples/ch02-primitives/src/pointers.zen:14:21

test "dereference operator" {
    var x: i32 = 42;
    const pointer = &x;
    ok(pointer.* == 42);

    pointer.* = 52;
    ok(x == 52);
}

暗黙のデリファレンス

プログラムを書きやすくするために、構造体や共用体のポインタを操作する場合、暗黙のデリファレンスが行われます。つまり、ある構造体や共用体を操作する時に、その変数が構造体の実体か、ポインタか、を意識しないコードを書くことができます。

examples/ch02-primitives/src/pointers.zen:23:40

const Person = struct {
    name: []u8,
    age: u32,

    fn getAge(self: Person) u32 {
        return self.age;
    }
};

test "implicit dereference" {
    const kris = Person { .name = &"kris", .age = 31 };
    const pointer = &kris;

    ok(kris.age == 31);
    ok(kris.getAge() == 31);
    ok(pointer.age == 31);
    ok(pointer.getAge() == 31);
}

*Person型のポインタであるpointerからも、メンバアクセス (.) 演算子でフィールドとメソッドが利用できています。これは、メンバアクセス演算子により、暗黙的にデリファレンスが行われているためです。

const

constで修飾されているポインタ (*const T) は、ポインタが指している値を書き換えることができません。ポインタ変数自体を書き換え可能とするかどうかは、ポインタ変数を定義する際のvar / constで制御します。次節のポインタ型でコードの例を示しながら説明します。

ポインタ型

Zenには4種類のポインタ型があります。それぞれの違いと使い分け方について説明します。

以下、Tは任意の型、Nは任意の正の数を意味します。

*T : 単一オブジェクトへのポインタ

これまでの例で見てきた単一オブジェクトへのポインタ型です。このポインタ型の変数に対しては、デリファレンス (.*) 演算子が利用できます。

examples/ch02-primitives/src/pointers.zen:42:66

test "pointer to single object" {
    // ミュータブルなオブジェクトのアドレスを取得する
    var mutable: i32 = 42;
    const pointer_of_mutable = &mutable;

    // ポインタ型は`*T`で参照先を読み書き可能
    ok(@typeOf(pointer_of_mutable) == *i32);

    // デリファレンスにより参照先オブジェクトの値を操作
    ok(pointer_of_mutable.* == 42);
    pointer_of_mutable.* = 52;
    ok(pointer_of_mutable.* == 52);

    // イミュータブルなオブジェクトのアドレスを取得する
    const immutable: i32 = 42;
    const pointer_of_immutable = &immutable;

    // ポインタ型は`*const T`で参照先には書き込みできない
    ok(@typeOf(pointer_of_immutable) == *const i32);

    // デリファレンスにより参照先オブジェクトの値を操作
    ok(pointer_of_immutable.* == 42);
    // Compile error: cannot assign to constant
    // pointer_of_immutable.* = 52;
}

*[N]T : 要素数がコンパイル時計算可能な配列へのポインタ

Zenの配列と密接に関係するポインタです。要素数がコンパイル時計算可能な配列に対するポインタ型です。このポインタ型の変数に対しては、配列型と同じ以下の操作が可能です。

  • [インデックス]によるインデックスアクセス
  • .lenによる要素数の取得
  • [begin..end]によるスライスの作成

examples/ch02-primitives/src/pointers.zen:68:84

test "pointer to comptime-known number of object" {
    var array = [_]i32{ 1, 2, 3, 4 };
    const pointer = &array;

    // ミュータブルな配列へのポインタ型は`*[N]T`
    ok(@typeOf(pointer) == *[4]i32);

    // インデックスアクセス
    ok(pointer[0] == 1);

    // 要素数の取得
    ok(pointer.len == 4);
    
    // スライスの作成
    const slice = pointer[1..];
    ok(slice[0] == 2);
}

[]T : 要素数が実行時計算可能な配列へのポインタ

これはスライス型そのものです。スライスは配列要素の先頭アドレスと、要素数からなる型です。スライス型はいわゆるファットポインタと呼ばれるものです。ファットポインタとは、「アドレス+制御情報」から構成されるポインタです。

他のポインタ型のサイズがアドレスのビット幅と一致するのに対し、こちらのポインタ型は要素数を格納する分、サイズが大きくなります。

examples/ch02-primitives/src/pointers.zen:86:94

test "size of pointer types" {
    // これらのポインタ型はアドレスのビット幅と同じサイズ
    ok(@sizeOf(*i32) == @sizeOf(usize));
    ok(@sizeOf([*]i32) == @sizeOf(usize));
    ok(@sizeOf(*[4]i32) == @sizeOf(usize));

    // スライス型は`アドレス+要素数`のファットポインタ
    ok(@sizeOf([]i32) == (@sizeOf(usize) * 2));
}

この型はスライス型であるため、使い方はもちろんスライスと同じです。

[*]T : 要素数が不明なオブジェクトへのポインタ

要素数が不明なオブジェクトに対するポインタです。こちらの型に対しては、以下の操作が可能です。

  • [インデックス]によるインデックスアクセス
  • [begin..end]によるスライス作成
  • ポインタの算術演算

examples/ch02-primitives/src/pointers.zen:96:110

test "pointer to unknown number of object" {
    var array = [_]i32{ 1, 2, 3, 4 };
    const pointer = @ptrCast([*]i32, &array);

    // インデックスアクセス
    ok(pointer[0] == 1);

    // ポインタを加算
    const second = pointer + 1;
    ok(second[0] == 2);

    // スライス作成が可能だが、`end`が必須
    const slice = pointer[0..3];
    ok(@typeOf(slice) == []i32);
}

NULLポインタ

NULLポインタとは、何のオブジェクトも参照していないことを意味する特殊なポインタです。C言語では、NULLポインタは0です。NULLポインタのデリファレンスは、未定義動作を引き起こすバグになります

Zenでは安全性のために、通常のポインタにNULLポインタ (0) を代入することはできません

    const pointer: *i32 = @intToPtr(*i32, 0);

上のコードは、下のコンパイルエラーになります。

error[E04042]: '*i32' does not allow address zero
    const pointer: *i32 = @intToPtr(*i32, 0);
                          ~

Zenにおいて、ポインタが何のオブジェクトも参照していないことを示すには、オプション型nullを使用します。

examples/ch02-primitives/src/pointers.zen:112:127

test "null of optional" {
    // `pointer`は何のオブジェクトも参照していない
    var pointer: ?*i32 = null;
    ok(pointer == null);

    // `pointer`は`x`を参照する
    var x: i32 = 42;
    pointer = &x;
    // Compile error: attempt to dereference non-pointer type '?*i32'
    // pointer.* = 52;

    // `pointer`が`null`でないことを検査してから、中身の`*i32`を使用する
    if (pointer) | non_null | {
        ok(non_null.* == 42);
    }
}

オプション型に包まれたポインタは、nullでないことを検査してからしか、デリファレンスできません。そのため、ZenではNULLポインタのデリファレンスをしてしまうバグは未然に防ぐことができます。

レアケースですが、本当にアドレスの0番地を取り扱いたい場合のために、allowzeroポインタが利用できます。

整数とポインタの相互変換

デバイスドライバやOSなど、ベアメタルの開発では特定のメモリ番地を指すポインタが必要です。組込み関数の@intToPtr@ptrToIntを利用することで、整数とポインタとを相互変換できます。

examples/ch02-primitives/src/pointers.zen:129:136

test "convert pointer and integer" {
    const pointer = @intToPtr(*i32, 0x8000_0000);
    const address = @ptrToInt(pointer);

    ok(address == 0x8000_0000);
    // @ptrToIntの戻り値型は`usize`
    ok(@typeOf(address) == usize);
}

@ptrToIntの戻り値の型は、usizeなので、他の整数型で扱いたい場合は、別途@intCastを使用します。

[*c]T: Cポインタ

C言語と相互にポインタをやり取りするため、C言語互換のポインタ型が存在します。それが[*c]T型です。

このCポインタ型は、必要がない限り利用するべきではありません。Cポインタを利用するケースは、CソースコードからZenコードを自動生成する場合のみです。

詳細は、14章 Cとのインターフェースで説明します。

volatile

OSや組込みシステムでは、メモリマップドIO (MMIO) を使用します。このようなメモリマップドIOが割り当てられたメモリ番地の読み書きはハードウェアの動作に影響を与えますが、コンパイラにはそのことがわかりません。コンパイラは最適化の過程で、意味がないと考えられるメモリ読み書きを削除することがあります。volatile修飾は、メモリの読み書きが副作用を持っており、全てのメモリの読み書きに意味があることをコンパイラに伝えます。

次のコードを見てみて下さい。fake_mmioは何らかのメモリマップドIOで、このメモリアドレスへの読み書きには何らかの副作用があると仮定して下さい。そのfake_mmioのメモリアドレスに連続して3回異なる値を書き込みます。

examples/ch02-primitives/src/pointer_qualifier.zen:5:10

    var fake_mmio: u32 = undefined;
    const ptr: *u32 = &fake_mmio;

    ptr.* = 1;
    ptr.* = 2;
    ptr.* = 3;

このコードをリリースモードでビルドすると、次のようなアセンブリが出力されます。

# 完全に削除される

ここからわかることは、fake_mmioのアドレスへの書き込みが最適化時に意味がないと判断され、削除されているということです。

では、volatileをつけるとどうなるのでしょうか。

examples/ch02-primitives/src/pointer_qualifier.zen:14:19

    var fake_mmio: u32 = undefined;
    const ptr: *volatile u32 = &fake_mmio;

    ptr.* = 1;
    ptr.* = 2;
    ptr.* = 3;

このコードをリリースモードでビルドすると、次のアセンブリになります。

        mov     dword ptr [rsp - 4], 1
        mov     dword ptr [rsp - 4], 2
        mov     dword ptr [rsp - 4], 3

123の3回、メモリへの書き込みが発生していることがわかります。

このようにvolatileでポインタを修飾することにより、メモリ読み書きに副作用があることをコンパイラに伝えることができます。

アライメント

全ての型はアライメントを持っています。アライメントとは、ある値が配置されるメモリ番地の先頭が、その数で割り切れるような数字を意味します。例えば、4バイトアライメントの型であれば、その値が格納されているメモリ番地の先頭は、4の倍数でなければなりません。

アライメントはプロセッサアーキテクチャに依存するものですが、2の累乗でなければならず、1 << 29より小さくなければなりません (Zenのアライメントを引数に取る関数がu29の型を受け取るのは、この理由からです) 。

Zenでは全てのポインタ型がアライメント値 (alignmentフィールド) を持っています。アライメント値が型のサイズと同じ場合、型情報からアライメント値は取り除かれます。アライメント値を型とは別のサイズにしたい場合、変数定義時にアライメント値を指定するか、組込み関数の@alignCastを使うことができます。ただし、@alignCastは失敗する場合があります。

型のアライメント値は@alignOfで取得することができます。

examples/ch02-primitives/src/pointer_qualifier.zen:23:36

test "align" {
    if (builtin.arch == builtin.Arch.x86_64) {
        ok(@alignOf(u32) == 4);
        ok((*u32).alignment == 4);
    }

    var x: u32 align(8) = 42;
    ok(@typeOf(&x) == *align(8) u32);
    ok(@alignOf(@typeOf(&x)) == 8);
    ok(@typeOf(&x).alignment == 8);

    var y = @alignCast(4, &x);
    ok(@typeOf(y) == *align(4) u32);
}

allowzero

allowzeroで修飾されたポインタは0番地のアドレスを持つことができます。これは、OSのないベアメタルの環境で、0番地が実際にアクセス可能な場合のみ、必要になります。それ以外の場合は、オプション型で包まれたポインタを利用すべきです。

examples/ch02-primitives/src/pointer_qualifier.zen:38:40

test "allowzero" {
    var ptr = @intToPtr(*allowzero u32, 0);
}

ポインタの算術演算

[*]Tポインタ以外を直接算術演算することはできません。要素配列にアクセスしたい場合、Zenでは可能な限りいつでも、配列かスライスを使うべきです。

ポインタの型変換

@ptrCastでポインタの型を変換することができます。

examples/ch02-primitives/src/pointers.zen:138:144

test "pointer type conversions" {
    var x: u8 = 255;
    // 符号なしから符号ありに型変換
    const pointer = @ptrCast(*i8, &x);
    // `u8`の`255`は、`i8`の`-1`
    ok(pointer.* == -1);
}

しかし、いくつか制限があります。

  • constを外す変換はできない
  • メモリアライメントが大きくなる変換はできない

下のコードは*const u8型を*u8型に変換しようとしています。

    const x: u8 = 42;
    const pointer = @ptrCast(*u8, &x);

このコードは、以下のコンパイルエラーになります。

error[E09037]: cast discards 'const' qualifier
    const pointer = @ptrCast(*u8, &x);
                    ~

次のコードは*u8型から*u32型への変換を試みています。

    var x: u8 = 42;
    const pointer = @ptrCast(*u32, &x);

このコードは、以下のコンパイルエラーになります。

error[E09036]: cast increases pointer alignment
    const pointer = @ptrCast(*u32, &x);
                    ~
note[E00007]: '*u8' has alignment 1
    const pointer = @ptrCast(*u32, &x);
                                    ~
note[E00007]: '*u32' has alignment 4
    const pointer = @ptrCast(*u32, &x);
                             ~

ノート: このような変換を行う場合、@ptrToIntで整数型に変換してから、@intToPtrで再びポインタ型に変換します。Zenの安全性検査を回避するやり方であるため、本当に必要な場合以外、この方法は使うべきではありません。

☰ 人の生きた証は永遠に残るよう ☰
Copyright © 2018-2019 connectFree Corporation. All rights reserved.
Zen, the Zen three-circles logo and The Zen Programming Language are trademarks of connectFree corporation in Japan and other countries.