はじめに
こんにちは、Futureでアルバイトをしている川渕です。
アルバイトの前はFutureのインターンシップでRust製SQLフォーマッタであるuroborosql-fmtの作成を行っていました(その時の記事はこちら)。
本記事ではそのフォーマッタをVSCodeの拡張機能化した方法について説明します。
説明すること
- napi-rsを使用してTypeScript(JavaScript)からRustのコードを呼び出せるようにする方法
- napi-rsにおけるクロスプラットフォームビルド方法
- VSCode拡張機能をパッケージ化する方法
説明しないこと
- 本記事ではフォーマッタの仕様、実装方法について説明しません。詳細を知りたい方は以下の記事をご覧ください。
- LSPを用いた拡張機能作成方法の詳細についても本記事では説明しません。詳細を知りたい方は以下の記事をご覧ください。
- napi-rsで作成したNode.jsアドオンの公開方法
- 作成したVSCode拡張機能の公開方法
環境
- OS: Windows 10 Pro
- VSCode: 1.73.1
- Node.js: v16.17.1
- rustc: 1.64.0 (a55dd71d5 2022-09-19)
- npm: 8.15.0
- yarn: 1.22.19
- napi-rs/cli: 2.12.0
- vsce: 2.14.0
作成するVSCode拡張機能の仕様
作成するVSCode拡張機能の仕様は以下の通りです。
- Language Server Protocolを利用する
- コマンドパレットで実行できる
- 範囲選択されている場合、その範囲のSQLをフォーマットする
- 範囲選択されていない場合、全体をフォーマットする
処理の流れ
作成する拡張機能の処理の流れを説明します。
処理の流れは以下のとおりです。
- まずユーザがフォーマットしたいSQLを範囲選択し、コマンドを実行します。
- コマンド実行をLanguage Serverのクライアントが検知し、サーバに選択範囲の情報を送信します。
- サーバは選択範囲のSQLを取得します。取得したSQLを引数に与えてSQLフォーマッタを実行します。
- SQLフォーマッタは引数として受け取ったSQLをフォーマットし、フォーマット済みSQLを返します。
- フォーマット済みSQLを受け取ったサーバは選択範囲をフォーマット済みSQLに置き換えるようにクライアントに送信します。
SQLフォーマッタはRust、自作Language ServerはTypeScriptで書かれているため、直接SQLフォーマッタを呼び出すことができません。
そこで、napi-rsというツールを使用して、TypeScriptからRustで書かれたSQLフォーマッタを呼び出せるようにしました。
TypeScriptからRustの呼び出し
まずTypeScriptからRustを呼び出す方法として以下の3つの方法が考えられます。
方法 | 使用するツール | メリット | デメリット |
---|---|---|---|
Rustコードのwasm化 | rustc または wasm-pack | プラットフォームに依存しないため移植性が高い | C/C++を呼び出しているコードをビルドするのが難しい |
RustコードのNode.jsアドオン化 | napi-rs | C/C++を呼び出しているコードでも比較的簡単にビルドできる | クロスプラットフォームビルドが必要 |
Rustコードをビルドしたものをexecで呼び出す | rustc | 特別なツールを使わなくても可能 | クロスプラットフォームビルドが必要 綺麗な方法とは言えないため最後の手段 |
wasmとNode.jsアドオンの性能差は現時点では調査しましたがわかりませんでした。(もしわかる方がいれば教えてください)
しかし、移植性の観点からできる限りwasmのほうがNode.jsアドオンよりも良いという意見が多く見受けられました。
- Performance: Rust and its relationship with Node.js
- How do NAPI works compare to WASM, and what are the use cases suiting each one? : rust
しかし、SQLフォーマッタは内部的にCで書かれたコードを呼び出していることが要因でwasm化がうまくいかなかったため、今回はnapi-rsを用いてNode.jsアドオン化する方法を選択しました。
Node-API
napi-rsについて紹介する前にNode-APIについて説明します。
Node-APIとはNode 8.0.0で導入されたツールで、C/C++コードをNode.jsのアドオン化するツールです。
Node-APIを使用することで、C/C++コードをJavaScriptで記述されたものと同様の方法で利用できるようになります。
napi-rsとは
napi-rsとはNode-APIをRustで使用できるようにしたものです。
例えば以下のようなRustコードをnapi-rsでビルドします。
|
すると、Node.jsアドオンが生成され、JavaScriptからRustの関数を呼び出せるようになります。
const { sum } = require("./index.js"); |
ちなみに、Node-APIは元々の名称がN-APIだったのですが、しばしば「NAPI」と発音され、蔑称と間違われる可能性があるとの懸念から現在のNode-APIに名称を変更しました。そのため、napi-rsにおいても、"エヌエーピーアイ"と発音したほうが良さそうです。
N-APIが「Node-API」へ名称変更、既存のコンパイル済みアドオンへの影響はナシ|CodeZine(コードジン)napi-rsの使い方
napi-rsの使い方を説明します。
napi-rsではx86_64-pc-windows-gnuの環境はサポートされていないため、もしwindowsでgnu版rustを使っている方はmsvc版のRustを入れてください。
1. CLIツールのインストール
yarnでnapi-rsのCLIツールをインストールします。
まずyarnをインストールします。以降もyarnが必要になるため、必ずインストールしてください。
npm install -g yarn |
napi-rsのCLIツールをインストールします。
yarn global add @napi-rs/cli |
インストールに成功するとnapi
コマンドが使えるようになります。
2. 新規プロジェクト作成
インストールしたCLIツールを使用して新規プロジェクトを作成します。
新規プロジェクトを作成したいディレクトリで以下のコマンドを実行します。
napi new |
すると、以下の質問が表示されるので、順に回答してください。
任意のパッケージ名 |
質問に回答すると指定したディレクトリ名のディレクトリが作成されます。
これでNode.js add-onを作るテンプレートが完成しました。
3. ビルドと実行
テンプレートのsrc/lib.rs
に既にサンプルのRustコードが含まれています。関数sumは2つの引数の合計を返す関数です。
|
これをNode.jsアドオンへビルドして実行してみます。
まず先ほど作成したプロジェクトのルートディレクトリでビルドコマンドを実行します。
yarn build |
ビルドに成功すると、プロジェクトのディレクトリ直下にindex.d.ts
、index.js
、<プロジェクト名>.<環境>.node
が作成されます。
index.js
には環境に合ったnodeファイルを読み込んでくれる処理が書いています。そのため、index.js
をimportすることで自動的に環境に合ったnodeファイルが読み込まれ、そこに含まれる関数を利用できるようになります。
以下のファイルをプロジェクトのディレクトリ直下に作成します。
const { sum } = require("./index.js"); |
実行して”7”という出力が返ってきたら成功です。
node test.js |
SQLフォーマッタをJavaScriptから実行
プロジェクトのテンプレートを変更してSQLフォーマッタをJavaScriptから実行できるようにしてみます。
1. 新規プロジェクト作成
先述した方法で新規プロジェクトを作成しました。プロジェクト名はuroborosql-fmt-napiとしています。
2. src/lib.rs
を変更し、ビルド
src/lib.rs
を以下のように変更します。
SQLフォーマッタのクレート名はuroborosql_fmt
で、format_sql()
関数にSQL文を渡すとフォーマットされたSQLが返ってきます。
|
プロジェクトのルートディレクトリでビルドします。
yarn build |
私の環境はwin32-x64-msvcであるため、index.d.ts
、index.ts
、uroborosql-fmt-napi.win32-x64-msvc.node
が生成されました。
3. run.jsの作成、実行
プロジェクトのディレクトリ直下にrun.jsを作成します。変数targetにはフォーマットしたいSQL文を格納しています。
const { runfmt } = require("./index.js"); |
作成したrun.js
を実行します。
node run.js |
出力結果は以下のようになりました。きちんとフォーマットされているため成功です。
SELECT |
クロスプラットフォームビルド
現在はビルドした環境(win32-x64-msvc)でしか作成したNode.jsアドオンが動作しません。
そこでGitHub Actionsを使ってクロスプラットフォームビルドを行います。
0. CI.ymlの作成
もしnapi-rsプロジェクト作成時にGitHub Actionsを有効にしていなかった場合はこちらの作業を行ってください。
- 適当なディレクトリで
napi new
- パッケージ名、ディレクトリ名は適当に入力
- サポートしたい実行環境を選択
(今回作成しているフォーマッタではできるだけ多くの環境をサポートしたかったため、で全ての実行環境を選択) - GitHub Actionsを有効にしてプロジェクトを作成
- 完成したプロジェクト内の
.github
ディレクトリをコピーして現在作業中のプロジェクトにペースト
1. yarn.lockの作成
プロジェクトのルートディレクトリで以下のコマンドを実行します。
yarn install |
yarn.lockが作成、または更新されれば成功です。
2. CI.ymlの編集、GitHub Actionsの実行
デフォルトではGitHubにpushするとGitHub Actionsが自動的に動いて以下の処理を行ってくれます。
- 各環境に対応したNode.jsアドオンをビルド
- npmパッケージのpublish
今回はnpmパッケージのpublishは行わないため、.github/workflows/CI.yml
のpublish以下をすべてコメントアウトします。
publish方法を知りたい方は以下の記事が参考になると思います。
GitHub Actionsでビルドを行うと、13個の環境のうち11個の環境でビルドが失敗してしまいました。Rust製SQLフォーマッタが内部的にC/C++のコードを呼び出していることが原因の1つであると考えられます。そのため、通常のRustプロジェクトであればもう少し成功すると思います。
試行錯誤して.github/workflows/CI.yml
を編集すると、最終的に13個中7個の環境でビルドが成功するようになりました。私が実施した変更を参考程度に示します。
CI.yml
の変更1: 長いパスに対応
hostがWindows-latestである環境のbuildに以下の処理を追加しました。
git config --system core.longpaths true |
CI.yml
の変更2: yarn testの削除
targetが i686-pc-windows-msvc
の場合のみビルド時にyarn test
が走っています。本来は消すべきではないかもしれませんが、今回はテストコードを書いていないのでとりあえず削除しました。
CI.yml
の変更3: aarch64-apple-darwinにおける一部処理の削除
targetがaarch64-apple-darwinの場合のビルド処理の上5行を削除しました。最終的にビルド処理は以下のようになりました。
yarn build --target aarch64-apple-darwin |
3. 成果物のダウンロード
GitHub Actionsでビルドした各環境のNode.jsアドオンをダウンロードします。
GitHubのリポジトリ > Actions > 最新のワークフローに移動し、ページ最下部のArtifactsのファイルをすべてダウンロードします。
各ファイルを解凍すると、各環境に合ったNode.jsアドオンが取得できます。
nodeファイルをまとめて圧縮
適当なディレクトリを作成
対応したい環境のnodeファイルを全て置く
napi-rsプロジェクトの
index.d.ts
とindex.js
をコピーしてそのディレクトリにペーストpackage.jsonを作成 (nameはパッケージ名)
フォーマッタの名前がuroborosql-fmtであるため、パッケージ名はuroborosql-fmt-napiとしました。package.json {
"name": "uroborosql-fmt-napi",
"version": "0.0.0",
"main": "index.js",
"types": "index.d.ts",
"license": "MIT",
"engines": {
"node": ">= 10"
}
}以下のコマンドを実行して圧縮
npm pack
プロジェクト名-バージョン.tgz
ファイルが生成されれば成功
今回の例ではuroborosql-fmt-napi-0.0.0.tgz
というファイルが生成されました。
拡張機能の作成
※再掲
TypeScriptからSQLフォーマッタを呼び出すことができるようになったので、次に拡張機能部分を作成します。
本記事ではmicrosoft/vscode-extension-samples/lsp-sampleをベースにして拡張機能を作成します。
LSPを用いた拡張機能作成方法の詳細を知りたい方は以下をご覧ください。本記事では簡単に解説します。
- Language Server Protocolを用いたVSCode拡張機能開発 (前編) | フューチャー技術ブログ
- Language Server Protocolを用いたVSCode拡張機能開発 (後編) | フューチャー技術ブログ
拡張機能の設定
package.json
を変更して拡張機能の設定を変更します。
まず、VSCodeが起動されると拡張機能が有効になるようにします。
"activationEvents": [ |
コマンドパレットから「format sql」コマンドを実行できるように設定します。
"contributes": { |
クライアント
client/src/extension.ts
にクライアント側の処理を記述します。
clientOptions
内のdocumentSelector
を以下のように変更し、全ての形式のファイル、保存されていないUntitledなファイルを拡張機能の対象とします。
documentSelector: [ |
uroborosql-fmt.uroborosql-format
コマンドが実行されたらuroborosql-fmt.executeFormat
の実行情報とドキュメントのuri、version、選択範囲をサーバに送信する処理を記述します。
context.subscriptions.push( |
サーバ
まず先程npm pack
で取得したuroborosql-fmt-napi-0.0.0.tgz
をserverディレクトリ内に置きます。
そして、server/package.json
のdependenciesを以下のように変更します。
"dependencies": { |
これでRust製SQLフォーマッタをimportできるようになりました。
server/src/server.ts
にサーバの処理を記述します。
まずフォーマットを実行する関数をimportします。
import { runfmt } from "uroborosql-fmt-napi"; |
コマンド実行時に選択範囲のテキストをフォーマットする処理を記述します。
// コマンド実行時に行う処理 |
動作確認
クライアントとサーバをコンパイルして実行してみます。
ちゃんとフォーマットされることが確認できました🎉
拡張機能のパッケージ化
vsceというツールを使用してパッケージ化を行います。vsceとはVSCode拡張機能のパッケージ化、公開、管理を行うことができるCLIツールです。
本記事では拡張機能の公開については説明しません。
vsceのインストール
私の環境(Windows10)ではインストールに手順が必要だったので順に説明します。
1. Python3のインストール
Python3が必要なためインストールします。既にPython3が入っている方は次のステップに進んでください。
まずこちらからインストーラをダウンロードします。
ダウンロードしたファイルを開き、一番下の「Add Python 3.x to PATH」にチェックを入れてください。
「Install Now」をクリックしてインストールし、「Setup was Succesful」と表示されればインストール完了です。
2. node-gypのインストールと設定
node-gypとは、Node.js のネイティブアドオンモジュールをコンパイルするためのツールです。既に入っていて設定済みの方は次のステップに進んでください。
node-gypのインストール
まずnode-gypをインストールします。
npm install -g node-gyp |
3. VisualStudioのビルドツールのインストール
次にこちらからVisualStudioのビルドツールのインストーラをダウンロードします。
インストーラを起動して「C++によるデスクトップ開発」を選択して、右側の「インストールの詳細」の中の「Windows 10 SDK」にチェックを入れて右下のインストールをクリックします。(Windows11の方は「Windows 11 SDK」にチェックを入れてください。)
4. npmの設定
以下を実行します。(2022の部分はダウンロードしたバージョンに合わせて適宜変更して下さい)
npm config set msvs_version 2022 |
5. vsceのインストール
以下を実行します。
npm install -g vsce |
vsceコマンドが実行できるようになれば成功です。
パッケージ化
先程作成した拡張機能のディレクトリで以下のコマンドを実行します。
vsce package |
すると、プロジェクト名-バージョン.vsix
というファイルが生成されます。今回の例ではuroborosql-fmt-1.0.0.vsix
というファイルが生成されました。
そして、以下のコマンドでインストールします。
code --install-extension .\uroborosql-fmt-1.0.0.vsix |
無事インストールされ、フォーマッタが動くようになったので成功です 🎉
まとめ
本記事ではRust製SQLフォーマッタをVSCode拡張機能化した方法を紹介しました。