第9章 struct・参照型・パッケージ

9.1 この章で学ぶこと

この章で学ぶ内容は以下のとおりです。

表9-1: この章で学ぶこと

学ぶこと

ポイント

struct とは何か

Mojo のユーザー定義型の中心。Python の class との違い

mut self / self の違いとアセンブリ

参照 vs コピーがシンボル名に現れる

trait によるコピー・ムーブ制御

CopyableMovable で値の扱い方を明示

参照型(ref / Pointer)

自己参照やヒープ確保が必要な場面

パッケージとモジュール

__init__.mojo による公開 API の整理

9.2 struct:なぜ class でなく struct か

Python の class との最大の違いは値型か参照型かです。

表9-2: struct:なぜ class でなく struct か

Python class

Mojo struct

メモリ配置

ヒープ上のオブジェクト。変数はそこへのポインタ

スタック上に直接配置(デフォルト)

代入の意味

参照のコピー(同じオブジェクトを指す)

値のコピー(独立した別のデータ)

継承

class Child(Parent): で継承可

継承なし。trait で振る舞いを共有

vtable

あり(動的ディスパッチ)

なし(静的ディスパッチ)

コンパイル時のサイズ確定

不可(任意のフィールドが追加できる)

可(フィールドが固定)

各項目について補足します。

代入の意味:参照共有 vs 値コピー

Python では b = a と書くと、ba と同じオブジェクトを指します。一方を変更するともう一方にも反映されます。

# Python: b と a は同じオブジェクトを指す
class Point:
    def __init__(self, x): self.x = x

a = Point(1)
b = a          # 参照のコピー
b.x = 99
print(a.x)     # → 99(a も変わる)

Mojo の struct では b = a値のコピーになります。ba は完全に独立したデータです。

# Mojo: b は a の独立したコピー
var a = Counter(1)
var b = a          # 値のコピー
b.increment()
print(a.get())     # → 1(a は変わらない)

継承なし、trait による振る舞いの共有

Python の class は継承ができますが、継承は vtable(仮想関数テーブル)を必要とし、メソッド呼び出しのたびにポインタを辿る動的ディスパッチが発生します。 Mojo の struct は継承を持たず、メソッド呼び出しはコンパイル時に解決する静的ディスパッチです。 これにより関数呼び出しオーバーヘッドが排除されます。

振る舞いを共有したい場合は trait を使います(9.6 節)。 trait はインターフェースだけを定義し、実装の継承は行いません。 コンパイラは呼び出し先を静的に確定できるため、動的ディスパッチのコストはかかりません。

コンパイル時のサイズ確定

スタックに値を置くには、コンパイル時にそのサイズが確定している必要があります。 Python の class はプログラム実行中にフィールドを追加できるため、サイズが不確定でヒープにしか置けません。 Mojo の struct はフィールドが宣言時に固定されており、コンパイラがバイト数を計算してスタック領域を確保できます。 スタック配置によってヒープ確保のオーバーヘッドがなく、CPU キャッシュにも乗りやすくなります。

所有権システムとの関係

struct は値型であるため、第 10 章の所有権システムと直接連動します。 struct の変数は明確な「所有者」を持ち、スコープを抜けると __deinit__ が呼ばれてリソースが解放されます。 var 引数修飾子や ^(transfer)による所有権移動も struct の値型としての性質があってこそ機能します。

Python class と Mojo struct の比較

図9-1: Python class と Mojo struct の比較

左(Python class)はヒープに確保したオブジェクトへの参照を変数が保持し、b = a は同じオブジェクトを指す参照のコピーになる。右(Mojo struct)はスタックに値を直接配置し、var b = a は独立した値のコピーになる。vtable・継承の有無とコンパイル時サイズ確定の違いもあわせて示す。

ヒープに置きたい場合

struct のデフォルトはスタック配置ですが、ヒープに置きたい場合はポインタ型でラップします。

StringList[T] はすでにこのパターンを使っています。 String の struct ヘッダ(ポインタ、長さ、タグ)はスタックに置かれますが、文字列データ本体はヒープに確保されます。 struct 自体をヒープに置く場合は次の 2 つが主な選択肢です。

表9-2a: ヒープ配置の選択肢

所有者

特徴

ArcPointer[T]

複数可(参照カウント)

clone() でカウントを増やして共有。カウントが 0 になると自動解放

Pointer[T](unsafe 操作)

手動管理

unsafe_alloc / unsafe_free を自分で呼ぶ。9.7.3 で詳説

ArcPointer[T] の使い方を簡単に示します。

from std.memory import ArcPointer

var p = ArcPointer(Counter(1))
p[].value = 42
var q = p          # 参照カウントが増える(コピーではなく共有)
print(q[].value)   # → 42
# p と q の両スコープを抜けるとカウントが 0 になり自動解放

struct の値型としての性質を保ちながら、複数の場所から同じデータを参照したいときに ArcPointer[T] を使います。 Pointer[T] の unsafe 操作は性能やシステムプログラミングを目的とした低レベルな操作が必要なときに使い、その詳細は 9.7.3 節で扱います。

9.3 struct の基本とアセンブリ

この節では最小の struct である Counter のコンパイル結果を追い、次の 3 点を確認します。

  • struct がスタックに直接置かれること:ヒープ確保は一切発生しない

  • mut selfself の違いが型システム上で区別されること:呼び出し側は同じアドレス渡しでも、書き換えてよいかがコンパイル時に確定する

  • メソッド呼び出しが静的ディスパッチであること:実行時の判定コードが存在しない

Countervalue: Int を 1 つ持つ最小の struct です。 increment() で値を増やし、get() で読み取ります。 コンパイル後のアセンブリには mut selfself の違いが明確に刻まれています。

struct Counter:
    var value: Int

    def __init__(out self, start: Int):
        self.value = start

    def increment(mut self):
        self.value += 1

    def get(self) -> Int:
        return self.value


def main():
    var c = Counter(0)
    c.increment()
    print(c.get())

リスト9-1: structs_what_counter.mojo

このコードをコンパイルすると main() のアセンブリは次のようになります。

; def main():
30:  subq   $0x88, %rsp                 ; スタック 136 バイト確保

; var c = Counter(0)
37:  xorl   %eax, %eax                  ; start = 0 を準備
39:  movl   %eax, %edi                  ; edi にコピー(第 1 引数として使う)
3b:  leaq   0x18(%rsp), %rsi            ; self(Counter の領域)のアドレス
40:  movq   %rsi, 0x10(%rsp)            ; そのアドレスを 0x10(%rsp) に保存
45:  callq  "_...Counter::__init__(::SIMD[::DType(int), ::SIMDLength(1)])"  ; コンストラクタ

; c.increment()
4a:  movq   0x10(%rsp), %rdi            ; 保存しておいた &c を rdi に
4f:  callq  "_...Counter::increment(structs_what_counter::Counter)"  ; mut self

; print(c.get())
54:  movq   0x10(%rsp), %rdi            ; 同じ &c を rdi に
59:  callq  "_...Counter::get(structs_what_counter::Counter)"  ; self のみ

アセンブリを 4 つのブロックに分けて読みます。

① スタック確保(subq $0x88, %rsp

0x88 は 136 バイトです。 Counter が持つ Int(8 バイト)そのものは小さくても、print(c.get()) まで含めた main() 全体で使う一時領域(後述する文字列オブジェクトの組み立て領域を含む)がまとめて確保されるため、値のサイズよりも大きな領域になります。 この 1 命令で main() 全体で使うローカル変数の領域が確保されます。

② コンストラクタ呼び出し(Counter::__init__

37:  xorl   %eax, %eax          ; 引数 start = 0 を準備
39:  movl   %eax, %edi          ; edi へコピー
3b:  leaq   0x18(%rsp), %rsi    ; &c(self の書き込み先アドレス)を rsi に
40:  movq   %rsi, 0x10(%rsp)    ; &c を後続の呼び出しのために保存
45:  callq  Counter::__init__   ; コンストラクタを呼ぶ

leaq 0x18(%rsp), %rsic の置き場所(スタック上のアドレス)を __init__ に渡しています。 out self は「まだ空のメモリ領域に自分で書き込む」という意味で、その領域のアドレスが rsi 経由で渡されます。 このアドレスは 0x10(%rsp) にも保存され、後続の increment() / get() 呼び出しで使い回されます。

c.increment()Counter::increment(structs_what_counter::Counter)

4a:  movq   0x10(%rsp), %rdi    ; 保存しておいた &c を rdi に
4f:  callq  Counter::increment  ; mut self を受け取る

mut self を宣言したメソッドには、②で保存した c のアドレス(&c)がそのまま渡されます。 increment の内部で self.value += 1 を行うと、このアドレスが指す元の c が直接書き換わります。

c.get()Counter::get(structs_what_counter::Counter)

54:  movq   0x10(%rsp), %rdi    ; 同じ &c を rdi に
59:  callq  Counter::get        ; self のみを受け取る

mut なしの self も、呼び出し側のコードとしては ③ と同じく &c を渡しているだけです。 Mojo コンパイラが「getc を変更しない」と確定しているため、increment と同じくアドレス渡しのまま安全に実装できます。

注釈

以前の Mojo では、increment(mut self) のシンボル名には Counter::increment(Counter&) のように & が付き、get(self) のシンボル名 Counter::get(Counter) には付かないという表記上の違いがありました。 Mojo 1.0.0 ではこのシンボル表記が変わり、両者とも structs_what_counter::Counter という同じ形式で現れます(上記の ③ ④ で確認したとおりです)。 ただし呼び出し側の実際の命令(movq 0x10(%rsp), %rdi&c を渡す)は mut selfself のどちらでも同じであり、この点は以前から変わっていません。 mut selfself かは、呼び出せる操作の範囲(書き換えてよいか)としてコンパイル時に区別されており、それはシンボル名の表記とは独立した、型システム上の制約です。

シンボル名がアセンブリに現れた形をまとめます。

表9-3: struct の基本とアセンブリ

メソッド定義

シンボル名

意味

def increment(mut self):

Counter::increment(structs_what_counter::Counter)

&c(可変参照)のアドレスを受け取る。元の c を直接書き換える

def get(self) -> Int:

Counter::get(structs_what_counter::Counter)

同じく &c のアドレスを受け取るが、コンパイラが変更しないと確定しているため安全

mut selfself かは、コンパイル時に確定する型システム上の制約です。 実行時に「どちらのメソッドか」を判定するコードは生成されません。

コラム: struct のスタックレイアウト

Countervar value: Int を 1 つ持ちます。Int は 8 バイトです。プロローグの subq $0x88, %rsp(136 バイト確保)の内訳は:

  • 0x18(%rsp)0x1f(%rsp)(8 バイト): Counter.value の領域

  • 残りは &c の退避スロット(0x10(%rsp))と、print(c.get()) のための一時領域

Python の Counter(0) はヒープに PyObject を確保し、参照カウント・型ポインタ・value フィールドをヒープに配置します。Mojo の Counter 自体はスタック 8 バイト(Int 1 個分)で完結しており、ヒープ確保は一切ありません(136 バイトの大半は print() 呼び出しのための領域です)。

Mojo struct のアセンブリレベルの構造

図9-2: Counter struct のアセンブリレベルの構造

左側に 3 つのアセンブリブロック(コンストラクタ・可変参照・読み取り)、右側にスタックレイアウトとシンボル名の違いを示す。subq $0x88, %rsp で 136 バイトのスタック領域を確保し、Counter.value0x18(%rsp) に直接配置される。mut selfself はいずれも structs_what_counter::Counter としてシンボル名に現れ、&c を渡す呼び出し規約自体は同じだが、書き換えてよいかどうかは型システム上コンパイル時に区別される。

9.4 __init__out self

__init__struct のコンストラクタです。 var c = Counter(0) と書いたとき、コンパイラは Counter::__init__ を呼んで c を初期化します。 Python の __init__ と名前は同じですが、第 1 引数の扱いが異なります。

Python では __init__self はすでに確保されたオブジェクトを指しています。 Mojo では __init__self はまだ何も書かれていない空のメモリ領域です。 そのため Mojo の __init__ では第 1 引数を out self と修飾します。

struct Counter:
    var value: Int

    def __init__(out self, start: Int):
        self.value = start   # 空のメモリに初めて書き込む


def main():
    var c = Counter(5)
    print(c.value)

リスト9-2: counter_init.mojo

out self は「このメソッドが self のすべてのフィールドを初期化する責任を持つ」という宣言です。 フィールドを書き忘れるとコンパイルエラーになります。 Python では未初期化フィールドを持つオブジェクトが実行時まで存在できますが、Mojo ではそれをコンパイル時に防ぎます。

__init__ の第 1 引数が Python の self と異なる理由を整理します。

表9-4: __init__out self

引数修飾子

役割

out self

__init__ 専用。まだ初期化されていない self に値を書き込む

mut self

呼び出し元の self を変更するメソッド

self(修飾子なし)

self を読み取るだけ。コピーまたは read

out は「self はまだ空の状態で渡される。このメソッドが初期化の責任を持つ」という意味です。 Python の __init__None を返すのと似た発想ですが、Mojo ではコンパイラがフィールドの未初期化を検出できます。

__init__ における self の違いと初期化フロー

図9-3: __init__ における self の違いと初期化フロー

Python の self はすでに確保済みのオブジェクトを指すが、Mojo の out self は空のメモリ領域(0x10(%rsp))を指す。__init__leaq でそのアドレスを受け取り movq で値を書き込むことで初期化を完了する。フィールドを書き忘れた場合は実行時ではなくコンパイル時にエラーが検出される。

9.5 @implicit と暗黙変換

Mojo はデフォルトで暗黙の型変換を行いません。 describe_distance(10.0) と書いたとき、引数の型が Miles であれば Float64 はそのままでは渡せません。 @implicit は、この「明示的に変換しなくてよい」という許可をコンパイラに与えるデコレータです。

struct Miles:
    var value: Float64

    @implicit
    def __init__(out self, km: Float64):
        self.value = km * 0.621371


def describe_distance(m: Miles):
    print("miles =", m.value)


def main():
    describe_distance(10.0)

リスト9-3: structs_implicit_miles.mojo

describe_distance(10.0) が呼ばれたとき、コンパイラは次の手順を踏みます。

  1. 引数の型 Float64、期待する型 Miles → 型が合わない

  2. Miles__init__@implicit が付いているものを探す

  3. def __init__(out self, km: Float64) が見つかる → Miles(10.0) に変換

  4. 変換後の値を describe_distance に渡す

この変換はコンパイル時に完結します。 実行時の型チェックは発生しません。

表9-5: @implicit と暗黙変換

ポイント

詳細

@implicit

この __init__ をコンパイラが暗黙変換のルートとして使える

describe_distance(10.0) の動作

Float64Miles として期待する文脈なので、Miles(10.0) に変換される

利点

呼び出し側を簡潔に書ける。リテラルをそのまま渡せる

注意点

暗黙変換が多すぎると「なぜ型が合っているか」が追いにくくなる

@implicit を付けない場合は describe_distance(Miles(10.0)) と明示的に書く必要があります。 変換の意図を読者に伝えたいときは明示、利便性を優先したいときは @implicit と使い分けます。

@implicit による暗黙の型変換

図9-4: @implicit による暗黙の型変換

左(Without @implicit)は Float64 をそのまま渡すとコンパイルエラーになり明示的な変換が必要。右(With @implicit)はコンパイラが Float64 Miles(10.0) に自動変換する。下段にコンパイル時の変換プロセス 4 ステップと、利便性と明示性のトレードオフを示す。

9.6 trait:コピー・ムーブを型で表明する

trait は「この型が持つ能力」をコンパイル時に宣言する仕組みです。 Python の Protocol や ABC に相当しますが、Mojo では duck typing ではなく、型定義の段階で能力を明示します。

struct Label(Copyable):(Copyable) は「この struct は Copyable trait を満たす」という宣言です。 コンパイラはこの宣言に基づいて a.copy() の呼び出しを許可します。 宣言がなければコンパイルエラーになります。

@fieldwise_init
struct Label(Copyable):
    var text: String


def main():
    var a = Label("hello")
    var b = a.copy()
    print(a.text, b.text)

リスト9-4: structs_copyable_label.mojo

Copyable を満たすには __copyinit__ メソッドが必要ですが、@fieldwise_init を付けた struct はコンパイラが自動生成します。 フィールドがすべてコピー可能な型(String, Int, Float64 など)であれば、(Copyable) と宣言するだけで .copy() が使えます。

表9-6: trait:コピー・ムーブを型で表明する

trait

意味

付けないとどうなるか

Copyable

.copy() でコピーできる

a.copy() がコンパイルエラー

Movable

^ で所有権転送できる

f(a^) に渡せない

CopyableMovable を含意

コピーできるなら move もできる

Copyable だけ宣言しておけば Movable は自動で付いてきます。 逆に Movable だけ宣言した型は「move はできるがコピーはできない」という意味になります。 これは大きなバッファや排他リソースを持つ struct に使うパターンです。

コラム: なぜデフォルトでコピーを許さないか

Rust や Mojo のような所有権を持つ言語では、コピーが起きる場所を明示することがパフォーマンスに効きます。

大きな ListString を含む struct を暗黙にコピーすると、予期しないヒープ確保が起きます。 Copyable を型定義で宣言することで、「この型のコピーは意図的な設計」と明示できます。

Python では代入はすべて参照コピーのため、このような問題は起きません。 ただし「値を変えたはずが元も変わっていた」というバグの原因にもなります。

trait による Copyable と Movable の使い分け

図9-5: trait による CopyableMovable の使い分け

左から Copyable only(コピーもムーブも可)、Movable only(ムーブのみ)、No traits(どちらも不可)の 3 パターンを示す。Copyable Movable の包含関係、@fieldwise_init による自動生成の有無、Python / Rust / Mojo のデフォルト動作の比較もあわせて示す。

9.7 参照型

struct は値型です。 変数に代入するとデータそのものがコピーされ、スタック上に実体が置かれます。 一方、参照型は「データの場所(アドレス)」を保持します。 データ自体はヒープ上にあり、複数の変数が同じ実体を指すことができます。

Mojo は値型を基本にしていますが、参照型が必要になる場面があります。 refPointerArcPointer の 3 種類がその代表で、それぞれ「借用」「手動管理」「共有所有」という異なる役割を持ちます。 Mojo 1.0 では、かつて別の型だった UnsafePointerPointer に統合され、危険な操作(確保・解放・オフセット計算など)は unsafe_ 接頭辞のメソッドで明示するようになりました。

9.7.1 なぜ参照が必要になるか

struct の値はスタック上に配置されるため、コンパイル時にサイズが確定していなければなりません。 しかし次のようなケースではスタックだけでは対応できません。

  • struct Nodenext: Node フィールドを持つ場合 → Node のサイズに Node が含まれ無限再帰になる

  • 実行時まで長さが決まらない可変長データを扱う場合

  • 複数の所有者が同一データを共有する場合

これらはいずれもヒープ上に値を置いてポインタ経由でアクセスすることで解決します。 Mojo が提供する主な参照型とポインタ型を目的別に整理すると次のとおりです。

表9-7: なぜ参照が必要になるか

種類

用途

ref

参照(借用)

既存の値への一時的なエイリアス。for ref x in list: など

Pointer[T](安全な借用)

非所有ポインタ

ライフタイムと連動した安全なポインタ

Pointer[T](unsafe 操作)

生ポインタ

ヒープの直接操作(unsafe_alloc/unsafe_free等)。解放はプログラマが管理

ArcPointer[T]

参照カウントポインタ

複数の所有者が必要な場合。解放は自動

ref は既存の値を「借りる」だけです。 所有権は移動しません。 ref が有効なのは借りた元の値が生きている間だけです。 Pointer の unsafe 操作はヒープの生アドレスを持ちます。 解放の責任はプログラマにあります。 ArcPointer は参照カウントを内部に持ち、最後の所有者がスコープを抜けたときに自動解放します。

参照・ポインタが必要になる理由

図9-6: 参照・ポインタが必要になる理由

スタックだけでは解決できない 3 つの問題(自己参照構造、可変長データ、複数所有者)と各問題に対応する解決策を示す。下段は refPointer[T](安全な借用/unsafe 操作)、ArcPointer[T] の種類、用途、解放方法の一覧。

9.7.2 ref:インプレース更新

ref は「コピーせずに元の値を直接読み書きしたい」ときに使います。 for ループで最もよく登場します。

def main():
    var xs: List[Int] = [1, 2, 3]
    for ref x in xs:
        x *= 2
    print(xs)

リスト9-5: reference_ref_list_double.mojo

for ref x in xs: と書いた場合と書かなかった場合の違いを比較します。

# ref なし:各要素のコピーに対して操作する
for x in xs:
    x *= 2   # xs の中身は変わらない

# ref あり:各要素への参照を直接書き換える
for ref x in xs:
    x *= 2   # xs の中身が [2, 4, 6] に変わる

ref xxs[i] のアドレスへのエイリアスです。 x *= 2 はそのアドレスに乗算結果を書き戻す操作になります。 アセンブリレベルでは lea でアドレスを取得して直接書き込む命令列になります(コピーを介しません)。

ref は借用なので、xs が有効な間だけ使えます。 ループの外に ref を持ち出すことはできません。

ref によるインプレース更新

図9-7: ref によるインプレース更新

左(Without ref)は x が要素のコピーのため xs は変わらない。右(With ref)は ref xxs[i] のアドレスへのエイリアスとなり xs が直接書き換わる。アセンブリレベルでは movq によるコピー操作と leaq + imulq によるアドレス経由の直接書き換えの違いとして現れる。

9.7.3 Pointer の unsafe 操作:ヒープの直接操作

Mojo 1.0 より前は、手動管理専用の UnsafePointer[T] という別の型が存在しました。 1.0 では Pointer[T] に統合され、危険な操作(確保・書き込み・解放など)は unsafe_ 接頭辞のメソッドで明示するようになりました。 unsafe_alloc でメモリを確保し、unsafe_free で解放するまでの責任はすべてプログラマが持ちます。 基本的な操作の流れは次のとおりです。

from std.memory.alloc import unsafe_alloc

var ptr = unsafe_alloc[Int](1)       # ヒープに 8 バイト確保
ptr.unsafe_write(42)                 # アドレスに 42 を書き込む
print(ptr[])                         # デリファレンスして読む → 42
ptr.unsafe_free()                    # 解放(忘れるとメモリリーク)

手動管理のリスクを避けるには __deinit__ を持つ struct にラップする RAII パターンが有効です。 アセンブリレベルの動作、複数要素の扱い、RAII パターンの詳細は 第12章 ポインタ・GPU・レイアウト で取り上げます。

Pointer の unsafe 操作によるヒープの直接操作

図9-8: Pointer の unsafe 操作によるヒープの直接操作

4 段階のライフサイクル(Allocation → Initialize → Dereference → Free)をスタックとヒープのメモリ図とアセンブリ命令で示す。下段に危険性(use-after-free、double-free、リーク)と RAII パターンによる安全化、使用上のポイントをあわせて示す。

9.7.4 ArcPointer:参照カウントによる共有

Pointer の unsafe 操作は所有者が 1 人であることを前提にします。 複数の変数が同じヒープオブジェクトを共有したい場合は ArcPointer(Atomic Reference Counting Pointer)を使います。

ArcPointer は内部に参照カウンタを持ちます。 arc2 = arc のようにコピーするたびにカウントが増え、スコープを抜けるたびに減ります。 カウントが 0 になった時点でヒープが自動解放されます。

from std.memory import ArcPointer


@fieldwise_init
struct Config(Copyable):
    var value: Int


def main():
    var arc = ArcPointer(Config(42))
    var arc2 = arc          # 参照カウントが 2 になる
    arc2[].value = 99
    print(arc[].value)      # 99(同じヒープオブジェクトを共有)
    # arc, arc2 がスコープを抜けるとカウントが 0 になり自動解放

リスト9-6: reference_arc_pointer_config.mojo

コードのポイントを整理します。

  • ArcPointer(Config(42))Config をヒープ上に置き、参照カウント 1 で初期化

  • var arc2 = arc:カウントが 2 になる。arcarc2 は同じヒープ上の 1 つの Config を指す

  • arc2[].value = 99arc2 経由で書き換えると、arc[] からも 99 が見える

  • スコープを抜けると arc2 のカウントが先に 1 減り、最後に arc のカウントが 0 になって解放される

Pointer の unsafe 操作との最大の違いは、カウントが 0 になった瞬間に自動解放される点です。 unsafe_free を手動で呼ぶ必要がなく、__deinit__ も不要です。

コラム: Arc の “A” は Atomic(アトミック)

ArcPointer の参照カウンタの増減は CPU のアトミック命令(x86 では lock xaddq、ARM では stlxr)で行われます。 複数スレッドから同じオブジェクトを共有しても安全なのはこのためです。

ただし、アトミック操作はメモリバリアを伴うため、シングルスレッドのみの用途ではカウント操作のコストが無駄になります。 スレッドをまたがない場合は Pointer の unsafe 操作 + RAII パターンのほうがオーバーヘッドが少なくなります。

ArcPointer による参照カウントの共有

図9-9: ArcPointer による参照カウントの共有

5 段階のライフサイクル(初期作成 RC=1 → コピーで RC=2 → 共有書き換え → arc2 スコープ抜けで RC=1 → arc スコープ抜けで RC=0、自動解放)をメモリ図で示す。最終段階で lock xaddq によるアトミックデクリメントと callq destructor が実行される。下段に Pointer の unsafe 操作との所有者、解放、コストの比較表を示す。

9.7.5 Pointer:安全な非所有ポインタ

Pointer[T] は、標準ライブラリが提供する安全な非所有ポインタです。 データを所有せず、参照先のライフタイム(origin)情報をコンパイラが追跡する点が、9.7.3 節で見た unsafe_ 系メソッドによる手動管理との違いです。

unsafe_alloc/unsafe_free のような unsafe 操作は「解放済みアドレスにアクセスする」「2 回解放する」などのミスをコンパイラが検出できません。 Pointer[T] を安全な借用として使う場合はライフタイム情報を持つため、こうした誤りを型システムで防ぎやすくなっています。

# 概念例(API は版により異なります)
var x: Int = 42
var p = Pointer(to=x)   # x を指す安全なポインタ
print(p[])               # 42

ref がスタック上の既存値への短命な借用であるのに対し、Pointer はライフタイム情報を持つ参照として、より広い場面で使えます。 ただし生メモリの確保・解放や任意のアドレス操作が必要な低レベル処理では、引き続き Pointer の unsafe 操作(unsafe_alloc/unsafe_free等)を使います。

9.8 Packages

Package(パッケージ)は __init__.mojo を持つディレクトリです。 複数の .mojo ファイルに分散したコードをひとつの名前空間としてまとめ、利用者には from shapes import Rect のような単純な import 経路を提供します。

Python の __init__.py と同じ発想ですが、Mojo では型情報がコンパイル時に確定するため、パッケージの境界が「何を公開するか」の明確な仕切りになります。 利用者はパッケージ内部のファイル構成を知らなくても、__init__.mojo に並べられた名前だけを使えます。

9.8.1 ディレクトリ構成と各ファイルの役割

サンプルは shapes パッケージを main.mojo から使う構成です。

packages_demo/
  main.mojo           ← エントリポイント
  shapes/
    __init__.mojo     ← 公開 API をここに集める
    rect.mojo         ← Rect 型の定義
    util.mojo         ← 補助関数

それぞれのファイルを順に見ていきます。

rect.mojoRect 型を定義します。

@fieldwise_init
struct Rect:
    var width: Int
    var height: Int

    def area(self) -> Int:
        return self.width * self.height

リスト9-7: rect.mojo

shapes パッケージの内部モジュールです。 Rectshapes/rect.mojo に定義されていますが、利用者はこのパスを直接 import する必要はありません。

util.mojo:パッケージ内で使う補助関数を置きます。

def min_dim(a: Int, b: Int) -> Int:
    return a if a < b else b

リスト9-8: util.mojo

init.mojo:パッケージの公開 API を一か所にまとめます。

from .rect import Rect
from .util import min_dim

リスト9-9: __init__.mojo

from .rect import Rect. は「同じ shapes/ ディレクトリ内の rect.mojo」を指す相対 import です。 __init__.mojo に再エクスポートを書いておくことで、利用者は from shapes import Rect とだけ書けばよく、ファイル分割の内部構造を隠せます。

main.mojo:利用者側のコードです。

from shapes import Rect, min_dim


def main():
    var r = Rect(3, 4)
    print(r.area())
    print(min_dim(r.width, r.height))

リスト9-10: main.mojo

from shapes import Rect, min_dimshapes はディレクトリ名です。 main.mojo と同じ階層に shapes/ ディレクトリがあり、その中に __init__.mojo があれば Mojo はそこを package として認識します。

表9-8: ディレクトリ構成

要素

Python との比較

Mojo

パッケージの定義

__init__.py があるディレクトリ

__init__.mojo があるディレクトリ

公開 API の整理

__all__ リストや __init__.py での再エクスポート

__init__.mojo での再エクスポート

相対 import

from .module import X

同じ(. は同階層、.. は上位)

配布用ビルド

setup.py / pyproject.toml

mojo precompile shapes -o shapes.mojoc

配布用ビルドについて補足します。 mojo precompile shapes -o shapes.mojoc を実行すると、shapes/ ディレクトリ全体がコンパイル済みの単一ファイル shapes.mojoc に変換されます。 利用者は shapes/ ディレクトリを手元に置かなくても shapes.mojoc だけで import できます。 ソースから import するときはディレクトリ名がパッケージ名になりますが、コンパイル済みパッケージから import するときはファイル名がパッケージ名になります(名前を変えたいときは .mojoc をリネームするのではなく mojo precompile をやり直します)。 ただし .mojoc は、それを生成したコンパイラのバージョンに強く結び付く形式で、汎用の配布形式ではありません。 別バージョンのコンパイラで読み込むとコンパイルエラーになります。 バージョンをまたいで配布したい場合は、ソースをそのまま配る source package(shapes/ ディレクトリ一式)のほうが安全です。

Package と __init__.mojo による公開 API の整理

図9-10: __init__.mojo による公開 API の整理

ディレクトリ構成と各ファイル(rect.mojoutil.mojo__init__.mojomain.mojo)の関係を示す。__init__.mojo が内部モジュールを再エクスポートすることで利用者はシンプルな import 経路を使える。左下に Python との比較表、中央下にパッケージのメリット、右下に import パターン 3 種を示す。

9.9 まとめ

本章で扱った内容を以下にまとめます。

表9-9: まとめ

要素

押さえるべきこと

struct vs class

スタック配置・値型・継承なし・静的ディスパッチ

mut self / self

呼び出し側は同じアドレス渡し。書き換え可否は型システムで区別される

out self

__init__ でのみ使う。コンパイラが未初期化を検出できる

Copyable / Movable

コピー・ムーブを型定義で明示する。デフォルトは禁止

参照の種類

ref(借用)・Pointer(unsafe 操作で手動管理)・ArcPointer(共有所有)

パッケージ

__init__.mojo で公開 API を整理。mojo precompile.mojoc にビルド