フェルマータ

個人用のメモ。ソフトウェアの導入とかが多くなる予定。

CMake の C++ プロジェクトを nodejs, TypeScript の世界に持っていく

ポエム

プライベートの開発環境は Qt with C++ を cmake でプロジェクト管理というスタイルだったのだが、いかんせんウェブエンジニアである本職とあまりに乖離していたので、もう少し本職に寄せておきたかった。最近は WebAssembly なるものもあり聞けば C++ のコードを WebAssembly にコンパイルして JavaScript から扱うことができるという。なので自作の C++ ライブラリを JavaScript で利用できる形にして、さらに nodejs プロジェクトで TypeScript から呼び出せるようにすれば、 GUI は web の世界に持ってこられるだろうと思い、今回は C++ を WebAssembly にして nodejs, TypeScript の世界に持ってくることにした。

やったこと

いままで Windows 環境で Windows なりの環境構築が厳しくなってきたので、 WSL2 をインストールして WSL から Ubuntu 使って開発することにしたので特筆なければ下記でやった内容は WSL, Ubuntu でやったことと思ってください。 10 年前後昔の環境でやってたらさすがになあ。

emscripten

cmake プロジェクトを WebAssembly (以下 wasm)にコンパイルするのに必要。細かいコマンドまで見ると面倒だが試しに使う分にはインストール後 cmake のオプションからパスを指定するくらいなのであまり意識しなくて良かった。

emscripten.org

インストールに関しては難しいことはない。公式のコマンドコピペでおわり。

CLion

qiita.com

こちらの方のセットアップをそのまま利用した。 CLion だけはまだ WSL での開発になっていないのでここは Windows で実施した。 emscriptenJavaScript にしたい C/C++ の機能に目印をつける役割のコードは C/C++ で書くので設定が必要。

C++

emscripten.org

emscripten の機能である embind を利用して JavaScript にどの機能を使えるようにしていくか書いていく。例えば時系列のデータを格納する Contour クラスだと下記のような感じ。

// Copyright 2022 Hal@shurabaP.  All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
#include "data/Contour.hpp"
#include <emscripten/bind.h>

using namespace emscripten;

EMSCRIPTEN_BINDINGS(ContourBindings) {
    class_<uzume::vocoder::Contour>("Contour")
        .smart_ptr<std::shared_ptr<uzume::vocoder::Contour>>("shared_ptr<uzume::vocoder::Contour>")
        .constructor(&std::make_shared<uzume::vocoder::Contour, double, double>)
        .function("at", &uzume::vocoder::Contour::at)
        .function("msLength", &uzume::vocoder::Contour::msLength)
        .property("length", &uzume::vocoder::Contour::length);
}

これは割と公式ドキュメント通りなので困らない。インタフェース的にメソッドがいくつか足りない気がするが本筋でないので今回は対処していない。

CMakeLists.txt

cmake_minimum_required(VERSION 3.1)

project(uzumejs)

include(ExternalProject)

file(GLOB_RECURSE VOCODER_SOURCES ./lib/uzume_vocoder/src/*.cpp)
file(GLOB_RECURSE VOCODER_HEADERS ./lib/uzume_vocoder/src/*.hpp)

include_directories(uzumejs PRIVATE ./lib/uzume_vocoder/src/)

add_executable(uzumejs
        ... # 割愛
        )

if (EMSCRIPTEN)
    set(CMAKE_EXECUTABLE_SUFFIX ".js")
    set(lflags "--pre-js ${CMAKE_SOURCE_DIR}/src/resources/pre.js --post-js ${CMAKE_SOURCE_DIR}/src/resources/post.js -s WASM=1 -s MODULARIZE -s 'EXPORT_NAME=\"factory\"' --no-entry --bind")
    set_target_properties(uzumejs PROPERTIES LINK_FLAGS ${lflags})
endif()

ここは結構厄介だった。ポイントを追って書いていく。

  • set(CMAKE_EXECUTABLE_SUFFIX ".js")

emscripten を通したコンパイルは実行ファイルを作ってくれるわけだが、ネイティブと違って JavaScript の世界では実行ファイルはこれ、という明確なのは指定しづらい。のでコンパイル結果の拡張子でどのファイルを出力させるのかを emscripten にお伝えする必要がある。

suffix 出力内容
html HTML + JavaScript + wasm
js JavaScript + wasm

さらに行くと wasm だけ出力するオプションもあるのだが今回は割愛する。 .html が指定されたときには出力された HTML が そのまま C++ の main を実行するコンソールやスクリーンを提供してくれて、 HTML, JavaScript, wasm をホスティングすれば C/C++ のコードがそのままブラウザ上で実行できるよう出力される。私にとっては高機能だがとりあえず C/C++ のコードをどーんと web に持っていきたいならこれで良さそう。 .js を選ぶと、 HTML を除いたものが作成される。私は nodejs で利用したいので今回は .js にしている。.js ファイルは html 上で動作することを前提にしているように見えるので、 nodejs に持っていくには高機能に見えるが面倒ないので一旦このまま使うことにした。(@types/emscripten を利用すると全機能使えそうだが今回は面倒なので見送り)

  • --pre-js ${CMAKE_SOURCE_DIR}/src/resources/pre.js --post-js ${CMAKE_SOURCE_DIR}/src/resources/post.js

前述の .js ファイルの前後になにか書きたいときにはこんな感じでファイルを直接指定できる。今回は空のファイルを置いた。

  • -s MODULARIZE -s 'EXPORT_NAME=\"factory\"'

ここが結構なハマりポインツで、どうも HTML とセットの利用用途がメインなのか .js ファイルはもともと他のファイルから利用できない形になっている。そのため、このオプションを指定して .js 内部の Module を作成してくれるファクトリメソッドを外部に公開する必要がある。今回の指定だと export function factory();.js ファイルに埋め込まれる。これに気づくまで時間がかかった。ここで factory を指定しているのは意味があって tsembind が生成する型定義ファイルは暗黙にここが factory 指定されている前提に合わせている。

tsembind

github.com

npm i -g tsembind

インストールしたら cmake プロジェクトから出てきた .js, .wasm ファイルのあるディレクトリで下記のようにすると標準出力に TypeScript の型定義が出力される。

tsembind YOUR_JS_NAME.js

これでよしなな名前のファイルに書き込めば、私のプロジェクトだと uzumejs.d.ts, uzumejs.js, uzumejs.d.ts の TypeScript から利用するのに必要な 3 ファイルが手に入る。

nodejs

上述で作られたファイルなどをあわせて下記のような形のツリー構造にする。

projectRootDir/
 + pkg
 | + uzumejs.d.ts
 | + uzumejs.js
 | + uzumejs.wasm
 + src
   + index.ts

TypeScript のプロジェクトの構築は下記の感じ。とりあえず動かす用途なので細かい設定はしていない。真面目にパッケージ化するとかやるなら webpack の設定が必要そうだなーと思っているが初心者なので動くのを優先した。

qiita.com

index.ts を下記のようにした。

import uzumejs from "../pkg/uzumejs"

uzumejs().then(uzume => {
    const contour = new uzume.Contour(1000.0, 2.0);
    console.log(contour.msLength());
})
shuraba_p@MyComputer:~/projects/uzumejs$ npx ts-node src/index.ts
1002

というわけで C++ で書かれた機能が TypeScript のコードから動いた。やったね。

終わりに

nodejs 初心者なので本当に手間取った。あと私の開発環境古すぎ???問題があってだいぶ更新をすることになった。まあ悪いことではないがちょっと開発自体からおいていかれている感があってつらい。とりあえずこれで nodejs から動かしながら C++ のコードを修正したり、 nodejs 上で GUI 作ったりのスタートラインに立てた。なんかそう書くと全然先が長くて厳しいな。とりあえず現状のものは下記のレポジトリに置いた。久々にきついのをやったが子育てと仕事しながらならこんなもんか。

github.com