ポインタ変数はデータや関数のメモリ上のアドレスを格納するためのものです。ポインタを使用することで、メモリ上のデータやメモリ空間にマッピングされたハードウェアを間接的に参照、操作することが可能です。低レイヤで使用されるシステムプログラミング言語では、パフォーマンス上の理由やハードウェアを直接操作する必要があるため、ポインタを介してメモリ空間を操作します。
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 = &mut 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
からも、メンバアクセス (.
) 演算子でフィールドとメソッドが利用できています。これは、メンバアクセス演算子により、暗黙的にデリファレンスが行われているためです。
ポインタ変数の型はデフォルトでは *T
になり、この型は ポインタが指している値 を書き換えることができません。
値を書き換えることができる(ミュータブルな)ポインタにする場合は、ポインタ変数定義時に mut
をつけて *mut T
という型にする必要があります。
const pointer: *mut i32 = アドレス;
var
で定義したミュータブルな変数のアドレスでポインタ変数を初期化する際にミュータブルなポインタにする場合は明示的に mut
をつける必要があります。
var x: i32 = 42;
const pointer = &mut x;
同様に、引数にミュータブルなポインタを要求する関数にポインタを渡す場合も明示的に mut
をつける必要があります。
fn incInteger(p: *mut i32) void {
p.* += 1;
}
var a: i32 = 0;
incInteger(&mut a);
ポインタ変数自体を書き換え可能とするかどうかは、ポインタ変数を定義する際のvar / const
で制御します。
次節のポインタ型でコードの例を示しながら説明します。
Zenには4種類のポインタ型があります。それぞれの違いと使い分け方について説明します。
以下、T
は任意の型、N
は任意の正の数を意味します。
これまでの例で見てきた単一オブジェクトへのポインタ型です。このポインタ型の変数に対しては、デリファレンス (.*
) 演算子が利用できます。
examples/ch02-primitives/src/pointers.zen:42:66
test "pointer to single object" {
// ミュータブルなオブジェクトのアドレスを取得する
var mutable: i32 = 42;
const pointer_of_mutable = &mut mutable;
// ポインタ型は`*mut T`で参照先を読み書き可能
ok(@TypeOf(pointer_of_mutable) == *mut i32);
// デリファレンスにより参照先オブジェクトの値を操作
ok(pointer_of_mutable.* == 42);
pointer_of_mutable.* = 52;
ok(pointer_of_mutable.* == 52);
// イミュータブルなオブジェクトのアドレスを取得する
const immutable: i32 = 42;
const pointer_of_immutable = &immutable;
// ポインタ型は`*T`で参照先には書き込みできない
ok(@TypeOf(pointer_of_immutable) == *i32);
// デリファレンスにより参照先オブジェクトの値を操作
ok(pointer_of_immutable.* == 42);
// Compile error: cannot assign to constant
// pointer_of_immutable.* = 52;
}
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: []i32 = pointer[1..];
ok(slice[0] == 2);
}
これはスライス型そのものです。スライスは配列要素の先頭アドレスと、要素数からなる型です。スライス型はいわゆるファットポインタと呼ばれるものです。ファットポインタとは、「アドレス+制御情報」から構成されるポインタです。
他のポインタ型のサイズがアドレスのビット幅と一致するのに対し、こちらのポインタ型は要素数を格納する分、サイズが大きくなります。
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));
}
この型はスライス型であるため、使い方はもちろんスライスと同じです。
要素数が不明なオブジェクトに対するポインタです。こちらの型に対しては、以下の操作が可能です。
[インデックス]
によるインデックスアクセス[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: []i32 = pointer[0..3];
ok(@TypeOf(slice) == []i32);
}
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: ?*mut i32 = null;
ok(pointer == null);
// `pointer`は`x`を参照する
var x: i32 = 42;
pointer = &mut x;
// Compile error: attempt to dereference non-pointer type '?*mut 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言語と相互にポインタをやり取りするため、C言語互換のポインタ型が存在します。それが[*c]T
型です。
このCポインタ型は、必要がない限り利用するべきではありません。Cポインタを利用するケースは、CソースコードからZenコードを自動生成する場合のみです。
詳細は、14章 Cとのインターフェースで説明します。
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: *mut u32 = &mut 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: *mut volatile u32 = &mut 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
1
、2
、3
の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 == std.Target.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
で修飾されたポインタは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);
}
しかし、いくつか制限があります。
mut
がついていないポインタ型に mut
をつける変換はできない下のコードは*u8
型を*mut u8
型に変換しようとしています。
const x: u8 = 42;
const pointer = @ptrCast(*mut u8, &x);
このコードは、以下のコンパイルエラーになります。
error[E09037]: cast cannot discards 'const' qualifier
const pointer = @ptrCast(*mut 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-2020 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.