課題3: 自己参照構造体とPin

マンダトリー要件(80点)

問題1: 自己参照構造体の理解(20点)

1.1 問題の本質(8点)

以下の質問に答えなさい:

  • 自己参照構造体がRustで困難な理由を、ムーブセマンティクスの観点から説明しなさい。(3点)
  • 以下のコードがコンパイルエラーになる理由をメモリレイアウトの図を使って説明しなさい:
  • struct SelfRef<'a> {
        data: String,
        pointer: &'a str,
    }
    
    impl<'a> SelfRef<'a> {
        fn new(text: String) -> Self {
            SelfRef {
                data: text,
                pointer: &text,  // エラー!
            }
        }
    }
    

    (5点)

    1.2 解決策の比較(12点)

    3つの解決策を比較分析しなさい:

  • Pinを使う方法
  • 間接参照(Box)を使う方法
  • ムーブ時に更新する方法(C++のムーブコンストラクタ的アプローチ)

各方法について:

  • 実装の複雑さ
  • 実行時コスト
  • 安全性の保証

を説明しなさい。(各4点)

---

問題2: Pinの実装(25点)

2.1 基本的なPinned構造体(15点)

以下の仕様に従って、Pinned構造体を実装しなさい:

use std::pin::Pin;
use std::marker::PhantomPinned;

/// 文字列データとそのスライスへの参照を持つ構造体
struct StringSlice {
    data: String,
    slice: *const str,
    _pin: PhantomPinned,
}

impl StringSlice {
    /// 新しいStringSliceを作成(Pinnedされた状態で返す)
    fn new(s: String, start: usize, end: usize) -> Pin<Box<Self>> {
        // TODO: 実装
        todo!()
    }

    /// データ全体を取得
    fn get_data(self: Pin<&Self>) -> &str {
        // TODO: 実装
        todo!()
    }

    /// スライス部分を取得
    fn get_slice(self: Pin<&Self>) -> &str {
        // TODO: 実装(unsafeブロックが必要)
        todo!()
    }
}

要件

  • new()メソッドで正しく自己参照を設定すること(5点)
  • get_data()get_slice()を安全に実装すること(5点)
  • テストコードを書いて動作を確認すること(5点)

テストケース

#[test]
fn test_string_slice() {
    let s = StringSlice::new("Hello, World!".to_string(), 0, 5);

    s.as_ref().with(|s| {
        assert_eq!(s.get_data(), "Hello, World!");
        assert_eq!(s.get_slice(), "Hello");
    });
}

2.2 Pinned構造体の操作(10点)

StringSliceに以下のメソッドを追加実装しなさい:

impl StringSlice {
    /// スライスの範囲を更新(新しいPinnedインスタンスを返す)
    fn update_slice(
        self: Pin<&mut Self>,
        start: usize,
        end: usize,
    ) -> Result<(), &'static str> {
        // TODO: 実装
        // unsafeブロック内で slice ポインタを更新
        todo!()
    }

    /// データの長さを取得
    fn data_len(self: Pin<&Self>) -> usize {
        // TODO: 実装
        todo!()
    }

    /// スライスの長さを取得
    fn slice_len(self: Pin<&Self>) -> usize {
        // TODO: 実装
        todo!()
    }
}

要件

  • update_slice()で範囲チェックを実装すること(4点)
  • 各メソッドが正しく動作すること(3点)
  • テストを作成すること(3点)

---

問題3: Unpin vs !Unpin(20点)

3.1 Unpinな構造体(10点)

以下のUnpinな構造体を実装しなさい:

struct Movable {
    x: i32,
    y: String,
}

impl Movable {
    fn new(x: i32, y: String) -> Self {
        Movable { x, y }
    }

    fn into_pinned(self) -> Pin<Box<Self>> {
        Box::pin(self)
    }

    fn from_pinned(pinned: Pin<Box<Self>>) -> Self {
        // TODO: Pin::into_inner()を使って実装
        todo!()
    }
}

#[test]
fn test_movable() {
    let m1 = Movable::new(42, "test".to_string());
    let pinned = m1.into_pinned();

    // Unpinなのでムーブできる
    let m2 = Movable::from_pinned(pinned);
    assert_eq!(m2.x, 42);
    assert_eq!(m2.y, "test");
}

要件

  • from_pinned()を正しく実装(5点)
  • なぜこれが安全か説明すること(5点)
  • 3.2 !Unpinな構造体の制限(10点)

    以下のコードを分析し、質問に答えなさい:

    use std::marker::PhantomPinned;
    
    struct NotMovable {
        data: String,
        ptr: *const String,
        _pin: PhantomPinned,
    }
    
    fn attempt_move(pinned: Pin<Box<NotMovable>>) {
        // 以下のコードはコンパイルエラーになる
        // let moved = Pin::into_inner(pinned);
    }
    

  • なぜPin::into_inner()が使えないか説明しなさい。(3点)
  • PhantomPinnedを削除するとどうなるか説明しなさい。(3点)
  • !Unpinを保ちつつデータにアクセスする方法を示しなさい。(4点)

---

問題4: Future Traitとライフタイム(15点)

4.1 SimpleFutureの実装(15点)

以下の簡単なFutureを実装しなさい:

use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};

/// カウンターFuture: 指定回数poll()されたらReady
struct CounterFuture {
    count: u32,
    target: u32,
}

impl CounterFuture {
    fn new(target: u32) -> Self {
        CounterFuture { count: 0, target }
    }
}

impl Future for CounterFuture {
    type Output = u32;

    fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
        // TODO: 実装
        // count をインクリメントし、target に達したら Poll::Ready(count)
        // そうでなければ Poll::Pending
        todo!()
    }
}

#[tokio::test]
async fn test_counter_future() {
    let result = CounterFuture::new(3).await;
    assert_eq!(result, 3);
}

要件

  • poll()を正しく実装すること(5点)
  • なぜself: Pin<&mut Self>が必要か説明すること(5点)
  • get_unchecked_mut()を使ってcountを更新すること(5点)

ヒント

impl Future for CounterFuture {
    fn poll(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
        unsafe {
            let this = self.as_mut().get_unchecked_mut();
            this.count += 1;
            // ...
        }
    }
}

---

ボーナス要件(20点)

ボーナス1: ouroboros風のマクロ実装(10点)

簡易版の自己参照構造体マクロを実装しなさい:

// 使用例
simple_self_ref! {
    struct MyStruct {
        data: String,
        slice: &'this str,  // data を参照
    }
}

// 展開後のイメージ:
// - MyStructBuilder を生成
// - with_slice() メソッドを提供
// - Pin<Box<MyStruct>> を返す

実装要件

  • proc_macroではなく、macro_rules!で実装(5点)
  • 基本的な自己参照のセットアップを自動化(3点)
  • サンプルコードで動作を確認(2点)

ヒント

macro_rules! simple_self_ref {
    (
        struct $name:ident {
            $data_field:ident: $data_type:ty,
            $ref_field:ident: &'this $ref_type:ty,
        }
    ) => {
        struct $name {
            $data_field: $data_type,
            $ref_field: *const $ref_type,
            _pin: std::marker::PhantomPinned,
        }

        // Builder や with_ メソッドを生成
        // ...
    };
}

---

ボーナス2: 高度な自己参照パターン(10点)

複数の自己参照を持つ構造体を実装しなさい:

use std::pin::Pin;
use std::marker::PhantomPinned;

/// テキストデータと2つの異なるスライスを持つ
struct MultiSlice {
    text: String,

    // 前半部分への参照
    first_half: *const str,

    // 後半部分への参照
    second_half: *const str,

    _pin: PhantomPinned,
}

impl MultiSlice {
    fn new(text: String) -> Pin<Box<Self>> {
        // TODO: 実装
        // text を2つに分割して、それぞれのポインタを設定
        todo!()
    }

    fn get_text(self: Pin<&Self>) -> &str {
        // TODO: 実装
        todo!()
    }

    fn get_first_half(self: Pin<&Self>) -> &str {
        // TODO: 実装
        todo!()
    }

    fn get_second_half(self: Pin<&Self>) -> &str {
        // TODO: 実装
        todo!()
    }

    fn swap_halves(self: Pin<&mut Self>) {
        // TODO: 実装
        // first_half と second_half のポインタを入れ替える
        todo!()
    }
}

#[test]
fn test_multi_slice() {
    let mut ms = MultiSlice::new("HelloWorld".to_string());

    assert_eq!(ms.as_ref().get_first_half(), "Hello");
    assert_eq!(ms.as_ref().get_second_half(), "World");

    ms.as_mut().swap_halves();

    assert_eq!(ms.as_ref().get_first_half(), "World");
    assert_eq!(ms.as_ref().get_second_half(), "Hello");
}

要件

  • 複数のポインタを正しく管理(4点)
  • swap_halves()の安全な実装(3点)
  • すべてのテストをパス(3点)
  • ---

    提出要件

  • コードファイル
- src/lib.rs または src/main.rs - すべての問題の実装を含む - コンパイルが通ること

  • テストファイル
- tests/ディレクトリ内に統合テスト - すべてのテストがパスすること

  • ドキュメント
- ANSWERS.mdに各問題の説明を記述 - 特にunsafeを使う理由を明記

  • Cargo.toml
   [package]
   name = "pin-exercise"
   version = "0.1.0"
   edition = "2021"

   [dependencies]
   # ボーナス問題で必要な場合のみ

   [dev-dependencies]
   tokio = { version = "1", features = ["full"] }
   

---

評価基準

マンダトリー(80点)

  • 問題1(20点): 概念の理解度
  • 問題2(25点): Pinの実装力
  • 問題3(20点): Unpin/!Unpinの理解
  • 問題4(15点): Future実装の正確性

ボーナス(20点)

  • ボーナス1(10点): マクロ実装のクリエイティビティ
  • ボーナス2(10点): 複雑な自己参照の処理

コード品質

  • unsafeの適切な使用
  • エラーハンドリング
  • ドキュメンテーション
  • ---

    ヒントとリソース

    Pinの重要なメソッド

    // 安全な操作(Unpin型のみ)
    Pin::new(pointer)
    Pin::into_inner(pinned)
    
    // unsafe操作(すべての型)
    Pin::new_unchecked(pointer)
    Pin::get_unchecked_mut(pinned)
    Pin::map_unchecked_mut(pinned, |x| &mut x.field)
    

    デバッグのコツ

  • コンパイラエラーを読む
- "cannot move out of Pin<&mut T>" - "the trait Unpin is not implemented"

  • メモリレイアウトを図示
   Stack:              Heap:
   ┌──────────┐       ┌─────────────┐
   │ Pin<Box> │──────►│ Actual Data │
   └──────────┘       └─────────────┘
   

  • unsafe使用時のチェックリスト
- ポインタは有効か? - ライフタイムは正しいか? - 不変条件は守られているか?

参考リンク

---

よくある質問

*Q1: なぜ生ポインタ(const T)を使うのか? A1: 参照(&T)はライフタイムが必要だが、自己参照の場合、ライフタイムを表現できない。生ポインタはライフタイムを持たないため、この問題を回避できる。

Q2: Pin::get_unchecked_mut()はいつ安全か? A2: 以下の条件を満たす場合:

  • フィールドがUnpinである
  • ムーブしない保証がある
  • 不変条件を破らない

Q3: PhantomPinnedは実行時コストがあるか?** A3: いいえ。ゼロサイズ型なので、コンパイル時の型チェックにのみ使われる。

---

頑張ってください!Pinの理解は、Rustの高度なメモリ安全性を習得する重要なステップです。