🔋

Webでユーザーの書いたカスタムコードを実行したい?AiScriptなんてどうよ

2024/09/19に公開

AiScript、知ってる?

おそらくこの記事をMisskey以外から見つけた方は「なんだよそれ」って感じだと思います。AiScriptは、Misskeyの開発者であるしゅいろ氏などを中心に開発されている、Javascript上で動作するスクリプト言語です。

https://github.com/aiscript-dev/aiscript

この言語は、現在Misskey上の以下のような機能の実装に使用されています:

  • プラグイン機能(Misskeyの機能を拡張するプラグインをユーザーが書ける)
  • Play機能(ユーザーがAiScriptを使ってミニゲームを作ることができる)

ただ、Misskeyの開発メンバーとしてこの言語と触れる中で、 「もっといろんな場所で使えるんじゃないの…?」 と思うようになってきました。そこで、今回はAiScriptを知らない皆さんにAiScriptを布教していきます。

言語の特徴

公式の説明によると、

  • 配列、オブジェクト、関数等をファーストクラスでサポート
  • JavaScript風構文で書きやすい
  • セキュアなサンドボックス環境で実行される
  • 無限ループ等でもホストをフリーズさせない
  • ホストから変数や関数を簡単に提供可能

とあります。実際に書いてみるとこんな感じになります:

fizzbuzz.ais
@fizzbuzz() {
    for (let i, 100) {
      <: if (i % 15 == 0) "FizzBuzz"
        elif (i % 3 == 0) "Fizz"
        elif (i % 5 == 0) "Buzz"
        else i
    }
}

fizzbuzz()

標準でいろんな関数も備わっていたりします。

date.ais
@today() {
    <: `今日は{Date:year()}年{Date:month()}月{Date:day()}日です`
}

today() // <- 今日は2024年9月19日です

サンドボックス環境

先ほどの説明にもあった通り、この言語で書かれた内容はサンドボックス環境で動作します。ユーザーはJavascriptのAPIに直接アクセスすることはできず、AiScriptの標準関数と後述するホスト側での拡張関数でJavascriptに間接的に干渉することしかできないようになっています。

拡張性が高い

個人的には、どちらかというと強調したいのはこっちです。 この言語は、それを実行する側から変数や関数を提供できるようになっています。これをうまく活用すれば、ユーザースクリプトの処理結果を既存のコードに反映させたり、既存のコンポーネントを組み立ててユーザー側でUIを構築したりなんてことができてしまうのです。

AiScriptにはパーサとインタプリタがあります。平文テキストで用意されたAiScriptをパーサに入れてASTと呼ばれる形式にし、それをインタプリタに入れて実際に実行します。

インタプリタの初期化時にはグローバルに使用できる変数・関数を拡張することができます。

AiScriptを呼び出す側のJavascriptファイル
import { account } from '@/account.js';
import { uiAlert } from '@/modals.js';
import { Interpreter, Parser, utils, values } from '@syuilo/aiscript';

const parser = new Parser();

const interpreter = new Interpreter({
    // グローバルな変数を注入
    USER_NAME: account ? values.STR(account.name) : values.NULL,

    // グローバルな関数を注入
    'Ns:alert': values.FN_NATIVE(async ([text, type]) => {
        // たとえば:既存のUIのダイアログを呼び出す
        // (ここでは普通のJavascriptを使用できる!!)
        await uiAlert({
            type: type ? type.value : 'info',
            text: text.value,
        });
        return values.NULL;
    }),
}, {
    err: (err) => {
        // AiScriptランタイム内でのエラーはこちらでハンドリング
    }
});

const aiscriptCode = `
@greeting() {
    // インタプリタで拡張した関数や変数が使える!
    Ns:alert({
        text: \`こんにちは、{USER_NAME}さん!\`
    })
}

greeting()
`;

async function run() {
    // 1. AiScriptをASTに変換(※要エラーハンドリング)
    let ast;
    try {
        ast = parser.parse(aiscriptCode);
    } catch (err) {
        // 構文エラー
    }

    // 2. AiScript ASTを実行
    try {
        await interpreter.exec(ast);
    } catch(err) {
        // 内部エラー
        // (ランタイムエラーはインタプリタのコールバックでハンドリングするのでここには来ない)
    }
}

このように、Javascriptの必要な機能へのアクセスを担保しつつも、アプリのほかの部分に影響を与えることなく安全にユーザーコードを走らせることができます。

どういう場面に使えそう?

A: ユーザーが自身で機能拡張を行えるようにする場面全般 で使える気がします。プラグイン・カスタムウィジェット・カスタムロジックなどなど…ユーザーによる拡張性をグンと上げたい場面でぜひ使ってみてくださいね!

【おまけ】VSCode拡張もあるよ

AiScriptはJavascriptの記法と似ているのでZennではJavascriptによるハイライトを使用しましたが、VSCode拡張もあります!Visual Studio Marketplaceにはサードパーティーのものがいくつかありますが公式版はまだないので、よければGitHubのリポジトリからダウンロードしてください ↓

https://github.com/aiscript-dev/aiscript-vscode

Discussion