前置き
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()
}
};
ここからがいいとこなのかもしれませんが、とりあえずだいたいの感じがわかったところで終わり・・・