TypeScriptで書いたプログラムをPython等で動かす魔法の「jsii」を紹介

まず結論から書くと、下記の図のような事ができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50391/8b7ab467-7c1a-0054-bb94-99ccdc672454.png)

上側に表示しているTypeScriptで書いたプログラムを、下側のPythonプログラムから実行している例です。

これは、[jsii](https://github.com/aws/jsii)というライブラリを使って、**TypeScriptプログラムをPythonで実行可能なライブラリに変換しています**。[公式ドキュメントに`the magic happens`と書いてある](https://github.com/aws/jsii/blob/v0.20.6/README.md#getting-started)とおり、魔法のような技術です。

jsiiはPythonの他にもJavaなどの言語で利用できますが、本稿ではTypeScriptで書いたプログラムをPythonで実行します。

実際にやってみましょう。

## 本稿の実行環境
- TypeScript 3.6.4
- Python 3.7.5
- Node.js 13.1.0

## jsiiを使ってみる
jsiiは大まかに、下記のような流れで利用できます。

1. TypeScriptでプログラムを記述
2. jsiiを使って他言語のパッケージをビルド
3. 他言語からインストールして利用する


### TypeScriptプログラムの記述
サンプルとして、TypeScriptで適当なFizzBuzzのプログラムを`lib/index.ts`に記述します。

```typescript:lib/index.ts
export class MyClass {
  fizzBuzz(arg: number): string {
    if (arg % (3 * 5) === 0) {
      return 'FizzBuzz';
    } else if (arg % 3 === 0) {
      return 'Fizz';
    } else if (arg % 5 === 0) {
      return 'Buzz';
    }
    return arg.toString();
  }
}
```

もしTypeScriptとして普通に実行することで動作確認したい場合は、`$ npx ts-node lib/index.ts`などお好みの方法で実行します。

### package.jsonの記述
jsiiはビルド時に`package.json`の情報を使って、各言語のライブラリ用のメタ情報を登録していきます。そのため、ある程度`package.json`を「ちゃんと」書いていないとjsiiを動作させることはできません。

下記が設定例です。

```json:package.json
{
  "name": "jsii-example",
  "version": "1.0.0",
  "main": "lib/index.js",
  "types": "lib/index.d.ts",
  "author": "your_name",
  "repository": {
    "url": "https://example.com",
    "type": "Git"
  },
  "license": "MIT",
  "jsii": {
    "outdir": "dist",
    "targets": {
      "python": {
        "distName": "your_name.jsii_example",
        "module": "your_name.jsii_example"
      }
    }
  },
  "devDependencies": {
    "jsii": "^0.20.6",
    "jsii-pacmak": "^0.20.6"
  }
}
```
上記のように`repository`や`author`など普段省略しがちな箇所が、jsii利用時には必須になります。また`main`と`types`には、`lib/index.ts`と同じディレクトリで指定する必要があります。

加えてjsii固有の設定として、`jsii`という所に各言語のパッケージ用の情報を記述します。今回はPythonのパッケージをビルドするため、`targets`に`python`としてモジュール名の情報を記述しています。

### jsiiでのビルド
`jsii`のビルド実行に必要なパッケージをインストールします。

```bash
$ npm install --save-dev jsii jsii-pacmak
$ echo "jsii
wheel
publication" > requirements.txt
$ pip install --require requirements.txt
```

上記インストールが完了したら`jsii`コマンドでビルドし、`jsii-pacmak`コマンドでパッケージングします。

```
$ npx jsii
$ npx jsii-pacmak
```

`dist/python`ディレクトリに、`your_name.jsii_example-1.0.0.tar.gz`が作成されていたら成功です。

### Pythonから実行する
先程作成したパッケージファイルを、Pythonライブラリとしてインストールします。

```
$ pip install dist/python/your_name.jsii_example-1.0.0.tar.gz
```
このライブラリをimportして、先程のfizzBuzz関数を呼び出すPythonプログラムを記述します。

```python:python.py
import your_name.jsii_example

obj = your_name.jsii_example.MyClass()

print(obj.fizz_buzz(2))
print(obj.fizz_buzz(3))
print(obj.fizz_buzz(5))
print(obj.fizz_buzz(15))
```
いよいよ実行します。

```bash
$ python python.py 
2
Fizz
Buzz
FizzBuzz
```
TypeScriptで書いたコードを、Pythonから呼び出すことができました！

## Dockerでjsiiを使う
ここまで述べてきた実行環境構築は複数言語のインストールが必要だったりと少々面倒のため、今回`jsii`実行に必要なPythonとNode.jsをまとめて用意できるDockerfileを用意しました。

```dockerfile
FROM python:3.7.5-slim

ENV DIR /app
WORKDIR $DIR
RUN apt update && apt install -y nodejs npm

# setup python
COPY requirements.txt $DIR
RUN pip install --requirement requirements.txt

# setup node
COPY package.json $DIR
RUN npm install

# main: lib/index.ts
COPY lib/ $DIR/lib
```
先述の`package.json`,`requirements.txt`,`lib/index.ts`を用意した後、下記のようにして使います。

```bash
$ mkdir dist
$ docker build --tag jsii-example .
$ docker run --rm --volume $(pwd)/dist:/app/dist jsii-example /bin/bash -c 'npx jsii && npx jsii-pacmak'
```
`dist/`以下にパッケージファイルが生成されたら成功です。

## jsiiの仕組み
jsiiの仕組みについては、jsiiのドキュメント内に解説があります。
[jsii/runtime\-architecture\.md at master · aws/jsii](https://github.com/aws/jsii/blob/master/docs/runtime-architecture.md)

Python等のホストコードがjsii利用箇所に到達した時、nodeの子プロセスを立ち上げてプログラムを実行し、その結果をPythonで受け取っているようです。ホストの`/tmp`ディレクトリを見てみると、プログラムを一時的にnpm installしていると思しき形跡が見つかると思います。

本来Pythonだけで終わる処理がNode.jsとの通信にも依存するようになっているため、**パフォーマンスは犠牲になっている**点がドキュメント内でも述べられています。jsiiは本番環境などパフォーマンスが要求されるアプリケーション内での動作は難しく、開発やビルド用ツール等での利用が想定されています。

### jsiiの利用例
jsiiはAWSが開発しているOSSです。実際の使用例として、AWSの提供するInfrastructure as Codeのツールである[AWS CDK](https://aws.amazon.com/jp/cdk/)内でjsiiが使われています。aws-cdkはjsiiと同じく、TypeScript、Python、.NET、Javaでインフラ構成を記述できるようになっています。

[AWS クラウド開発のよくある質問](https://aws.amazon.com/jp/cdk/faqs/) より引用
>AWS は、AWS Construct ライブラリパッケージのビジネスロジックを TypeScript で構築し、サポートされている各プログラミング言語へのマッピングを提供します。これにより、AWS CDK コンストラクトの動作が異なる言語間で一貫していることを確認でき、すべての言語で利用できる包括的なコンストラクトパッケージのセットを提供できます。AWS CDK プロジェクトで作成したコードはすべてお客様ご希望のプログラミング言語でネイティブになっています。JavaScript ランタイムはお客様のプログラミング経験の実施詳細です。jsii プロジェクトは https://github.com/aws/jsii で参照できます。

## Tips
- jsiiで実行できるTypeScriptには制限がある
    - 分かりやすい例を挙げると、他言語の予約語をTypeScriptで使用しようとすると正常に動作しない場合があります。
    - 他、どういった制限があるかはドキュメントに記載されています。
        - [jsii/typescript\-restrictions\.md at master · aws/jsii](https://github.com/aws/jsii/blob/master/docs/typescript-restrictions.md)
- Python 3.8には未対応
    - 本稿ではPython 3.7.5で実行しています。3.8.0を使うと、本稿執筆中の時点ではエラーが発生してしまいjsiiで作られたライブラリを実行することができません
    - issueも立っています
        - [Python 3\.8\.0: ImportError: cannot import name '\_Union' from 'typing' · Issue \#913 · aws/jsii](https://github.com/aws/jsii/issues/913)
- Ruby対応は開発中
    - ドキュメントにハッキリとは書いていないのですが、Historyやソースコード内を読んでいくとRubyに対応しているかのような記述が所々に見つかります。
    - package.json内の`targets`に`ruby`と指定すると、ちゃんと`jsii`は動作して`dist/ruby/`にパッケージらしきファイルが生成されるのですが、[TypeScriptのソースコードをそのまま含めるプログラム](https://github.com/aws/jsii/blob/v0.20.6/packages/jsii-pacmak/lib/targets/ruby.ts)が動いているだけのようでgemの生成は実装されていません。
    - Ruby対応についてはissueが立っています
        - [Ruby support · Issue \#144 · aws/jsii](https://github.com/aws/jsii/issues/144)

## まとめ
[jsii](https://github.com/aws/jsii)を使って、TypeScriptで書いたプログラムをPythonのソースコード内で実行することができました。まだ開発中の部分もあり制約は多いものの、実際にAWS CDKで使われているため一定の将来性もあります。もし「1つのコードベースで作ったツールを、複数の言語から利用したい」という場合には、このjsiiが選択肢に挙がるのではないかと思います。

今回利用したプログラムはGitHubにも上げています。もし興味を持った方はご覧ください。
https://github.com/s2terminal/jsii-example