Pythonならわかるシステムプログラミング

[pyspa Advent Calendar 2021](https://adventar.org/calendars/6234)のエントリーです。昨日は@kuenishiさんでした。

どんなプログラミング言語でも、最終的にはOSへのシステムコールとなってプログラマーがやりたいことを実現しています。文字列をコンソールに出したり、ファイル入出力、ネットワーク入出力などなど。Goの場合[Goならわかるシステムプログラミング](https://www.lambdanote.com/products/go)という本がありますね。もちろん、みんな一人10冊ずつぐらいお買い上げのことだと思いますが、せっかくなので他の言語でも見てみましょう。

ここで扱うのは[先日、TIOBEインデックスでプログラミング言語の人気No.1になったPython](https://atmarkit.itmedia.co.jp/ait/articles/2110/08/news129.html)です。

ただし、Pythonでメインの処理系のCPythonではなくてPyPyで見ていきます。理由は後から説明します。

PyPyのコードは次の場所でホストされています。

https://foss.heptapod.net/pypy

早速、次のコードの中身を見ていきましょう。

```py
print("hello world")
```

# print()関数

Pythonは何もインポートしなくても使える関数は `__builtins__` の中にいます。PyPyリポジトリの`/pypy/module/__builtin__/`というフォルダがそのコードに該当しそうです。この中のapp_io.pyにそれっぽいものがありました。

https://foss.heptapod.net/pypy/pypy/-/blob/branch/default/pypy/module/__builtin__/app_io.py#L87

Pythonの`print()`はfileオプションで出力先を変更できます。何もしていないければ標準出力をとってきています。内部で定義された`write()`関数はこれの`write()`メソッドを呼んでいます。最後に呼ばれるのはこの`write()`メソッドですね。

```py
def print_(*args, **kwargs):
    """The new-style print function from py3k."""
    fp = kwargs.pop("file", None)
    if fp is None:
        fp = sys.stdout
        if fp is None:
            return
    def write(data):
        if not isinstance(data, basestring):
            data = str(data)
        fp.write(data)
    # 途中省略
    for i, arg in enumerate(args):
        if i:
            write(sep)
        write(arg)
    write(end)

```

次に`sys.stdout`を探索します。この中に定義されていました。

https://foss.heptapod.net/pypy/pypy/-/blob/branch/default/pypy/module/sys/moduledef.py

```py
class Module(MixedModule):
    # 中略
    interpleveldefs = {
        '__name__'              : '(space.newtext("sys"))',
        '__doc__'               : '(space.newtext("PyPy sys module"))',

        'platform'              : 'space.newtext(sys.platform)',
        'maxint'                : 'space.newint(sys.maxint)',
        # 中略
        'stdin'                 : 'state.getio(space).w_stdin',
        '__stdin__'             : 'state.getio(space).w_stdin',
        'stdout'                : 'state.getio(space).w_stdout',
        '__stdout__'            : 'state.getio(space).w_stdout',
        'stderr'                : 'state.getio(space).w_stderr',
        '__stderr__'            : 'state.getio(space).w_stderr',
        # 略
        }
```

この`interpleveldefs`というのはPython本体にはない概念です。[PyPyのドキュメントに説明があります](https://doc.pypy.org/en/latest/coding-guide.html#interpreter-level-definitions)。

そこから参照される[ラッピング](https://doc.pypy.org/en/latest/coding-guide.html#wrapping-rules)に関する説明に、次のようなことが書かれています。

* PyPyには2つのレベルがある
    * アプリケーションレベル: 通常のPythonのインタプリタに近い名前空間(space)
    * インタプリタレベル: 名前空間(space)そのものをいじるなど、よりインタプリタ内部に根ざしたレベル

インタプリタレベルの関数は実行時にspaceへのバインドを持つとありますね。確かに、`stdin`は`space`を引数にとる`getio()`の返り値であります。spaceは[PyPyの名前空間そのもの](https://doc.pypy.org/en/latest/objspace.html)ですね。

ちなみに、最初の`print_()`も実はこの`appleveldefs`の仕組みを使って、`print()`関数と、最初に説明した`print_`を結びつけられていました。

https://foss.heptapod.net/pypy/pypy/-/blob/branch/default/pypy/module/__builtin__/moduledef.py

```py
    appleveldefs = {
        'print'         : 'app_io.print_',
    }

```

# getio()

`getio`はこちらで定義されていました。

https://foss.heptapod.net/pypy/pypy/-/blob/branch/default/pypy/module/sys/state.py

```py
class IOState:
    def __init__(self, space):
        from pypy.module._file.interp_file import W_File
        self.space = space

        w_stdin = W_File(space)
        w_stdin.file_fdopen(0, "r", 1)
        w_stdin.w_name = space.newtext('<stdin>')
        self.w_stdin = w_stdin

        w_stdout = W_File(space)
        w_stdout.file_fdopen(1, "w", 1)
        w_stdout.w_name = space.newtext('<stdout>')
        self.w_stdout = w_stdout

        w_stderr = W_File(space)
        w_stderr.file_fdopen(2, "w", 0)
        w_stderr.w_name = space.newtext('<stderr>')
        self.w_stderr = w_stderr

        w_stdin._when_reading_first_flush(w_stdout)

def getio(space):
    return space.fromcache(IOState)
```

`space.fromcache`は次のところで定義されています。


```py
class ObjSpace(object):
    """Base class for the interpreter-level implementations of object spaces.
    http://pypy.readthedocs.org/en/latest/objspace.html"""

    reverse_debugging = False

    @not_rpython
    def __init__(self, config=None):
        # 略
        self.fromcache = InternalSpaceCache(self).getorbuild
```

cacheという名前の通り、get or buildなので、インスタンスがなければビルドして、そうでなければキャッシュから返すという感じっぽいですね。まあ大きな流れには影響なさそうなのでここはスルーします。stdoutの定義は前述の`IOState`から辿る必要がありますね。`W_File`というクラスがポイントのようです。`w_`という命名規則はアプリケーションレベルとインタプリタレベルの接点となるラッパーの意味です。

# `W_File`クラス

`W_File`は次のコードで定義されています。

https://foss.heptapod.net/pypy/pypy/-/blob/branch/default/pypy/module/_file/interp_file.py

コンストラクタはたいしたことやっていないので、`IOState`で呼ばれている`file_fdopen()`を見てみると、`direct_fdopen()`を呼んでいますね。どんどんいきましょう。

```py
    def file_fdopen(self, fd, mode="r", buffering=-1):
        try:
            self.direct_fdopen(fd, mode, buffering)
        except OSError as e:
            raise wrap_oserror(self.space, e)
```

`direct_fdopen`のコードは次のようになっています。この最後の`fdopenstream()`はインスタンス変数に設定をちょこちょこ書き込んでいるだけなので、本命は`streamio.fdopen_as_stream()`っぽいですね。

```py
    def direct_fdopen(self, fd, mode='r', buffering=-1):
        self.direct_close()
        self.w_name = self.space.newtext('<fdopen>')
        self.check_mode_ok(mode)
        stream = streamio.fdopen_as_stream(fd, mode, buffering,
                                           signal_checker(self.space))
        self.check_not_dir(fd)
        self.fdopenstream(stream, fd, mode)
```

この関数はここで定義されています。いままでは`/pypy/module`以下だったのが、`rpython/rlib`以下に移動しました。

https://foss.heptapod.net/pypy/pypy/-/blob/branch/default/rpython/rlib/streamio.py

# `/rpython/rlib/streamio`

PyPyはPythonで書かれたPythonインタプリタというのを聞いたことがある人はいるでしょう。この前者のPythonというのが訳ありで、RPythonという特別なルールに限定された機能縮小版のPythonです。

ここで軽く説明しています。

* [PyPyよりも5倍高速な最速のPython処理系](http://blog.shibu.jp/article/51727956.html)

Pythonではあるが、ネイティブコードに翻訳可能なレベルに限定されたPythonです。つまり、ここから下はネイティブコード化されるということですね。でもPythonのまま読めるというのがPyPyの特徴です。

`fdopen_as_stream()`の実装は次のようになっています。

```py
def fdopen_as_stream(fd, mode, buffering=-1, signal_checker=None):
    os_flags, universal, reading, writing, basemode, binary = decode_mode(mode)
    _check_fd_mode(fd, reading, writing)
    _setfd_binary(fd)
    stream = DiskFile(fd, signal_checker)
    return construct_stream_tower(stream, buffering, universal, reading,
                                  writing, binary)
```

``construct_stream_tower()``は、ファイル入出力にバッファリングを追加する感じのように見えます。システムコールを頻繁に呼び出すとパフォーマンスが落ちるのである程度まとめて書き込んだり、読み込んだりするのを「バッファリングする」といいます。

OSへのアクセスは`DiskFile`クラスですね。こちらもこのファイルの中に定義されています。こちらの`write()`メソッドが最初の`print_()`で呼ばれるやつですね。`os.write()`を呼んでいます。

```py
def write(self, data):
    while data:
        try:
            n = os.write(self.fd, data)
        except OSError as e:
            if e.errno != errno.EINTR:
                raise
            if self.signal_checker is not None:
                self.signal_checker()
        else:
            data = data[n:]
```

`os.write()`っていうと、アプリケーションレベルの`os`パッケージにもどってしまっています。ネイティブコードからインタプリタに依存することはないはずなので、これから先はすべてrlibのはず。せっかくネイティブコードに戻ったのに、そんなはずはない、とrlibの中を見たら、rposix.pyにこんな関数がありました。

https://foss.heptapod.net/pypy/pypy/-/blob/branch/default/rpython/rlib/rposix.py

rlibの中のosパッケージはこちらの関数に置き換えられているようですね。歴史的経緯ですかね。

```py
@replace_os_function('write')
@signature(types.int(), types.any(), returns=types.any())
def write(fd, data):
    count = len(data)
    with rffi.scoped_nonmovingbuffer(data) as buf:
        ret = c_write(fd, buf, count)
        return handle_posix_error('write', ret)
```

`c_write（）`ってのを呼んでいますね。だいぶ核心に近づいた感じがします。Windows以外だと次のような感じで定義されています。

```py
    c_write = external('write',
                   [rffi.INT, rffi.VOIDP, POSIX_SIZE_T], POSIX_SSIZE_T,
                   save_err=rffi.RFFI_SAVE_ERRNO)
```

`external()`の定義は以下のようになっています。

```py
def external(name, args, result, compilation_info=eci, **kwds):
    return rffi.llexternal(name, args, result,
                           compilation_info=compilation_info, **kwds)
```

rffi周りについてはドキュメントにまとまっています。ここからlibcの関数にいって、OSへのシステムコールに変換されて文字列の出力になります。

https://rpython.readthedocs.io/en/latest/rffi.html

libcはシステムコールを呼び出すためのC言語のラッパーライブラリです。ここでC言語かよ、と思われるかもしれませんが、C言語の標準ライブラリと違い、libcはかなり低レベルな関数集です。メモリ管理ともなるとlibcの中でもかなり複雑なことをしていますが、`write()`ぐらいになると、OSのシステムコールとほぼ1:1です。Goの場合はここまで自前で実装していましたが、Go以外の多くの言語はWindows以外はlibcを使って実装されています。WindowsはWin32 APIとかを使っています。システムコールの番号も公式に固定されているわけではなく、各OSが保証しているのはlibcの関数を使えば動くよ、というだけですので、むしろGoが変わっています。あえてクロスコンパイルのためにそのリスクをかぶっています。

# まとめ

PyPyのソースを見ながら、`print()`から、libcの関数を呼び出すところまで辿っていきました。

なぜPyPyかという種明かしをすると、CPythonって高速化のためにほとんどがC言語で実装されているのです。ということは、Python本体のコードを見て学ぼうとするとC言語で学ぶに等しい。Cで学ぶのであればすでにいろいろ参考文献とかいろいろあるのであえてやる必要はないですよね？

他に似たようなことができる言語といえばRustですかね。Rustも次のラッパーライブラリでlibcをラップしており、それまでのところはRustで完結しているようです。まあRustはそれ以前の言語の知識で先に進むのがハードモードになりそうだし、今のところ仕事で触れるチャンスがなくて経験値が貯まってないので僕が同じようなことをするとしてもだいぶ先になりそうではあります。

https://github.com/rust-lang/libc

Rustには、libc自身も自分で実装しようという実験的なプロジェクトがあるようなので、それが完了したら上から下までRustという世界を見ることもできそうです。楽しみですね。

https://blog.sunfishcode.online/rust-programs-entirely-in-rust/

あとはDartあたりも気になっていたりはしますが、僕にとって「実家」と言える言語はPythonだし、Pythonでやってみました。

明日は@r_rudiさんです。

# 参考

* [Goならわかるシステムプログラミング](https://www.lambdanote.com/products/go)
* [Goならわかる相続放棄](https://dev.to/mopemope/go--2m4)