sansyrox/robynのソースコードをヌルく読む

# 前置き
[sansyrox/robyn](https://github.com/sansyrox/robyn)というプロジェクトがあります。詳しくは[作者の公演](https://www.youtube.com/watch?v=DK9teAs72Do)を見てもらうとして。

表はPythonでサーバーを書いて、裏では実はRustのランタイムが走る。というシロモノらしいです。

面白そうなのでヌルめにソースコードを読みます。構造がだいたいわかればいいかな、という感じですすめます。

robynのやってることを大雑把に書くと、PythonからRustで作ったサーバーやルーターを呼んでます。サーバーはActixのHTTTPサーバーみたいです(ルーターも同じ?)。Python => Rustは[PyO3/pyo3](https://github.com/pyo3/pyo3)を使ってるみたいです。

んじゃあ、Actixで書きゃよくね?というツッコミはナシで。PythonからFlask的にサーバー書けて、それが爆速で動いたら嬉しいですよね。

なお文中のコードは雰囲気を説明するために適当に抜きがきしたものです。

(pyo3は昔々「これはまだプロダクションには向きません」って書いてあった気がします。そうとうなところまで出来上がったんですね。すごいですね・・・・)

# robyn

## まずはサンプルコード

```python
from robyn import Robyn

app = Robyn(__file__)

@app.get("/")
async def h(requests):
    return "Hello, world!"

app.start(port=5000)
```

サンプルコードがもろFlaskですね。Pythonやる人なら、シンタックスを学習するまでもない感じ・・・

## [Robyinのstart](https://github.com/sansyrox/robyn/blob/main/robyn/__init__.py#L101)を見ると

```python
    def start(self, url="127.0.0.1", port=5000):
        if not self.dev:
           # -----(略)-----
            for _ in range(self.processes):
                copied_socket = socket.try_clone()
                p = Process(
                    target=spawn_process,
                    args=(
                    # -----(略)-----
                    ),
                )
                p.start()
                processes.append(p)
           # -----(略)-----
            try:
                for process in processes:
                    process.join()
           # -----(略)-----

```

マルチプロセスを実行はPythonのスタンダードライブラリーではなく、[多分こっち](https://pypi.org/project/multiprocess/)です。ちょい紛らわしい。

ここは、プロセスを実行してるだけ、です。targetに渡ってるspawn_processがキモっぽいです。

## [spawn_process](https://github.com/sansyrox/robyn/blob/d4d5f562ff7c3f12d17f92f00f4ae169a5b05979/robyn/processpool.py#L13)を見てみる

Serverをイニシャライズして、ルートやらを追加して、スタート・・・こういうのはFlaskのソースで見た気がします。

```python
from .robyn import Server

# ---[略]---

def spawn_process(
    directories, headers, routes, middlewares, web_sockets, event_handlers, socket, workers
):

    # ---[略]---

    server = Server()

    for directory in directories:
        route, directory_path, index_file, show_files_listing = directory
        server.add_directory(route, directory_path, index_file, show_files_listing)

    # ---[略]---

    try:
        server.start(socket, workers)
        loop = asyncio.get_event_loop()
        loop.run_forever()
    except KeyboardInterrupt:
        loop.close()
```

ただ、「Server」がどこに定義されてるのかわからなくてウロウロしました。

pyiファイルにServerの定義があるんですがまさかこれじゃないよな・・・そんなわきゃないよな・・・と。


## Serverはどこにあるのか
Rust側で定義されてます。

[別のところ](https://github.com/sansyrox/robyn/blob/d4d5f562ff7c3f12d17f92f00f4ae169a5b05979/src/server.rs#L40)にありました。

```rust
#[pyclass]
pub struct Server {
    router: Arc<Router>,
    websocket_router: Arc<WebSocketRouter>,
    middleware_router: Arc<MiddlewareRouter>,
    headers: Arc<DashMap<String, String>>,
    directories: Arc<RwLock<Vec<Directory>>>,
    startup_handler: Option<Arc<PyFunction>>,
    shutdown_handler: Option<Arc<PyFunction>>,
}
```

```#[pyclass]```であとはいい感じにやってくれるようです。

黒魔術や・・・・

何をやってるかは、pyo3(正確にはpyo3のpyo3-macros)の[ここ](https://github.com/PyO3/pyo3/blob/f84c740e414f712348629f8845f52a254c7d55e5/pyo3-macros/src/lib.rs#L97)ですね。

```rust
#[proc_macro_attribute]
pub fn pyclass(attr: TokenStream, input: TokenStream) -> TokenStream {
    use syn::Item;
    let item = parse_macro_input!(input as Item);
    match item {
        Item::Struct(struct_) => pyclass_impl(attr, struct_, methods_type()),
        Item::Enum(enum_) => pyclass_enum_impl(attr, enum_, methods_type()),
        unsupported => {
            syn::Error::new_spanned(unsupported, "#[pyclass] only supports structs and enums.")
                .into_compile_error()
                .into()
        }
    }
}
```

Rustのマクロは大雑把に言うと、コードをトークン化してそれを書き直せます。ここでは必要なものを他のところにわたしてるだけのようで・・・

どうもたどっていくと```build_py_class```ってところでいろいろやってる?

見てみましょう。

# pyo3の[build_py_class](https://github.com/PyO3/pyo3/blob/f84c740e414f712348629f8845f52a254c7d55e5/pyo3-macros-backend/src/pyclass.rs#L179)

マクロ、特にproc macroは[syn](https://docs.rs/syn/latest/syn/)で、トークンを組み直していきます。これが結構独特なやり方で以前苦労した記憶があります。ここなんかはもうそれ。synをぐりぐり使っています。

[特にこのあたり↓](https://github.com/PyO3/pyo3/blob/f84c740e414f712348629f8845f52a254c7d55e5/pyo3-macros-backend/src/pyclass.rs#L199)は見ただけではわからないですね。

```rust
    let field_options = match &mut class.fields {
        syn::Fields::Named(fields) => fields
            .named
            .iter_mut()
            .map(|field| {
                FieldPyO3Options::take_pyo3_options(&mut field.attrs)
                    .map(move |options| (&*field, options))
            })
            .collect::<Result<_>>()?,
        syn::Fields::Unnamed(fields) => fields
            .unnamed
            .iter_mut()
            .map(|field| {
                FieldPyO3Options::take_pyo3_options(&mut field.attrs)
                    .map(move |options| (&*field, options))
            })
            .collect::<Result<_>>()?,
        syn::Fields::Unit => {
            // No fields for unit struct
            Vec::new()
        }
    };
```

ここからがいいとこなのかもしれませんが、とりあえずだいたいの感じがわかったところで終わり・・・