コンパイルエラー以外の局面における「所有権」

この記事は、Rust Advent Calendar 2016の12/19の記事になるはずだったエントリです。

所有権

Rustといえば所有権というのは、Rubyといえばオープンクラス、というくらいの「言語ならでは」の機能だと思います。 基本的にはこういうやつですね。

struct Z {
    x: i32,
}

fn main() {
    let x = Z { x: 1 };
    let y = x;
    println!("x = {}", x.x);//エラー E0382: use of moved value cf. https://doc.rust-lang.org/error-index.html#E0382)
}

Rustを最初に書き始めた時は、なんでこんなに煩いんだろうと思う所有権ですが、慣れてくるとその良さがわかります。 以下では、その具体的な

ところで、RustでI/Oに関するnative libraryを触りたい

ところで、RustでI/Oを、native なコードで触りたいと思ったとします。問題が複雑になるので*1、一旦Unix-like OSに範囲を限定しましょう。 で、UnixでI/Oというと普通はfile descriptorを触るじゃないですか。さて、Rustにfile descriptor関係のライブラリってあったっけ、ということになるのですが、ここで登場するのが、std::os::unix::ioモジュールです。

以下では、file descriptorを"FD"と表記します。

std::os::unix::ioモジュール

std::os::unix::ioモジュールは、FD周りの取り扱いのためのモジュールです*2。定義されているものも、3つのTraitだけです。AsRawFd、IntoRawFd、FromRawFdの3つだけです。 これらのTraitは、プログラマがその実装を提供すべきTraitというよりも、既存の標準クラスに機能を追加するためのTraitです。ドキュメントを見てみればわかりますが、FileやTcpStream、TcpListenerなど、UnixならFDを持ってそうだなというものについては大体実装を提供しています。

本当は怖い*3TcpListener

ところで、以下のようなコードを考えます。native functionは適切に実装されているものとします。 このプログラムは、callback()関数が呼ばれると異常終了します。なぜでしょうか。

use std::io::prelude::*;
use std::net::TcpListener;
use std::os::unix::io::*;

fn bind() {
    let listener = TcpListener::bind("0.0.0.0:37564").unwrap();
    let fd = listener.as_raw_fd();
    // some native function call to call callback() on connect.
}

fn callback(fd: RawFd) {
    let mut listener = unsafe { TcpListener::from_raw_fd(fd) };
    match listener.accept() {
        Ok((mut stream, _)) => {
            // Some network process...
        }
        Err(_) => {
            panic!("error");
        }
    }
}

fn main() {
    bind();
    loop {
        // some loop waiting process.
    }
}

17行目のエラーに落ちるから死ぬんでしょ、というのは、回答としては30点です。なぜここで、TcpListener#incoming()がエラーになるのか。

答えは、9行目でクローズされているからです。 もちろん9行目はただの関数終端です。ところでここのブロックでは、listenerが寿命を迎えます。listenerはもちろんTcpListenerです。TcpListenerの寿命終端では、FDのクローズ処理が実行され、クローズされます。クローズされたサーバソケットをacceptしたらエラーになるよね、という話です。

だがちょっと待ってほしい

となると、変数寿命が異なるnativeレイヤにどうやって意味のあるFDを渡せば良いのでしょう。元のインスタンスの寿命を抜けた瞬間にクローズするのでは、nativeレイヤにFDを渡しても意味ないではありませんか。結局フルnativeでやらなければならない、ということなのでしょうか?

そこでIntoRawFd

そこで使うべきは、実は上記のas_raw_fd()ではありません。into_raw_fd()を使うのです。

into_raw_fd()のドキュメントには、こう記載があります。

Consumes this object, returning the raw underlying file descriptor.

This function transfers ownership of the underlying file descriptor to the caller. Callers are then the unique owners of the file descriptor and must close the descriptor once it's no longer needed.

(拙訳) このオブジェクトを消費し、このインスタンスが内包している生のFDを返す。

この関数は、内包するFDについて、 その所有権を呼び出し側に移転する 。そして呼び出し側は当該FDの唯一の所有者となり、不要になった場合はそのFDをクローズしなければならない。

これにより、当該FDはもはやFDとして管理され、勝手にクローズされることは無くなります。よって、これを使って得たFDは任意の場所で利用することができます。 その自由度は、上記の記述にある責務、つまり不要になったところでクローズしなければならない、ということと裏腹ですし、実際にFDから復元するのにどのクラスに復元するか(TcpListenerなのかTcpStreamなのかUnixStreamなのかFileなのか)ということは、プログラマが責任を持たねばなりません。

さて、ではこのメソッドを使ってさっきのコードを書き直してみましょう。

use std::io::prelude::*;
use std::net::TcpListener;
use std::os::unix::io::*;

fn bind() {
    let listener = TcpListener::bind("0.0.0.0:37564").unwrap();
    let fd = listener.into_raw_fd();
    // some native function call to call callback() on connect.
}

fn callback(fd: RawFd) {
    let mut listener = unsafe { TcpListener::from_raw_fd(fd) };
    match listener.accept() {
        Ok((mut stream, _)) => {
            // Some network process...
        }
        Err(_) => {
            panic!("error");
        }
    }
}

fn main() {
    bind();
    loop {
        // some loop waiting process.
    }
}

7行目でlistener変数がFDへの所有権を手放しているので、もはやlistenerの寿命にFDが縛られない、ということになります。なので、callbackに制御が回ってきても問題なく処理が続行できるわけですね。

ところで、上記のコードにはまだ問題があります。というのは、せっかくのTcpListenerが、callbackの終端でやっぱりcloseされてしまう、ということなのです。なので、より正確には、callback()を以下のように直し、使い回すべきTcpListenerが自動クローズされないようにする必要があるでしょう。

fn callback(fd: RawFd) {
    let mut listener = unsafe { TcpListener::from_raw_fd(fd) };
    match listener.accept() {
        Ok((mut stream, _)) => {
            // Some network process...
        }
        Err(_) => {
            panic!("error");
        }
    }
    listener.into_raw_fd();//所有権を解放
}

というわけで

Rustで、コンパイルエラー以外で「所有権」が問題になる事例について書いてみました。

*1:当方がWindowsのI/O周りは全く知らないという事情もあります。Unix系でも詳しくはありませんが

*2:この名前だと他のIO系のシステムコールとか定義されてそうですよね

*3:イベントドリブンで使うには