日本帰国

https://i.sios.com/ir/news/20180615jinji.pdf

米国に来て丸4年が経過しましたが、この度、7月1日付でサイオステクノロジー株式会社 常務執行役員を拝命いたしましたので日本に帰国します。サイオスグループは最近になってホールディングス制に移行していて、そのため私が2009年から一時期執行役員を兼任していたことのあるサイオステクノロジー株式会社はホールディングス持株会社たるサイオス株式会社となり、今回着任するのはそこから全事業を分割移譲した新しい会社なのですが、内外の実質はその昔テンアートニーという社名から始まった社歴を継承するそのものです。

  • サイオステクノロジーは事業部が3つあり、そのうちRedhat Linuxがあるライセンスディストリビューション事業、LifeKeeperがある事業継続ソリューション事業、MFP向け自社パッケージ事業、AWS向けクラウドSaaS事業で構成される第1事業部の副事業部長です。グループ内最大単位の製品開発&販売組織に、技術寄りな立ち位置から経営参画することになると思います。取締役ではないので登記はありません。
  • 1999年に私が設立して2008年にサイオスに買収されたグルージェントからは既に過年度に登記から離れていて、今回帰国で改めて属することはありません。グルージェントは私がいなくなったのちにGoogle向けSaaS事業が爆発成長してグループ内でも有力なキャッシュジェネレーターになってます。鈴木都木丸すごい。
  • 2015年よりプレナスと合弁で作ってたレストランPOS開発事業のBayPOS Inc.も既に過年度に退任しています。BayPOSも私の退任が材料だったかのようにその後に本格的な世界展開がはじまり、事業規模の成長が急加速しています。既に日本への逆輸入?も遂げました。新井正広すごい。
  • 2014年以来の米国SIOS Technology Corp.取締役には留任しますが、私の拠点が日本に移るという時空間的問題から、おそらくより領域を絞った関与にならざるを得ないと思っています。この米国取締役が唯一登記として残ります。私がいずれもし外れることがあれば、ジンクス的には急かつ爆発的な成長を始めちゃったりして。。。

この後数週間で引っ越しなので、家具や車や諸契約やらの整理に追われております。特にウチは大型犬(ゴールデンレトリバー オス2歳半、体重30kg超)が居るので、その手続きは妻に任せっきりですが、結構大変なことになっています。先に6/22に妻と娘と犬を帰国させてから後日6/25に引越荷物の搬出を行い、6/27の朝に家の鍵を大家さんに返す&空港で例のAudi SQ5を買取業者さん(日本にもあるあのガリバー)に引き渡すというスケジュールで、何かミスると正午発の飛行機に乗り遅れるかもしれないので、今からドキドキです。

f:id:masataka_k:20170706091220j:plain

でかいでしょ?

日本の新居は既に5月末にAWS Summit登壇の用事で日本へ出張した時、ついでに探して決めていました。私の通勤と長女の通学の便利が目黒線沿線になるのを大型犬飼育可能条件で絞ると物件は希少だったのですが、お値打ちで質も良いものに巡り会う幸運があり、奥沢に住みます。白金高輪サイオスビルまで所要時間が電車&徒歩でも車でもドアtoドア30分ぐらいなので良かった。中学高校時代に体育が多摩川河川敷のグラウンドであったり、友人がちらほら住んでたりで、目蒲線と呼ばれてた時代に乗ったことがあっただけのところが、30年ぐらい前はカエルみたいに緑で丸っこい路面電車風の記憶で、それぞれ駅は都心近くなのにローカル線のような雰囲気だったんだけれども、今は綺麗で大きな駅になってたり編成車両数増えて列車が長くなってたりして驚きました。

deno2

deno/deno2 at master · ry/deno · GitHub

今週から、denoの根幹部分の新しいプロトタイプとしてdeno2(いずれはlibdenoとする計画?)のコミットが始まってました。GolangをやめてC++とRustで書き直している(Rustのコードがパッと見で見当たらないけど)のは、GolangとV8の両方でGCが動くと難しいというのが主たる理由みたい。さらにはこれまでなかったWindowsプラットフォームでのビルドも上位トピックになっています。 で、ビルドツールはchromiumのgn。私はgn自体を知らないので雰囲気ですが、V8を腹に抱えていることから、最も無難っぽいところに落ち着きました。

I am excited about all the interest in this project. However, do understand that this is very much a non-functional prototype. There's a huge amount of heavy lifting to do. Unless you are participating in that, please maintain radio silence on github. This includes submitting trivial PRs (like improving README build instructions).

ですって。さて、これからどうなるか。静かに遠くから薄く見守ります。

せっかくだからサイズの小さなうちにコード読んでみてるのだけど、TypeScriptへ積極フューチャーしているというのはどこから、何なのか、はまだよくわからん。しかしBUILD.gnをまず読むとgnをよく知らない身としてもdeno2の大まかな構造がすぐわかる。これはgnの筋が良いのかもしれない。

tscを呼び出すのはgnで呼ぶPythonからnodeのシェル実行を使ってやっとJSコードを実行してますがな。そういうことも様子を複雑にして難しい。多分今だけのことなんだろうけど。

deno

github.com

作者のRyan Dahlは言わずと知れたnode作者。nodeのしがらみを捨ててV8+GolangでTypeScriptの実行環境を作ると言う。。。いいぞもっとやれ。

たった24日前からのスタートで、まともなリリースも無いのに、すでに13.4kのStarがついているのが驚く。

個人的な期待

個人的な期待はTypeScriptネイティブな実行環境として存在感示しながらnodeへフィードバックされることです。denoがnodeをリプレースするのは正直ちょっと考えづらいけど、へっぽこな自分では到底書けないだろうなーというレベルのチャレンジを早期からウォッチし続けるのは楽しいし、皆もそう思うようですごい勢いで課題とプルリクが集まってますね。。。もしやワンチャン有るのかな?

そして、ちょうど先月にnode-gypやらcmakeやらで拡張を作ってたタイミングでdenoが出てきた偶然に驚くし、とても琴線に響きます。静的に型が付いてるTS拡張オブジェクト(型の無いJS拡張ではなく)をGolangで書けると素敵で、もしちゃんとdenoが世にでるのであればこれが個人的に期待されます。V8のAPI層ではこれまでと同じでも新しくdenoで丁寧にブリッジ構成したら可能かもしれない。しかしGolangじゃなくSwiftだった方がよりインパクト強烈だった気がする。GolangもSwiftもC/C++バインディングが容易なのでどうにでもなるけど、SwiftだとmacOSiOSにネイティブなのでたったそれだけで何か産まれてきそう。まあ、GolangとSwiftをone on oneで比べたらGolangの方が世の多数に好かれてるようにも思いますけど。

ビルドしてみた

v8worker2のビルドでエラー吐いて止まった。馴染みのないビルドプロセスで追うのが辛そうだから、根性なしはすぐ目をつぶって、もうちょい優しくなってからトライする。brewインスコした環境は残しておきます。go getしたものは削っとく。最近Golangで書いてたもの全部TypeScriptで書き直し終えたところだったからGOPATHの中身は空でした。

$ cd ~
$ rm -rf go
$ mkdir go

ccache ?

手仕舞ってから、ふとビルドの際に--use_ccacheとスイッチ渡していたのを思い出して見直すと、確かにccacheはインスコしてなかった!ということでbrew install ccacheでもう一度ビルドやり直し。。。うん、やっぱりエラー。今日はおしまい。

OfficeのAutoUpdateそれ自体の更新が失敗する

f:id:masataka_k:20180601135642p:plain

普段はGoogle G-Suiteばかり使ってるのでMS-Officeを利用することが無いのですが、Office for Macサブスクリプション(Office 365というべきか)を持ってます。たまにExcel開くと画面のように、「Office Update サブ Web」なるもので更新があることを伝えてくるのですが、ここからポチポチ進めても一向に更新できませんでした。それはAutoUpdateそのものが立ち上がってるためにインストールできないという何のジレンマなのか、パラドックスなのか。

解決方法

support.office.com

重要: [ヘルプ] メニューに [更新プログラムの確認] が表示されない場合は、最新バージョンの Microsoft AutoUpdate ツールを https://go.microsoft.com/fwlink/?linkid=830196 からダウンロードします。ツールを実行し、手順 1 からやり直します。これで、[更新プログラムの確認] オプションが [ヘルプ] メニューに表示されるようになります。

  • ヘルプメニューに更新プログラムの確認オプションが表示されていても、ダウンロードしないとだめ。
  • このページで見つけたリンクから、AutoUpdateのバイナリをダウンロードする。
  • ダウンロードしたバイナリをインストールする。
  • 治る。

AutoUpdateを頼みにしていたら永遠に治らない。

CLionが最強かもしれない

WebStorm大好きっ子な私ですが、WebStormはWEB系技術をターゲットとしていてC/C++IDEサポートはありません。Go言語関連がGoLandに製品格上げなされた結果としてプラグイン提供がなくなってしまったのと同様にC/C++プラグインもどうやらWebStormに提供されていない。素のWebStormでもかろうじてカラーリングやカッコ対応などの静的テキストファイルの範囲で頑張れるような機能はあったので、メモ帳よりはマシだと考えてましたがヘッダを見るのにもSpotlight検索かよ、と、気が短くなった中高年には無理無理。C++「も」気持ちよく書ける環境を模索して構築します。前提はTypeScriptで気持ちよく書けることで、さらに同じその上でC++

CLion

www.jetbrains.com

信頼するJetBrainsの製品群を眺めてそれっぽいのをピックアップしました。ReSharper C++なるものはVisualStudioのプラグインだって書いてありますから初見除外すると、CLionとAppCodeというのになります。まずMac向けっぽい体裁のAppCodeをトライアルして見ましたがいきなりXCodeが無くて動かないぞエラーを出すので即アンインストールしました。私はXCodeを入れずにcommandline-toolsで凌いでいるし、今後もフルのXCodeを入れる予定は無い。またAppCodeはメインとしてSwiftとObjective-Cを対象としてMac向けに閉じていました。次いでCLionをトライアル。CLionはプラグイン設定で「NodeJS」と「Vue.js」それぞれのサポートプラグインをインストールできました。なぜか製品デフォルトでTypeScriptとTslintのサポート(当然上位のJavaScriptサポートやHTML5関連なども)は入ってたので私のWebStormの使い方にC++が加わったセットを容易に構成完了。既に動かす前からガッツポーズな気分。node/TypeScript/tslint/Jestは一通り動かして問題ない。

CMake

CLionでC++の新プロジェクトを作る場面でいきなり直面するのは、CMakeを用いた開発への強い推し姿勢です。IDEを一貫してCMake中心に考えてられているように見受けます。これまでCMakeは使ってなかったので手元環境には入れてなかったのですが、/Applications/CLion.app/Contentsを掘って確認すると、clang・cmake・gdb・lldbを含んでいました。CMakeはBrewだともっと新しいバージョンになりますが、CLionがサポートしているバージョンが一個古いものまでなので逆らわずに/usr/local/binへリンクを作って、IDE外部からの利用でも同じものを使うようにします。

$ cd /usr/local/bin
$ sudo ln -s /Applications/CLion.app/Contents/bin/cmake/bin/cmake

CLionもその他のIDEでもnode-gypを綺麗にサポートしているものは無いと断じられる中、同じ効能をCMakeベースで構築し直したというCMake.jsがありました。これのチュートリアルをざっと流してみて(10分程度で完了します)。。。ちゃんとビルドできるし、node-gypで実現されていたみたいに自動コンフィギュアかつ自動ツールチェーンで動きます。よしよし。あくまで短時間での感触ですが、CLion-CMake-CMake.jsの組み合わせは筋が良さそうな気がする。

$ yarn add nan cmake-js bindings

WebStormが初年度$129.00/Yなのに対してCLionが初年度$199.00/Yなので、この値段差に柔軟性の違いがあるのでしょう。年末までWebStormのサブスクリプション残しているけど、CLion欲しいな。30日トライアルし尽くして気持ち変わらなければWebStormの半年分がもったいないけど切り替えよう。

とりあえずの設定

CMake.jsがNodeアドオンを作るために必要な環境の情報をかき集めてCMakeの設定を行ってくれるので、それをIDEにも教えてあげないといけない。

$ yarn cmake-js print-configure --debug

このコマンドで、CMake.jsがどんな情報収拾し、CMAKEに設定しているかがダンプされます。

cmake "/Users/masataka_k/Projects/clpoc" --no-warn-unused-cli -G"Unix Makefiles" -DCMAKE_JS_VERSION="3.7.3" -DCMAKE_BUILD_TYPE="Debug" -DCMAKE_LIBRARY_OUTPUT_DIRECTORY="/Users/masataka_k/Projects/clpoc/build/Debug" -DCMAKE_JS_INC="/Users/masataka_k/.cmake-js/node-x64/v10.1.0/include/node;/Users/masataka_k/Projects/clpoc/node_modules/nan" -DNODE_RUNTIME="node" -DNODE_RUNTIMEVERSION="10.1.0" -DNODE_ARCH="x64" -DCMAKE_CXX_FLAGS="-std=c++11 -D_DARWIN_USE_64_BIT_INODE=1 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DBUILDING_NODE_EXTENSION -w" -DCMAKE_SHARED_LINKER_FLAGS="-undefined dynamic_lookup"

ながーいコマンド行のうちのオプション部分をコピーしてCLionの設定画面に流し込む。

f:id:masataka_k:20180522060034p:plain

メニューでCLion > Preferences > Build, Execution, Deployment > CMAKE を選び、「CMake Options」にペースト。これでプロジェクトを背後でシンボルの構築が行われてIDEにおよそ期待する機能が動作し始めます。

このオプション群は、チュートリアルでコピペしたCMakeLists.txtに流し込まれてくる。

cmake_minimum_required(VERSION 2.8)

# Name of the project (will be the name of the plugin)
project(addon)

# Build a shared library named after the project from the files in `src/`
file(GLOB SOURCE_FILES "src/*.cc" "src/*.h")
add_library(${PROJECT_NAME} SHARED ${SOURCE_FILES})

# Gives our library file a .node extension without any "lib" prefix
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node")

# Essential include files to build a node addon,
# You should add this line in every CMake.js based project
target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_JS_INC})

# Essential library files to link to a node addon
# You should add this line in every CMake.js based project
target_link_libraries(${PROJECT_NAME} ${CMAKE_JS_LIB})

これらの環境変数で穴開けてるところに当てはまるのをなぞると、だいたい何が行われているのかが想像できます。ここまでくるとあと必要なのはIDEではなく「そのもの」の知識ですね。チュートリアルはOKでも、CMakeの使い方の詳しいところはこれから習得しないといけない。

OSXの.frameworkを探す

# 〜省略
if(APPLE)
    find_library(CORETEXT CoreText)
    message("CORETEXT= ${CORETEXT}")

    find_library(FOUNDATION Foundation)
    message("FOUNDATION= ${FOUNDATION}")

    set(CMAKE_JS_LIB ${CMAKE_JS_LIB} ${CORETEXT} ${FOUNDATION})
    message("CMAKE_JS_LIB= ${CMAKE_JS_LIB}")
endif(APPLE)

target_link_libraries(${PROJECT_NAME} ${CMAKE_JS_LIB})

練習としてOSXの.frameworkを探す定義を書いてみました。find_libraryの第1引数は変数名で第2引数以降に色々探す方法を書くのだけど、今回の場合はCoreText.frameworkの頭の部分だけ「CoreText」を書けば見つけてくれる。よってFoundation.frameworkは「Foundation」。チュートリアルでコピペしたCMakeLists.txtでは、CMAKE_JS_LIBが書かれてたけど実際は使われていなかった。もしかするとどっかから飛んでくることがあるのかもしれないけど、今のところは何も。見つけたライブラリを跡地利用としてsetを用いてCMAKE_JS_LIBに積んでみた。プラットフォームの切り分けは、if(APPLE) - endif()でできる。endifに引数渡さなくても問題ないと思うけど、引数「APPLE」を渡すこの書き方の方が読みやすいです。

ごちゃごちゃやってる後の対応

ごちゃごちゃ色々やってる後で、突然今までなんでもなかったところでコンパイルエラーをエディタが表示し始めることがあった。でもコマンドラインからcmake rebuildを実行すると何事もなくビルドが通る。これはIDEのキャッシュが疑わしい。実際にググって見つけたヘルプの通りにinvalidしてrestartしたら元に戻った。

C++でCoreText

NodeモジュールでまずはOSのフォント情報を取ろうと思います。Objective-CでやればサンプルもたくさんあるのだけどここはC++で書きたかった。

#include <ApplicationServices/ApplicationServices.h>
#include <nan.h>
using namespace v8;

ApplicationServices/ApplicationServices.hを取り込みます。この中にCoreFoundationやCoreTextやCoreGraphics等のOSXの中で特にC++で書けるライブラリのヘッダだけがまとまってます。他はObjective-CかSwiftじゃないと書かせてもらえないみたい。文字列はNSStringではなくCFString。

char* CFStringToUtf8String(CFStringRef str) {
    if (str == NULL) {
        return NULL;
    }
    CFIndex max = CFStringGetMaximumSizeForEncoding(CFStringGetLength(str), kCFStringEncodingUTF8) + 1;
    char* buffer = (char*)malloc(max);
    if (CFStringGetCString(str, buffer, max, kCFStringEncodingUTF8)) {
        return buffer;
    }
    free(buffer);
    return NULL;
}

char* getStringAttribute(CTFontDescriptorRef ref, CFStringRef attr) {
    return CFStringToUtf8String((CFStringRef)CTFontDescriptorCopyAttribute(ref, attr));
}

char* getLocalizedAttribute(CTFontDescriptorRef ref, CFStringRef attr, CFStringRef *localized) {
    CFStringRef value = (CFStringRef)CTFontDescriptorCopyLocalizedAttribute(ref, attr, localized);
    if (value) {
        return CFStringToUtf8String(value);
    }
    return getStringAttribute(ref, attr);
}

逆算的な説明ですが、とにかくCoreFoundationの文字列であるCFStringから、char*へ変換するのは上記の通り。この用意を踏まえて、フォントDescriptorをいじっていきます。キモはCFStringGetCStringで、CoreFoundationの提供する世界から外界たるC++へ値を取り出すのは他のAPIでもおよそこんな感じ。戻りがBoolで成功失敗で値はバッファをポインタ渡しで受け取ります。APIはだいたい汎用な作りになっていて細かにフラグを設定することが多いです。ここではエンコーディング種別がフラグになってます。

CTFontDescriptorCopyLocalizedAttributeでは可能な場合にログインロケールな文字列を戻します。すなわち日本語をメイン言語として利用している私は「ja」でロケールが探され、得られれば第3引数のポインタにjaを返します。多分このログイン言語環境を動的に変更できるAPIもあるんだろうけど、調べてません。とりあえず今の私は不要。

int getFontWeight(CTFontDescriptorRef ref) {
    CFDictionaryRef traits = (CFDictionaryRef)CTFontDescriptorCopyAttribute(ref, kCTFontTraitsAttribute);
    CFNumberRef value = (CFNumberRef)CFDictionaryGetValue(traits, kCTFontWeightTrait);
    CFRelease(traits);
    float weight = 0.0f;
    if (CFNumberGetValue(value, kCFNumberFloat32Type, &weight)) {
        if (weight <= -0.8f) {
            return 100;
        } else if (weight <= -0.6f) {
            return 200;
        } else if (weight <= -0.4f) {
            return 300;
        } else if (weight <= 0.0f) {
            return 400;
        } else if (weight <= 0.25f) {
            return 500;
        } else if (weight <= 0.35f) {
            return 600;
        } else if (weight <= 0.4f) {
            return 700;
        } else if (weight <= 0.6f) {
            return 800;
        }
        return 900;
    }
    return 0;
}

char* getSrc(CTFontDescriptorRef ref) {
    CFURLRef url = (CFURLRef)CTFontDescriptorCopyAttribute(ref, kCTFontURLAttribute);
    return CFStringToUtf8String((CFStringRef)CFURLCopyPath(url));
}

Local<Object> toJSObject(CTFontDescriptorRef ref) {
    Nan::EscapableHandleScope scope;
    Local<Object> res = Nan::New<Object>();
    CFStringRef localized = NULL;
    res->Set(
        Nan::New<String>("font-family").ToLocalChecked(),
        Nan::New<String>(getLocalizedAttribute(ref, kCTFontFamilyNameAttribute, &localized)).ToLocalChecked()
    );
    res->Set(
        Nan::New<String>("font-style").ToLocalChecked(),
        Nan::New<String>(getStringAttribute(ref, kCTFontStyleNameAttribute)).ToLocalChecked()
    );
    res->Set(
        Nan::New<String>("font-weight").ToLocalChecked(),
        Nan::New<Number>(getFontWeight(ref))
    );
    res->Set(
        Nan::New<String>("src").ToLocalChecked(),
        Nan::New<String>(getSrc(ref)).ToLocalChecked()
    );
    if (localized) {
        res->Set(
            Nan::New<String>("localized").ToLocalChecked(),
            Nan::New<String>(CFStringToUtf8String(localized)).ToLocalChecked()
        );
    }
    return scope.Escape(res);
}

JSの世界へ持って来るために、Local<v8::Object>へ変換するのですが、いちいち面倒ですね。〜Refというのは全てポインタ型ですが、API毎に細かく定義されているのでCoreTextやCoreFoundationのヘッダファイルを見てあたりつけて、AppleのDeveloperサイトを検索するというのを繰り返す。。。同じCTFontDescriptorCopyAttributeでも、引数のフラグに応じて戻るポインタ型が違うので組み合わせの正解探しが続きましたが、ヘッダをよく見たらフラグのコメントに戻りの型がちゃんと書いてあった。

フォントWeightの変換ロジックは、font-managerからもらってきました。このfont-managerがメンテナンス止まってたので今回のコードを書いてます。ちょっと直すだけで使えたのだけど、繰り返しますが自分で書いてみたかったから。font-managerでのOSX版はObjective-Cを用いてます。

NAN_METHOD(getAllLocalFonts) {
    CTFontCollectionRef collection = CTFontCollectionCreateFromAvailableFonts(NULL);
    CFArrayRef results = CTFontCollectionCreateMatchingFontDescriptors(collection);
    CFRelease(collection);
    if (results) {
        CFIndex i, count = CFArrayGetCount(results);
        Local<Array> res = Nan::New<Array>(count);
        for (i = 0; i < count; i++) {
            res->Set(i, toJSObject((CTFontDescriptorRef)CFArrayGetValueAtIndex(results, i)));
        }
        CFRelease(results);
        info.GetReturnValue().Set(res);
    } else {
        info.GetReturnValue().Set(Nan::New<Array>(0));
    }
}

NAN_MODULE_INIT(Init) {
    Nan::Export(target, "getAllLocalFonts", getAllLocalFonts);
}

NODE_MODULE(fontFinder, Init)

JSからアクセスされる関数はこちら。C++というよりボキャブラリを問われる書き方が過ぎて、新種のDSLみたいな。。。先日はAsyncの作りを調べてましたが、結局ローカルのAPIを叩くのは完全にSyncで十分だった。500以上のフォントが入っていてもすぐに値が返って来ます。

node-gypそしてnanへ。おまけでTypeScript

github.com

nodeのアドオンを調べてたら本もなければ情報も少なく、この土日ずっと家族には仕事が忙しくてヤバいという体裁を装い篭ってたので、せめて記録ぐらいは残そうとサンプルを書きました。サンプルは超シンプルになりましたがこれは理解してからシンプルに書き直した結果で、元は何倍ものゴミクズです。

承前

  • nodeだけではできないことをやる。
    • 動画や画像の出力や解析はゼロから書くのは無理ゲーだけど良いライブラリが既にある
    • OSにインストールされているフォントを取得して使う。更に出力サイズはきっちり計る
  • パフォーマンス
    • 極重な処理をハイパーにこなす

既存のコマンドをシェルで叩き標準入出力やファイルシステムでツールをチェーンするのは手軽に実現できます。これは手元環境だけでなく、AWS Lambdaとかのサーバレス環境も要はDockerなのでこの方法は有効です。魔法を見て驚いて中身見たらexecやspawnかよとズッコケることたまにある。

無い物はシェルコマンドをGoやSwiftででも作れば良いのでしょう。そのほうがUNIX的な思考だし再利用もしやすい。でも、なんかズッコケるんだよね。燃えない。TypeScriptで楽に書きたいのに、nodeで無理なところ探してアドオン書こうというのは、やってみたかったという初めから答えあってのことです。手間を考えたらあんまり正解じゃなかった気もするけど、ギリギリ日曜の夜にはできるようになったからOK。

nodeのアドオンを作る

nodeのアドオンはnode-gypを使って作るのが便利なようです。これはマルチプラットフォーム開発環境を提供してくれて、プラットフォーム毎にMakefile等を調整してくれる。しかもnpmと組み合わせて利用者の手元でOSとnodeのバージョンに応じてコンパイルすることができるので配布が柔軟になります。あらかじめよくあるOSとnodeの組み合わせについてバイナリを用意しておくためには、node-pre-gypというのもあります。これはAWSに提供側ビルドをホストしておくことがすぐできる。

nodeのアドオンというのは結局はそのJSインタープリタであるV8の拡張なのですが、こいつが後方互換軽視なアグレッシブな姿勢で作られているらしく、つられてnodeもアグレッシブにならざるを得ない様子なので、過去からの互換性を維持するための抽象層がマクロとC++テンプレートで提供されています。それがNan。nodeアドオンの作りかたでググったら全く違ういろんな書き方が出てきます。これは記事が書かれた時期によるV8のAPI変化であり、書き手それぞれのNanの適用範囲の差であり、中にはNanを使っていない例も多いためでした。よってスマートかつモダンにnode-gypでV8かつフルNanなプログラミングをモノにしましょうという算段です。

ざっくりとした手順

実際はサンプル見てもらうことにして、ざっくりと手順をまとめると以下の感じ。

  • nodeのプロジェクトを作る。フォルダにpackage.jsonがあることが最低。
  • nodo-gyp、nanを用意する。node-gypはグローバルに入れるのが大勢のように思われる。
  • binding.gypを書く。これはnode-gypの唯一設定ファイル。
    • Nanの利用はおきまりのフレーズをここに書く。
    • 驚いたのはnodeのシェル実行でrequireを用いていた。node -e "require('nan')"
  • C++ソースコードを書く。V8とNanを利用。
  • package.scriptsに、"{ install": "node-gyp rebuild }" を追加する。
    • 開発時のビルドはこれをnpmやyarnで実行する
    • 公開後は利用者がnpm installないしyarn addしたと同時にビルドが走る
    • ちなみにnode-gyp rebuildはnode-gyp clean && node-gyp configure && node-gyp build
  • マルチプラットフォームはbinding.gypにうまく書ける仕組みがある。サンプルでは利用してない。
  • 既存ライブラリのリンクはbinding.gypにうまく書ける仕組みがある。サンプルでは利用してない。

Nan::AsyncWorker

サンプルでは、TypeScriptコードから文字列の引数とコールバックをアドオンに送る->アドオンは文字列の引数を保存して作業を行い(サンプルでは何も作業していないけど)、結果がOKだったら非同期にコールバックに返す -> コールバックのTypeScriptコードが実行される、という段取りになります。

ポイントはNan::AsyncWorkerがlibuvをラップしていて別プロセスを立ち上げていることです。ハイパーな処理はメインに迷惑かけずにここでやってくれという。サンプルのような軽い処理だったら本当は同プロセス同期実行でいいですね。その上でさらなるポイントは非同期に入る前に文字列を保存しているところです。V8の環境では常にガベージコレクションが走っていて、メモリの解放はAPIの中で強力にサポートされているので、別プロセス非同期実行だと解放された後にメモリアクセスしてしまいます。ここが私の週末の最後の壁で何度もクラッシュさせてました。今はstrcpyでコピーしているけど多分V8のAPIの中にうまい書き方が隠れていそう。 *1

Nan::New<String>("Hello ").ToLocalChecked()

頻出イディオムとして上記のようなコードを書くことになりますが、Nan::Newでv8::MaybeLocalというクラスを作り、ToLocalCheckedでガベージコレクションの対象となるように登録します。

TypeScript

ここまでの論点でTypeScriptは全く関係ないです。全編C++。利用のコツとしては、bindingsなるライブラリを使うことです。const binding = require('bindings')(<binding.gypのtarget_name>); bindingsはバイナリの位置を探します。

const binding = require('bindings')('helloLib');

export const greetingPromise = (name: string): Promise<string> => {
    return new Promise<string>((resolve) => {
        binding.nativeHello(name, (msg: string) => {
            resolve(msg);
        });
    });
};

こんな感じで、最近流行りのPromise化も可能。

import { greeting, greetingPromise } from '../hello';

test('greetingPromise', async () => {
    console.warn(await greetingPromise('Promise World!'));
});

こんな感じにasync-awaitで書けます。素敵。

気づき

  • V8はガベージコレクションをやるんだなということを思い出せばかなり光が射してくる
  • コールバックの有無と同一プロセス/別プロセスは違うこと
  • Nanは常に何かを楽にしてくれようとしてる。彼の親切心が何処にあるのかを探せば真実が見える

25年ぶりのC++、そして新たな問題

対象言語としてC++ 11が必須となっています。で、25年ぶりだから遠い記憶のそれは当然C++ 11じゃないんですよ。私の経験にあるのは学生の時に16bitのVC++でした。その後すぐにDelphiで更にはJava時代がきちゃう。初見はどこまでがマクロでどこから何のAPIで、言語仕様的なものはどこなのかわかりませんでした。結局Nanマクロが黒魔術であり、私が単にC++を忘れていただけでC++ 11的な何かというのは少なかったのですが。。。

ところでそもそも何でこんなことをしているかというと、まずはC/C++で書かれた魔法的ライブラリ達を取り込むためですが、次いではOSにアクセスするためです。しかしそのOSとは私の場合はOSXなのです。これが問題。OSXは基本的にObjective-Cであり、近年Swiftで代替されてつつあるわけです。C++からSwiftをイージーに呼べればアドオンのベースだけC++で書いてあとはSwiftで良かったのだけど、結構調べましたがC++からSwift(SwiftからC++じゃなく)をうまく使う方法がわからなかったので「おいおい、この年でObjective-Cかよ。俺はもう45才だぞ」と暗い気持ちになりました。Objective-CiPhoneの初期のころにいくらかやりましたが、なんかしっくり来なかったのでちゃんと習得しなかった。老い先短いので、同じく老い先短いObjective-Cは避けたい。その点でC++はいつまでも老いずに良いね。

更に調べると、CoreGraphicsやCoreTextといった一部のフレームワークC++でOKなよう。でもC++版のガイドやサンプルは当然皆無。OSXOSSじゃないからヘッダの先はブラックボックスなので、ヘッダを読んでObjective-C版のドキュメントやサンプルを参考にするしかない。ま、OSXの範囲でやりたいことは幸いにCoreGraphicsやCoreTextに集約されているようなのでボチボチやります。

*1:隠れてた。AsyncWorkerのSaveToPersistentとGetFromPersistentを使う