🦀

Next.jsのServerComponentからRustのasync fnを使用する

2023/09/29に公開

ウェブアプリケーションを実装する際、個人的にも組織としても backend をRustで、frontend をReact/TypeScriptで実装しその間はGraphQLまたはgrpcとなることが多いのだけれども、Next.jsServerComponentからnapi-rsを使用してRustの関数を直接呼んでやるとかなり楽できるのではないかと思いそういうことができそうか試してみた。

成果物

とりあえずnpm run devで動作は確認できているがbuildする場合はprebuiltされたbinaryへのパスは調整する必要があるかもしれない。

https://github.com/bokuweb/rsc-napi-sandbox

セットアップ

まずはすべてデフォルト設定で OK なのでRSCだけ有効にしてNext.jsのプロジェクトを立ち上げる。その後以下に従いnapi-rsのセットアップを行う。

Getting started – NAPI-RS

napiの設定はpackage.jsonに書くようで、今回はひとまず手元の M1 mackbook で動作させることをゴールとしているのでtriplesaarch64-apple-darwinを指定している。

package.json
{
  "napi": {
    "name": "napi",
    "triples": {
      "defaults": false,
      "additional": [
        "aarch64-apple-darwin"
      ]
    }
  }
}

Rust 側の関数

今回はサンプルとしてsrc/lib.rsに以下のようなasync fnを用意した。

src/lib.rs
#[macro_use]
extern crate napi_derive;

#[napi]
pub async fn async_sum(a: i32, b: i32) -> i32 {
  tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await;
  a + b
}

index.js の修正

この状態でnpm run buildするとプロジェクトのルートにindex.jsnapi.darwin-arm64.nodeが吐かれる。が、そのままでは動かないので修正していく。詳細は省略するが以下のようにswitchplatformarchを判別するコードが吐かれる。

index.js
switch (platform) {
  case 'android':
    switch (arch) {
      case 'arm64':
        ...
	require("xxxxx.node")
        ...
        break
      case 'arm':
        ...
        require("xxxxx.node")
	...
        break
      default:
        throw new Error(`Unsupported architecture on Android ${arch}`)
    }
    break
  case 'win32':
    ...

今回はaarch64-apple-darwinだけ考慮するのと、requireを使用してしまうとwebpack側のrequireが使用されてしまうので以下のように__non_webpack_require__を利用してbinaryを読み込むようindex.jsを修正する。

また、このindex.js.next/server/appに配置されることを前提として*.nodeのパスを指定する必要がありそう。今回はnpm run devで動けばよし。としているのでbuildする場合はこの辺のパスに注意すること。

index.js
const node = __non_webpack_require__("../../../napi.darwin-arm64.node"); // use relative path from .next/server/app

export const { asyncSum } = node;

index.js の利用

上記で変更したindex.jssrc/app/page.tsxから以下のように使用する。

src/app/page.tsx
import { asyncSum } from "../../";

export default async function Home() {
  const result = await asyncSum(1, 2);
  return <main>Result: {result}</main>;
}

以下のように表示されたら成功だ。

Result: 3

まとめ

たとえばデスクトップPCだけを想定したアプリや管理画面などはこの方法で実装すると楽ではないかと考え試してみた。実際にproductionで動かすのであればまだ考慮することはありそうだが、ひとまずasync関数をNodeから使用できることが確認できた。今後の選択肢に加えてもう少し検証してみたい。

以上。

FRAIMテックブログ

Discussion