LoginSignup
0
0

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-05-30

前置き

sansyrox/robynというプロジェクトがあります。詳しくは作者の公演を見てもらうとして。

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

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

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

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

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

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

robyn

まずはサンプルコード

from robyn import Robyn

app = Robyn(__file__)

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

app.start(port=5000)

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

Robyinのstartを見ると

    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のスタンダードライブラリーではなく、多分こっちです。ちょい紛らわしい。

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

spawn_processを見てみる

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

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側で定義されてます。

別のところにありました。

#[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)のここですね。

#[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

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

特にこのあたり↓は見ただけではわからないですね。

    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()
        }
    };

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0