multi-core threading


昔にも書いたマルチコアスレッドコードです。マルチコアによる分散処理を行います。
長らく修正を反映出来ていなかったのでgithubに上げてまとめました。

github

  • github multi-core and muti-threading

https://github.com/Ko-Ta2142/MultiCore


簡単なマルチコアスレッディング管理用クラスです。
マルチコアとマルチスレッドの違いは、処理をコア(論理)に割り当ててるかどうかどうかだけの違いです。
コアに割り当てない設定も可能ですが使う機会は無いでしょう。
性能については、60fpsでの使用に耐えうるモノで、開始と待機の精度はそれほど高くありませんが、待機のCPU使用率が低めの、バランス型です。
もっと精度が欲しい場合はsleep(0)でポーリングすればいいだけなのでこのライブラリは要らないと思います。


このライブラリはWindowsXPで動作するように、XPまでの命令で組まれています。
なので、物理コアと論理コアの判別出来ず、OSに委ねられています。

マルチコア処理

マルチコアによる分散処理を行うには、基本的にスレッドをコアに割り当てるだけ。
windowsの場合は割り当てるAPIは2つあります。

  • SetThreadAffinityMask

https://msdn.microsoft.com/ja-jp/library/cc429346.aspx

DWORD SetThreadAffinityMask (
  HANDLE hThread,             // 操作対象スレッドのハンドル
  DWORD dwThreadAffinityMask  // スレッドアフィニティマスク
);
  • SetThreadIdealProcessor

https://msdn.microsoft.com/ja-jp/library/cc429348.aspx

DWORD SetThreadIdealProcessor(
  HANDLE hThread,         // スレッドのハンドル
  DWORD dwIdealProcessor  // 理想的なプロセッサ番号
);

SetThreadIdealProcessorがよく使われます。
スレッドを論理コア番号に紐付けし、使用中であれば他の空きコアを試みるので、4/8コアのような一部を使う場合にとても素直で賢い挙動を行ってくれます。
ほぼこちらの使用で問題ないでしょう。
もしCPUコアの全てを使用したい場合は、SetThreadAffinityMaskが良いでしょう。
これはスレッドに使用しても良いコア番号をビットマスクで指定します。つまり4コアなら、1,2,4,8とすることで全てのコアに振り分けます。
1+2=3で1と2コアの両方の使用を許可出来ます。使い方としては良い案が浮かびませんが、今は1物理コア2論理コアのハイパースレッディングが主なので、物理コア単位にすると安定するかも知れません。


このライブラリではCPUのコアをほぼ全部使う場合以外はSetThreadIdealProcessorを使うようになっています。

初期化

使い方は簡単です。
初期化はグローバル関数を呼ぶだけです。生成されるスレッド管理クラスは、1アプリケーションにつき1つだけのシングルトンです。
アプリケーション終了時やコア数を変えるときは、解放関数を呼んでください。

  // initialize
  _MultiCoreInitialize(4);  // 実際の生成スレッド数はCPUコア数の最大値に丸め込まれます
  corecount := _MultiCoreManager.Count;
...
  // finalize
  _MultiCoreFinalize;

タスクの登録

マルチスレッド処理の場合、処理を関数にまとめて、その関数ポインタとして管理クラスに登録します。
登録なのでまだ実行はされません。

  for i:=0 to TaskCount-1 do
    _MultiCoreManager.Add( MultiCore_HorizonBlur , @TaskRecord[i] );

与える関数で気をつけるところはマルチスレッドプログラミングと同じです。順序を伴う処理は含まないこと、読み込みは良いけど書き込みは他と重複しないこと、あたりは守る必要があります。
また、与える関数ポインタには、自由に使えるポインタ型の引数が備わっており、タスクに渡したい変数内容などはポインタ(アドレス)にして受け渡します。
上記のように構造体を定義して渡すのが一般的です。

type
  TMultiCoreData = record
    // scanline area
    StartIndex,EndIndex : integer;
  end;
  PMultiCoreData = ^TMultiCoreData;

実行と同期

タスクが登録出来たら実行、そして大事なのが同期です。
全ての処理の完了を待つ必要があります。

  _MultiCoreManager.Start; // start execute task
  _MultiCoreManager.Sync;  // sync all task finished

Startの後にすぐSyncを呼ぶ必要はありません。待ち時間にメインスレッドでなにか処理をさせたいなら間に処理を挟むと良いでしょう。


以上がマルチコア処理の1サイクルです。

仮想負荷値を与える

このライブラリにはシンプルな負荷によるタスクの振り分け機能を持っています。

  _MultiCoreManager.Add( hogefunc,@hogerecord , 100 );

例えば、重い処理と軽い処理があった場合、重さが均等になるようコアに割り当てられます。

core0 : [task][task][task]
core1 : [task][task][hevytask]
core2 : [task][hevytask][task]
core3 : [task][task][hevytask]

イメージとしてはこのように構築します。指定無しはweight:1とされます。


だいたいタスクは均等になるように調整して渡すことになるので、この機能は有用そうに見えますが、実際使うことは無いでしょう……。
パフォーマンスを求めるならコア数に対して平等に処理を分割するコードを書くので尚更不要です。

待機と同期精度のお話

昔のバージョンもこれとあまり大きな変化はありませんが、何が変わったかというとタスクが無い状態での待機処理です。
割と面倒なのがこの待機処理だったりします……。


普通の使用なら、CPU使用率は度外視できるので、セマフォで同期を取ってタスク開始フラグをポーリングするだけでOKなので簡単です。
また、1回の処理限定なら、終わったらスレッドをそのままterminate(終わり)させておけば何の問題もありません。
しかし60fps環境で使用する場合は要求がちょっとタイトです。

  • 16ms周期で使用されるためスレッドは使い回すこと。
  • start/syncの応答精度が早い、個々のスレッドの待機と復帰が早いこと。
  • 待機状態でのCPU使用率が低いこと。(負荷ではなく使用率です)

の3条件です。


上でも出たフラグポーリングですが、よくあるのが以下のようなコードです。

  while true do
  begin
    semaphore.lock;
    f := StartFlag;
    semaphore.unlock;
    if f then break;
    // sleep
    sleep(1);   // sleep(0);
  end;

StartFlagがtrueになったら抜けるだけですがsleepが問題です。
応答を早めたいならsleep(0)を使いたいところですが、実際はCPU使用率が恐ろしく跳ね上がるのでsleep(1)が妥協点です。
sleep(1)の場合1msで同期が終わるかというと、スレッド間の微妙な誤差もあるので倍の2msぐらいに膨れ上がることは考慮しなければなりません。
更にstartとsyncでそれぞれ判定が入ることを考えれば、最大2+2=4ms。
更にwindowsは3msが最小単位だとかなんとかあるので、もしかすると3+3=6msになるかも。
60fpsで動作するなら、約16msのうち4ms(6ms)が消えるのは痛い出費です。


というわけで、実際のコードはちょっと変な感じですが、semaphoreであるクリティカルセクションの二重ロック(デッドロック)による永久待ちを基本にしています。

// main thread
  _MultiCoreManager.Start; ( wait.unlock )
  _MultiCoreManager.Sync; **1

// sub thread
  wait.lock;  // lock 1
  while true do
  begin
    wait.lock;  // lock 2. wait.
    wait.unlock;  // lock 1-1=0
    // flag check
    if (flagcheck) then break;
    ...
    sleep(0);
  end;
  TaskExecute;
  wait.lock;  // lock=1 **1

ただし、この方法は不完全ですり抜けることがあります。
そこですり抜けからデッドロックされるまでのちょっとの間だけ、精度の高いsleep(0)によるフラグポーリングを行います。

あれ?

マウスでぐりぐりする最初の1回目だけ処理時間が40msとかやたらかかることがあります。
この症状、どうやらクリティカルセクションの挙動によるところのようです。
待ち時間が短いとキビキビ動くんですが、待ち時間が長いと判定を甘くして地球に優しいモードになるようです。

  • InitializeCriticalSectionAndSpinCount - spin count

https://msdn.microsoft.com/ja-jp/library/cc429225.aspx
スピンカウントの値で調整してくださいね!と言うことですが、単位が不明なので正直解らないです:-Q
だいたい500ms過ぎたら寝る感じなので、60fpsなりで動く分にはキビキビ動くので、このあたりは妥協しましょう……。
尚、CPU負荷率が常に高い場合だとこの待機時間が緩和されます。このあたりはハードウェアな領域なのでこれ以上足を突っ込まない方が良さそうです。


ちなみに、普通は2でデッドロック解除待ちになりますが、任意の数値にも出来るそうです。
各スレッドごとにセマフォ持つよりは一本化する方が良い結果が出そうなのでそのうちやってみますが、更新が無かったら……期待値は得られなかったと言うことで。

暈かし処理

いつか触れようと思いますが、サンプルの暈かし処理には "prefix sum" という、値を加算して蓄積していく手法を使用しています。
原理は以下のページが詳しいです。動画に至っては英語では無いですが、図解でなんとなくわかるかと思います。

  • prefix sum

https://en.wikipedia.org/wiki/Prefix_sum

  • 2d sum quaries

https://www.youtube.com/watch?v=hqOqr6vFPp8
サンプルは横方向しか暈かしていませんので1次元です。ボックスの左端と右端を読み込めば、差分でボックス内の合計値がわかるという優れものです。
暈かしのボックスサイズが変わっても一律の処理負荷で行うアルゴリズムとして有名です。
shaderでやるならnVIDIAのサンプルを見るのが良いでしょう。

https://docs.nvidia.com/gameworks/index.html#gameworkslibrary/graphicssamples/d3d_samples/d3dcomputefiltersample.htm
下ごしらえに加えて、RGBの深度ビット数が増える、ハードウェアなら浮動小数バッファが必要になるので、ちょっと要求が高いのが難点ですが、暈かしをリアルタイムで出来るようになったのは昨今の進化を感じます。

UndoRedoのメモリの最適化


今までundo機能をなぁなぁで組んでいたら(全シリアライズデータをそのまま保持)メモリ使用量がヤバいことになったのでまじめに組み直してました。
というわけでundoとそれに使用するメモリ量の最適化のお話です。
エディタでも作らない限りundoredoのお世話になることは無いと思いますが……。

git hub

https://github.com/Ko-Ta2142/UndoRedo

まずはサンプル。
サンプルは簡単なペイントソフトでundoredoが可能です。
本題はお絵かきではなくて、キャンバスを128x128のセルで分割し、セルのシリアライズデータ(まんまピクセルデータ)をundoデータとして出力、内容の重複を検知して共有化、メモリ使用量が減る、といった一連の流れになります。
スクリーンショットだと、だいたい60〜80%ぐらいの削減が出来ています。


根幹のライブラリコードは20KB程度ととても小さい&依存性が無いので移植も簡単な内容です。
サンプルのペイントアプリのコードの方がよっぽど巨大です。(GUIアプリは仕方ないよね)

まずundoとredo

記憶するデータは、大きく分けると二種類あると思います。行動を記憶する手法(アクション)か、復元データを記憶(シリアライズ)していく手法です。
アクションの方が容量がコンパクトになる傾向ですが、undo処理そのものが編集対象に大きく依存したり、処理がかなり複雑・大規模になるので、とりあえずSaveLoadで保存と復元が可能なシリアライズデータを扱う方法で行きます。
圧縮も今回の本題では無いのでパスします。今回の最適化処理を施した後にかけるのが、容量的にも速度的にも理想的です。


シリアライズ方式であれば原理はとても単純です。作業が進めばデータをpushしていくだけ。前後の差分を気にする必要もありません。
ちょっと気を遣うとすれば、追加時に「変化が無ければ追加しない」を組み込んでおくのが現実的です。同じデータを追加させない機能があるだけで、undoredoの管理は雲泥の差で楽ちんになります。

  if UndoAdd(data) then
    // 変化があったのでundoに追加された
  else
    // 同一データなので変化無し

動作で気をつける点があるとすれば、undoのデータ保持上限を超えた場合(古いデータを破棄)、undo後に編集を行った場合(redo用に残っている前方のデータを破棄)の2つはデータの削除が発生します。

[redo data 3] (delete)
[redo data 2] (delete)
[*now] (edit!)
[undo data 1]
[undo data 0]

undo管理クラスがデータの解放まで行う都合上、データは編集オブジェクトとの関係が完全に分離されているのが望ましいでしょう。
データの解放にオブジェクトの参照が必要な構造は、オブジェクトの変化があるエディタではちょっと危険です。

シリアライズデータを小分けにする

シリアライズ方式の欠点は、サイズ・容量が大きくなりがちなことです。圧縮でなんとかなる場合もありますが、画像データなどはあまり期待出来ません。
またサイズが大きいと言うことはそれだけ生成負荷も高くなる傾向にあります。特にメモリ確保の負荷は大きくなるでしょう。
そこで、今後の布石としてまずはシリアライズデータを小分けにします。アニメーションデータ1つ!ならそれを素材や構造ごとに分けましょう。

(animation object)
  [image0 data]
  [image1 data]
  [sound0 data]
  [motion0 data]
  [motion1 data]
  [motion2 data]
...

分けただけではメモリ使用量は減ったりしてくれませんが、データの変更部分の特定、そしてなによりシリアライズデータの生成コスト(負荷)を分散、軽くすることに役立ちます。
また、小分けにしたことで生成の最適化も以下のような感じで簡単に組み込むことが出来ます。

procedure AnimationMotionClass.MakeSerialize();
begin
  if not(ChangeFlag) return;
  // make serialize data
  ...
  ChangeFlag = false;
end;
function AnimationMotionClass.SetTime(time:float);
begin
  MotionTime := time;
  ChangeFlag = true;
end;

変更が無ければ作り直さないよ。というフラグを噛ましておく常套手段が有効でしょう。
シリアライズデータの生成は未圧縮でもそこそこ重い処理なので、分散と生成の最適化(キャッシュ)は速度面できわめて重要です。

データ(メモリ)サイズの最適化

undoデータはその性質上、全く変更されていないデータの部分が大半を占めることになります。

(undo->)
[MotionData0]*---------*****
[MotionData1]---*******----*
[MotionData2]---------------
pass : -
changed : *

こんなかんじで大半が1つ前の同一のデータが並びます。
1つ前との差分方式などXOR比較など様々な方式が思い浮かびますが、比較速度、複製速度、扱いやすさ(取り出し)で同一データの場合は共有化させることで落ち着きました。
上の例なら以下のような感じに共有化します。

[data0]*---------*****
[data0]011111111123456
number : data class

実装も極力シンプルに、メモリ上のバイナリデータ(MemoryStream)から最適化されたクラスを取得、あとはそれを介してバイナリにアクセスするだけです。

  CacheClear;
  CacheSerializeData := object.GetSerializeData;
  CacheUndoData := MemoryOptimizePool.Get(CacheSerializeData); // get optimize data class

データを保持するクラスは自身の複製を作ったら参照カウンタを+1、解放されたら-1、0で実際に解放するよくある構造です。
ガベージコレクトというよりwindowsのcomみたいなヤツですね。

// replicate 複製
function MemoryBlock.Replicate();
begin
  AddRef();
  return(self);
end;

procedure MemoryBlock.AddRef();
begin
  RefCount++;
end;
procedure MemoryBlock.Release();
begin
  RefCount--;
  if RefCount = 0 then self.Free;
end;

複製と解放のコストがほぼ0(カウンタを+-1するだけ)なので、大量に複製する事になるundoに向いてます。
また、(共有化済み)データの比較がアドレス比較で済むので、内容に変化があったか(undoに追加して良いか)を検知が限りなく低コストで済みます。
嬉しいですね。

procedure MemoryBlock.Compare(other:TMemoryBlock):boolean; inline;
begin
  // self === other
  Result := pointer(self) = pointer(other);
end;

わざわざ関数作ってやることでは無いですが。

管理プールについて

共有化されたメモリデータはプールに登録、管理されます。

  // create
  pool := TmemoryBlockPool.Create;
  // get block from memory pointer
  block := pool.GetBlock(ptr,size);
  // free block
  pool.FreeBlock(block);
  // free
  pool.Free;

基本はシングルトンで動き、あのオブジェクトのデータもこのオブジェクトのデータもなんでも突っ込んで、雑に使って良いように設計されています。
無駄に重複しそうなものがあれば、undo以外のモノをぶち込んでも問題ありません。


雑に使っても良いようにプールとしてはそれなりに多いデータを保持することになります。だいたい1万個ぐらいは想定の範囲でなければなりません。
データの登録削除もそれなりに早くて、かつ検索が早い方式として、BinaryTree(二分木)を使用しています。
二分木と言っても、「同値を入れることが出来る」「削除が可能」なものが必要で、ちょっとアルゴリズムが複雑です。
ライブラリに無ければ、リニア(ふつうのarray配列)でも十分な速度が出てたので、それでもいいかなと思います。

最後に

ツールを作っていると億劫になりがち(出来れば実装したくないなぁ)なundoredoですが、わりと楽出来るように落とし込めたと思います。
オブジェクトの方にシリアライズ出力を書くのが億劫ではあるんですが、undoredoについてはマルチプラットフォーム(ビッグ、リトルエディアント問題)を無視出来るので(データを他に持っていったりしないので)、既にSaveLoadがあればそのデータを使って、無ければ雑にバイナリに書き込むのを用意して楽するのが良いと思います。雑で。

spine morph-target

本家より良い返事が貰えたので、経緯を残しておきます。そのうち使えるようになるそうです。ありがたし!


ver3.6のspineを弄っているとメッシュデフォームがあるものの、口パクや表情でよく使うモーフターゲット(morph-target)がないので、固定的なものは力業でなんとかなるとしても、動的な表情変化などの実現が厳しいなぁと気づくと思います。
なんとかしたいですよねナナチ。
幸いランタイムのソースコードがフルオープンなので中を覗けますし、(ライセンスの範囲内で)勝手に弄ってもOKなので、なんとかできないかなーと休日眺めてみました。

サンプル


どういったものかはサンプルで。

  • spine-c morph-target customize sample

https://1drv.ms/u/s!AvtcMsC8irYBgzJkqmizB2me52T1

まず過去ログ

フォーラムの過去ログ(英語)を眺めてみたら、過去に2度ほど話題には上がったみたいです。
少なからず同じような悩みはあったみたいですね。

まずモーフターゲットが実装できるか

モーフターゲットを実現させるにはいくつかの条件が必要です。

  • 変形前のベースとなる形状が必要

名前の通りベースとなる”ターゲット”が必要になります。これは変形前の形状で、変形後との差分の計算に使用されます。
差分はそのままベースに加算されていき、1つのオブジェクトに変形が複数ミックスされていきます。

diff? = Animation? - base
out = base + diffA + diffB + diffC + diffD ...

みたいな感じです。
幸いspineにはベースが存在します。エディタではSETUPとして部品を組み立てるモードがありますね。あれです。あれを使いましょう。
プログラム上だとちょっとわかりにくいですがskeletonから追跡できます。Animation.cを見れば手っ取り早いかなと思います。

  • 変形後の形状もベースと同じ構造をしていること

これはSETUPから変形させて作られたアニメーションのことを指します。
モーフターゲットの場合はベースと構造が同じでなくてはいけません。メッシュなら頂点数とかポリゴンの張り方とか。
幸いspineも同じガイドラインを敷いているので問題無さそうです。

  • それら複数の形状を指定できる環境

複数合成するのでその土壌がなければいけません。幸いアニメーションはトラックという概念で複数扱えるので問題ありません。

  • ということで

エディタ上では難しいけど、ランタイムのアニメーショントラックに仕込めば実現できそうです。
エディタだとプレビューのあそこですね。タイムラインではありません。

実装

spine-c ver3.6を使用します。実装の大半はAnimation.cになります。
「void _spDeformTimeline_apply」あたりを見てみると分かりやすいと思います。
前半と中盤は定義キーが無い前と後ろの処理、要するに例外処理なので飛ばします。
一番最後あたりにアニメーションの合成にあたるコードがあります。

for (i = 0; i < vertexCount; i++) {
      float prev = prevVertices[i];
      float v = prev + (nextVertices[i] - prev) * percent;
      vertices[i] += (v - vertices[i]) * alpha;
}

prevVerticesが現在位置より後方の変形情報、nextVerticesが現在位置より前方の変形情報です。
percentがキー間のブレンド率なのでvが現在地点での変形座標。alphaがモーションのブレンド率なのでアニメーション間の合成だけ抜き出せば

out = out + (v - out) * alpha

と単純な構造です。ここをモーフターゲットに改良するだけです。


モーフターゲットはベースとなる変形前の座標が必要になります。
spineでいえばSETUPですが、このコードの上あたりを見ると―

case SP_MIX_POSE_SETUP:
    if (!vertexAttachment->bones) {
        memcpy(vertices, vertexAttachment->vertices, vertexCount * sizeof(float));
    } else {
        for (i = 0; i < vertexCount; i++) vertices[i] = 0;
    }
...

まんまがあります。
「vertexAttachment->vertices」がSETUP、変形前のベース情報になります。
SP_MIX_POSE_SETUPは最初に投げられるもので、変形前の状態を変形バッファにコピーしてる感じです。


これで必要な要素は揃いました。
あとは以下の公式に変形すればいいだけです。

out = out + (A - SETUP) * alpha

なので

float* setupVertices = vertexAttachment->vertices;
for (i = 0; i < vertexCount; i++) {
    float prev = prevVertices[i];
    float v = prev + (nextVertices[i] - prev) * percent;
    vertices[i] += (v - setupVertices[i]) * alpha;
}

となります。おわり。

本当はもうちょっと複雑

アルゴリズムの話は以上でおしまいですが、実際はもうちょっと複雑です。計算が複雑というわけではなく、例外処理、最適化処理との相性、言語的な部分などなど。
今までアニメーションは上書きだったので、alpha1.0だとそれより前のアニメーション処理要らないですね!なども組み込まれているので対処が必要です。
実際に組み込むには20カ所ほど変更や修正が必要です。
実装されるまで待ちましょう :Q

spine c++ serialize

たぶん必要になるでしょうアニメーションのシリアライズ、保存、復元です。ただし、ゆるーいシリアライズです(後記)。
spineにはアニメーションを保存する機能が、ありそうでありません。作らないといけません。付けて欲しいなぁ…。

spineのアニメーション

アニメーションはトラックという概念で管理されています。エディタだと「preview」機能で確認できます。

[優先↑]

track2:[何もしない][瞬き][何もしない][瞬き]
track1:[狙う]
track0:[歩く][走る]
base  :[setup状態]

横方向が単純なモーションの切り替わりです。上の例では歩きから走るモーションに切り替わります。切り替わるタイミングは「delay」で指定することが出来、0(指定無し)だとループポイントで切り替わります。アニメーションを作成する際はループポイントで繋がるように作りましょうって感じみたいです。もちろん切り替わりが分からないようにミックス(ブレンド)されます。
で、縦方向がtrack(トラック)の概念です。難しいものではありません、単にキーフレームが上書きされるだけです。キーフレームが無い箇所(ボーンやスキニングポイント)はそのまま通過、歩いているアニメーションが使用されます。
こんな感じで、部分部分のアニメーションを上書きさせていって、ゲームで必要な動的なアニメーションをプログラムから制御しましょうというのがtrackです。
なお、trackが何も無い状態だと、エディタにおける setup 状態となります。


頑張ればlive2Dみたいなことも出来そうですが、顔を左右に動かすようなものをプログラムからブレンドさせてコントロールするのは現状無理があるので、そこが利用用途が異なる理由の1つでしょう。決まったアニメーションには強いのですが。
(でもソースを見てるとちょっと弄れば出来そうなので暇が出来たらまとめたいと思います)

データ構造

アニメーションを保存するには spAnimationState の情報を使用します。

  • spAnimationState
struct spAnimationState {
	spAnimationStateData* const data;

	int tracksCount;
	spTrackEntry** tracks;

	spAnimationStateListener listener;

	float timeScale;

	spTrackEntryArray* mixingTo;
	void* rendererObject;
};

trackscount,tracks が上で説明したアニメーションの構造そのままです。ポインタのポインタ(**)がきたら9割は「あー配列かぁ」なので恐れないでください。trackscount分だけ構造体のポインタが並んでいるだけです。timescaleは使うかも知れませんね。保存して復元させると良いでしょう。
あとは復元には不要なので省略します。


次にtrackの情報 spTrackEntry について。

  • spTrackEntry
struct spTrackEntry {
	spAnimation* animation;
	spTrackEntry* next;
	spTrackEntry* mixingFrom;
	spAnimationStateListener listener;
	int trackIndex;
	int /*boolean*/ loop;
	float eventThreshold, attachmentThreshold, drawOrderThreshold;
	float animationStart, animationEnd, animationLast, nextAnimationLast;
	float delay, trackTime, trackLast, nextTrackLast, trackEnd, timeScale;
	float alpha, mixTime, mixDuration, interruptAlpha, totalAlpha;
	spIntArray* timelineData;
	spTrackEntryArray* timelineDipMix;
	float* timelinesRotation;
	int timelinesRotationCount;
	void* rendererObject;
	void* userData;
};

色々ありますが、9割ぐらいはアニメーションをセットした際に設定される値なので省略しちゃっても良いかなと思います。厳密にやり出すと前のアニメーションとのブレンドミックスなどもあるので相当大変です。今回はそこまでやりませんし、そこまで必要ないと思いますし。
必要そうなのは、現在のアニメーション名を知るための animation、次のアニメーションを知るための next、再生時間関連の tracktime,delay,timescale あたりでしょうか。alphaも弄るなら要りそうです(このalphaは色では無く、trackごとのアニメーションのブレンド率です)。


名前取得するアニメーションデータの構造体 spAnimation は簡単です。

  • spAnimation
typedef struct spAnimation {
	const char* const name;
	float duration;

	int timelinesCount;
	spTimeline** timelines;
} spAnimation;

これはデータなので、弄ってはいけません。動的な情報を管理する state ではありません。
name だけ拾えば良いですね。
duration はこのアニメーションの1ループの長さです。1.0で1secの単位で表されていますが、これは今回不要です。

シリアライズ、保存

主に spTrackEntry の情報を保存すれば、復元は出来そうです。trackごとにアニメーションが何個並んでいて、それぞれがどのような状態なのかを記憶していきます。簡単に言えば2次元ループ処理ですね。

  • _savestate
void _savestate(spAnimationState* animation,const std::string &filename) {
	std::stringstream* ss = new std::stringstream();
	int count = animation->tracksCount;

	*ss << "trackcount " << count << std::endl;
	for (int i = 0; i < count; i++) {
		if (animation->tracks[i] == nullptr) {
			*ss << "exists 0" << std::endl;
		}
		else {
			*ss << "exists 1" << std::endl;
			_savestate_tracks(ss, animation->tracks[i]);
		}
	}
	*ss << "[eof]";

	_writestringfile(ss, filename);  //stringstreamを保存する関数

	delete ss;
}

状態保存は構造体にして保存したほうが良いかなと思いましたが、テキストの方が見易いので stringstream 使っておきます。実際組む場合はもうちょっと賢いjsonなどに出力させた方がいいでしょう。
trackscount でループを回す際に注意点があります。アニメーションを track0,2 と指定した場合、track1 がnullになります。なので、nullチェックが必要です。ここでは「exists」がそれにあたります。存在する場合は、track の中のアニメーション情報 spTrackEntry の stringstream に追加していきます。

  • _savestate_tracks
void _savestate_tracks(std::stringstream* ss, spTrackEntry* track) {
	//entry animation count
	int count=1;
	{
		spTrackEntry* data = track;
		while (data->next != nullptr) {
			data = data->next;
			count++;
		}
		*ss << "animationcount " << count << std::endl;
	}
	//each animation satate (track entry)
	spTrackEntry* data = track;
	for (int i = 0; i < count; i++) {
		*ss << "name " << data->animation->name << std::endl;			// string
		*ss << "duration " << data->animation->duration << std::endl;	// animation time length
		*ss << "tracktime " << data->trackTime << std::endl;
		*ss << "timescale " << data->timeScale << std::endl;
		*ss << "loop " << data->loop << std::endl;						// int
		*ss << "delay " << data->delay << std::endl;
		//next
		data = data->next;
	}
}

横方向に当たるtrackないのアニメーションですが、数を知る場合は count が存在しないのでちょっと工夫が必要です。next で次の情報が取得出来る数珠つなぎで管理されているので、nextをくるくる遡ってカウントを数えます。nullがでたら終端です。
count が取得出来たら、あとは同じようにくるくる回して情報を保存していきます。
できあがったのがこちら。

  • savestate.txt
trackcount 3
  exists 1
  animationcount 2
    name walk
      duration 0.8667
      tracktime 0.333333
      timescale 1
      loop 1
      delay 0
    name run
      duration 0.8
      tracktime 0
      timescale 1
      loop 1
      delay 0.6667

  exists 0

  exists 1
  animationcount 1
    name aim
      duration 0
      tracktime 0.333333
      timescale 1
      loop 0
      delay 0

改行とスペースと空行は実際にはありません。分かりやすいように付けただけなので、実際のデータとは異なります。

シリアライズ・復元

この情報から復元します。まっさらなモーション状態 spAnimationState_clearTracks にして、そこから再構築します。
コードもシリアライズとほぼ同じ構造なので、理解は簡単でしょう。
「>> name >> exists」のような構文は、値の読み込みです。便利ですが手抜きです:)

  • _loadstate
void _loadstate(spAnimationState* animation, const std::string &filename) {
	std::stringstream* ss = _readstringfile(filename); // stringstreamにファイル読み込みます
	std::string name;
	int count;
	*ss >> name >> count;
	printf("%s:%d\r\n", name.c_str(),count);

	for (int i = 0; i < count; i++){
		int exists;
		*ss >> name >> exists;
		if (exists == 0) {
			// null
		}
		else {
			_loadstate_tracks(ss, animation, i);
		}
	}
	delete ss;
}

void _loadstate_tracks(std::stringstream* ss, spAnimationState* animation, int no) {
	// animation count
	std::string name;
	int count;
	*ss >> name >> count;

	// each animation
	for (int i = 0; i < count; i++) {
		// animation name
		std::string animationname;
		*ss >> name >> animationname;
		// duration (not use)
		float duration;
		*ss >> name >> duration;
		// tracktime
		float tracktime;
		*ss >> name >> tracktime;
		// timescale
		float timescale;
		*ss >> name >> timescale;
		// loop
		int loop;
		*ss >> name >> loop;
		// delay
		float delay;
		*ss >> name >> delay;

		spTrackEntry* entry;
		if (i == 0) {
			entry = spAnimationState_setAnimationByName(animation, no, animationname.c_str(), loop);
		}
		else {
			entry = spAnimationState_addAnimationByName(animation, no, animationname.c_str(), loop, delay);
		}
		entry->trackTime = tracktime;
		entry->timeScale = timescale;
		//printf("animation:%s time:%f delay:%f loop:%d\r\n", animationname.c_str(), tracktime, delay, loop);
	}
}

モーションを構築する際に一応 setAnimation と addAnimation を使い分けてあります。全部 add で良いような気もしますが、set のほうがtrackにあるモーションをすべて破棄して設定するとあるので、安全かな?と思います。
実は stringstream の特性でアニメーション名にスペースがあると動きません。実装の際は string convertspaceword(const string) みたいな関数で文字を置き換えて対処などしてください。
あと AnimationByName はサンプルなので使ってますが、無い名前を渡すとnull例外アクセスで死ぬので、ちゃんと自分で名前チェック入れるか自前でfix命令作って代用してくださいね。

実験

前回作ったポリゴン表示ログに細工して、出力ファイルが同じかどうか比較します。

//before
triangle:vector(824.137,932.741,728.086,745.995,547.565,838.845):argb=(1,1,1,1)
triangle:vector(547.565,838.845,643.616,1025.59,824.137,932.741):argb=(1,1,1,1)
triangle:vector(568.606,539.411,586.991,577.27,592.505,573.412):argb=(1,1,1,1)
//after
triangle:vector(824.138,932.741,728.086,745.995,547.565,838.845):argb=(1,1,1,1)
triangle:vector(547.565,838.845,643.616,1025.59,824.138,932.741):argb=(1,1,1,1)
triangle:vector(558.639,534.024,592.008,535.425,590.503,537.489):argb=(1,1,1,1)

あれ?ちょっと違う…。
いろいろ試した結果、spSkeleton_updateWorldTransform(ワールド座標などをスケルトンに反映)を抜いてみると

//before
triangle:vector(824.137,932.741,728.086,745.995,547.565,838.845):argb=(1,1,1,1)
triangle:vector(547.565,838.845,643.616,1025.59,824.137,932.741):argb=(1,1,1,1)
triangle:vector(568.606,539.411,586.991,577.27,592.505,573.412):argb=(1,1,1,1)
//after
triangle:vector(824.137,932.741,728.086,745.995,547.565,838.845):argb=(1,1,1,1)
triangle:vector(547.565,838.845,643.616,1025.59,824.137,932.741):argb=(1,1,1,1)
triangle:vector(568.606,539.411,586.991,577.27,592.505,573.412):argb=(1,1,1,1)

やったーおなじだー。
spSkeleton_updateWorldTransform のドキュメントを見ると「IKやら物理計算も反映させます」とあるので、それかなと思うのですが、サンプル「spineboy」には設定されてないような……。


これ以上は実際に表示させて詰めるしか無さそうです。
なお、復元直後に_savestateして保存したアニメーション情報を見てみると一緒だったので、アニメーション情報に関してはちゃんと復元されているようです。

忘れてました、emptyアニメーション

アニメーションをその状態で停止させる命令に EmptyAnimation というのがあります。これは特殊なアニメーションになるので、対応させるには上記にちょっと工夫が必要になります。


まず empty をセットすると trackentry の情報がどうなるのかを調べましょう。

spAnimationState_addEmptyAnimation(animationState, 0, 0.0f, 0.0f);  //mixDuration=0.0 delay=0.0

...
  name walk
...
  name 
    duration 0
    tracktime 0
    timescale 1
    loop 0
    delay 0.7667
  name run
    duration 0.8
    tracktime 0
...

名前が になるのが特徴です。そのほかのパラメータは特に変わりはありません。
mixDurationは前のモーションとミックスブレンドするものだと思われます。停止させるのにミックス?よくわかりませんが、試しに1秒を指定してみると。

spAnimationState_addEmptyAnimation(animationState, 0, 1.0f, 0.0f);  //mixDuration=0.0 delay=0.0

...
  name walk
...
  name run
    duration 0.8
    tracktime 0
...

消えました。…なんで?
コードを追うと spAnimationState_update あたりで消滅しています。
最後に追加しないとだめよ!という事で最適化されたのかもしれませんが、コードを追ってもよく分からなかったので……見なかったことに。

emptyアニメーションの判断

このemptyアニメーションなんですが、trackentryから判別する手段がこれといって用意されているわけではありません。なので、自前で判別する必要があるのですが、大きく2パターンあります。

  • 名前から判別

entry->animation->name の名前が となるのが特徴です。
たぶんこの仕様は変わらないと思います。たぶん。(なおconstで名前が定義されてたりはしません)
くれぐれも自分が作ったモーションに なんて名前を付けてはいけません。ってマニュアルには書いてなかったので、世界中で数人ぐらいはこれで苦しんだ人も居たかも知れません。

  • animetionポインタのアドレス

もっとプログラム的に判別するなら AnimationState.c ファイルの戦闘を見るとemptyアニメーションがちゃっかり static されてます。

static spAnimation* SP_EMPTY_ANIMATION = 0;

いわゆるひとつのしんぐるとんで、entry->animation が SP_EMPTY_ANIMATION と同値であればemptyだと判断できます。
問題はこの SP_EMPTY_ANIMATION がexternされていないことなんです……。


というわけで、名前なりで判断して、loadstateのところを以下のように切り替えてあげましょう。

void _loadstate_tracks(std::stringstream* ss, spAnimationState* animation, int no) {
...省略...
		spTrackEntry* entry;
		if (i == 0) {
			if (animationname == "<empty>")
				entry = spAnimationState_setEmptyAnimation(animation, no, 0.0f);
			else
				entry = spAnimationState_setAnimationByName(animation, no, animationname.c_str(), loop);
		}
		else {
			if (animationname == "<empty>")
				entry = spAnimationState_addEmptyAnimation(animation, no, 0.0f, delay);
			else
				entry = spAnimationState_addAnimationByName(animation, no, animationname.c_str(), loop, delay);
		}
		entry->trackTime = tracktime;
		entry->timeScale = timescale;
...省略...

なお、delayの値がどうにも-999.7みたいな値を示すこともあるので、設定しないほうが良いのかも知れません。わからん。

Netjs

Netjsもduocodeと同じくC#からTypeScript(JS)へ変換するプロジェクトです。ちょっと試してみましょう。

インストール

  • Netjs

https://github.com/praeclarum/Netjs
これはduocodeほど環境を必要とされません。githubからcloneして、コンパイルしてexeとして使用します。
出力はTypeScriptなので、まだならその環境もインストールしておきましょう。

変換

c#のクラスモジュール、特にPCL(ポータブル・クラス・ライブラリ)が望ましいみたいです。
C#でDLLで吐かせた後、そのDLLからTypeScriptに変換します。
公式の通り

netjs Library.dll

でtsが出力されます。
これも mscorlib 依存ですが、そのファイルは一緒に出力されません。どこにあるのかというと、cloneしたもののトップディレクトに「mscorlib.ts」として存在しするので、適当にコピーして環境にもってきましょう。
コンパイルはバージョンの指定「-t ES5」が必要です。無いと怒られます。

tsc -t ES5 mscorlib.ts Library.ts --out out.js 

で、1つのファイルにまとめられて完了です。
duocodeと比べると「mscorlib.ts」のファイル容量がかなり少ないです。これはtsによるところもあるでしょうが、バージョンが古いみたいです。
例えば下記のようなクラスを変換してみたところ

	public class text
	{
		/**
		 * A to Z change low case.
		**/
		static public string lowercase(string s)
		{
			int len = s.Length;
			//StringBuilder sb = new StringBuilder(len);    // error unsupport capacity
			StringBuilder sb = new StringBuilder();

			for (int i = 0; i < len; i++)
			{
				char c = s[i];
				if ((c >= 'A') && (c <= 'Z')){
					c = (char)((int)c + 32);
				}
				sb.Append(c);
			}
			return sb.ToString();
		}
	}
....

StringBuilder Class は用意されていますが、StringBuilder.capacityが未実装で、変換後にエラーが出ました。
github も2年前に更新となっているので、長らくプロジェクトは停滞しているみたいです。

まとめ

duocodeに比べるとTypeScriptで出力されるのがとても良い感じなのですが、プロジェクトとしてはバージャンアップが望めないみたいなので他を当たるのが良さそうです。

duocode - C# to JS

今までキワモノだぁ!と思っていたdocodeをちょっと試してみたので、忘れないように書き残しておきます。
主にマルチプラットフォームに耐えうるか調べてみます。

  • duocode

http://duoco.de/
doccode それはC#JavaScriptに変換してくれる素敵なツール。VSコンパイラの機能で行っているようです。
30日間無料があるのでそれで使用感をつかみたいと思います。

インストール

インストールは公式にそって大まかに進めてください。
ただし、以下の2点が大まかな説明からは抜けているので、個別にインストールしないとコンパイルできません。。

  • iis express のインストール

https://www.microsoft.com/ja-JP/download/details.aspx?id=48264
webアプリのプロジェクト作成に必要になります。もちろんiisが入っていれば不要でしょう。

  • node watcher

nodeアプリのデバッグに必要なようです。
実行時に「無いのでこのコマンドでインストールしてね」と出るので省略します。
自分の環境では壮大にインストールエラーになりましたが、その後デバッグ可能になりました。コワイです。きっと機能不完全だと思います。
今回は使わないのでパスします。

プロジェクト

大きく分けて3種類のプロジェクトが選択可能になります。

  1. webアプリ(html)
  2. クラスモジュール
  3. nodeJSアプリ

感触をつかむには最初はwebアプリがいいでしょう。

webアプリデバッグの注意点

さっそく売りの1つのVS上でのデバッグを試してみたくなりますよね。
サンプルにも「ここにブレークポイントを置いてみよう!」なんてコメントもあります。わくわくしますね。
設置して実行すると、あ…豪快に通過しますね…ダメじゃないですか。


理由はリモートターゲット(webなのでブラウザ)によって対応してないみたいです。
そんな…Edgeちゃんここでもダメなの?IEだとちゃんと止まるようですね。しかしめっちゃ重い……。
Edgeの場合はブラウザ上でデバッグすれば問題ありませんでした。吐き出されたJSにSourceMapコードがついてるので、対応ブラウザでちゃんと元のC#ファイルの箇所を示してくれるようです。
うん。

クラスモジュールの作成

慣れてきた(?)ところで、最小構成とマルチプラットフォームの程度が知りたいのでクラスモジュールを試します。
この機能はC#の一連のクラスモジュールをそのままJSのモジュールに変換します。デフォルトでは自動的に一個のファイルの形に結合して落とし込んでくれます。
新規プロジェクトを選ぶと、空のクラスが作成されます。適当に書いて弄ってコンパイル
script\ フォルダに返還後のJSファイルなどが出力されます。

ClassLibrary1.dll     // C#クラスモジュール
ClassLibrary1.js      // 出力JS
ClassLibrary1.js.map  // JS SourcaMap debug
ClassLibrary1.pdb     // VS SourceMap debug
mscorlib.d.ts         // type script 定義ファイル
mscorlib.js           // .net 基礎ライブラリのJS版
mscorlib.min.js       // mscorlib.js最適化版。改行と空行を無くしたもの

mscorlib と一緒に出力・使用されるのが duocode の特徴です。
動作させるには mscorlib.js/mscorlib.min.js と ClassLibrary1.js の2つを使用します。
webや組み込みであれば

<script src="mscorlib.js" />
<script src="ClassLibrary1.js" />

という感じで2つのソースを連結すれば動作します。
またnodeJSなら

require("mscorlib.js");

を ClassLibrary1.js の先頭に追加すれば動作します。


なお、変換されたJS(ClassLibrary1.js)はこんな感じになっております。

(function ClassLibrary1() {
"use strict";
var $asm = {
    fullName: "ClassLibrary1",
    anonymousTypes: [],
    types: [],
    getAttrs: function() { return [new System.Reflection.AssemblyTitleAttribute.ctor("ClassLibrary1"), new System.Reflection.AssemblyDescriptionAttribute.ctor(""), new System.Reflection.AssemblyConfigurationAttribute.ctor(""), new System.Reflection.AssemblyCompanyAttribute.ctor(""), new System.Reflection.AssemblyProductAttribute.ctor("ClassLibrary1"), new System.Reflection.AssemblyCopyrightAttribute.ctor("Copyright \xA9  2017"), new System.Reflection.AssemblyTrademarkAttribute.ctor(""), new System.Reflection.AssemblyCultureAttribute.ctor(""), new System.Reflection.AssemblyVersionAttribute.ctor("1.0.0.0"), new System.Reflection.AssemblyFileVersionAttribute.ctor("1.0.0.0"), new DuoCode.Runtime.CompilerAttribute.ctor("3.0.1654.0")]; }
};
var $g = (typeof(global) !== "undefined" ? global : (typeof(window) !== "undefined" ? window : self));
var ClassLibrary1 = $g.ClassLibrary1 = $g.ClassLibrary1 || {};
var $d = DuoCode.Runtime;
$d.$assemblies["ClassLibrary1"] = $asm;
ClassLibrary1.Class1 = $d.declare("ClassLibrary1.Class1", 0, $asm);
$d.define(ClassLibrary1.Class1, null, function($t, $p) {
    $t.ctor = function Class1() {
        $t.$baseType.ctor.call(this);
        console.log("constructor :)");
    };
});
return $asm;
})();
//# sourceMappingURL=ClassLibrary1.js.map

TypeScriptの出力と決定的に違うのはすべてが mscorlib に依存したコードで出力されることです。細かい部分も.netをエミュレートしているような印象が強いです。よく見る.ctorなんかconstrucotrですね。
このソースを保守管理するの現実的なレベルではありません。ソースマップがあるとはいえ実行時エラーはかなり厳しい宇宙言語的な内容を返すでしょう。
デバッグや開発は基本的にC#で完結する環境を作って行い、多言語との受け渡しとしてDLLみたいな(もう中身は弄らないぞ!)感じで扱うなら問題無さそうです。とはいえ、DLLに比べれば追跡もエラー箇所特定も初期状態で恵まれた環境なのは確かです。

mscorlib.jsを許せるかどうか

このファイルの存在が duocode を使用した際の最大の課題点です。
.netの基礎部分を記したもので、duocodeで変換を行う限りこれに依存します。クラスの根幹部分もこれに依存しているため事実上分離は不可能です。サイズが mscorlib.js(864K) / mscorlib.min.js(471K)と結構あります。今後も増えるでしょう。容量と初回起動時のコストで評価が分かれると思います。個人的には規模の割に十分軽いと思いますが。
また、外部から使用する際は.netエミュレータの方言があるので、一層自前インターフェイスクラスで包んであげたりする必要があるでしょう。ただし、TypeScriptを使用するのであればこの辺の問題はパス出来そうです(後記)。
吐き出されたコードによるオブジェクト(クラス)の展開は、ちゃんとグローバルに構造を展開してくれているので、プログラムや他のJSから容易に引っ張ってこれる点はとても気が利いています。グローバル汚染と言われるかも知れませんが…。

所感

C#移植とおもって使うと肩すかしを食らいますが、言語変換としてみればとてもよく出来てると思います。デバッグまで付いてきますし。
単なるwebページに使うにはちょっと大がかりかなーというのは確かですが、C#におけるPCL(ポータブル・クラス・ライブラリ)の一種として使用するなら十分に運用の可能性があると思われます。
C#上で環境を構築、テストを行い、その一部モジュール(ただし環境依存性が無いこと)を色んなところで使いたいなら協力だと思います。
サンプルのような1からwebアプリを作るのは他の色んなJSライブラリを入れることを考えると……ちょっと現実的じゃない気がします。すべてC#で組む勢いならいいですが、結局既存のJSライブラリとの連携がどうにもならないので…。
nodeJSアプリについても同様の不安が残ります。データベースや各種HTTPのシェイクハンドなども言語依存性が高いですし…。


あくまでC#による一部資産を生かすための手段の1つ以上のことは望まないほうが幸せかな?と言った印象です。
おっと忘れてましたが、売りのVS上でのデバッグについては、思った以上に素直に動きませんでしたので残念な印象です。まだ産まれたばかりですし今後に期待しましょう。

おまけ、ChakraCoreで動かす場合

基本的には1つのファイルに結合すれば問題ないのですが、一点だけエラーが発生します。

var $g = (typeof(global) !== "undefined" ? global : (typeof(window) !== "undefined" ? window : self));

グローバルオブジェクトの取得で失敗します。検索する3つの変数がどれもデフォルトでは存在しません。まぁそうですよね。
global、windowsをプログラム側で用意してやれば動作します。selfでも大丈夫だと思いますが、他のオブジェクトのプロパティでも使われているのでどうかなーといったところです(参照優先順位的にだいじょうぶなのは確認済みです)

JsGetGlobalObject(globalref);
JsGetPropertyIdFromName("global",idref)
JsSetProperty(globalre,fidref,globalref); // (global).global = global

そろそろglobal参照を定義しませんか?JSさん…

おまけ、TypeScript出力

VSプロジェクト設定から選ぶことができま…あれ?declare?

/// <reference path="./mscorlib.d.ts" />

declare module ClassLibrary1 {
    // ClassLibrary1.Class1
    export interface Class1 extends System.Object {
    }
    export interface Class1TypeFunc extends TypeFunction {
        (): Class1TypeFunc;
        prototype: Class1;
        new (): Class1;
        ctor: { new (): Class1; };
    }
    var Class1: Class1TypeFunc;
}

TypeScriptの定義ファイル(d.ts)が出力されるだけでした。今まで通りJSも出力しとくから最後に勝手に繋げて使ってね!と言うことみたいです。
クラスに関する取り扱いはTypeScriptを通した方が断然楽だと思うので、変換したものにTypeScriptでインターフェイスを書いて一層噛ましてあげるとプログラムとの連携がスムーズになりそうです。全部プログラムで済まそうとするときっと大変です。

mscorlibの対応状況

Microsoftのバックアップもあるので大丈夫かと思うのですが、今のところStringBuilderなどの主要クラス、プロパティもちゃんと実装されておりました。
ただ、ローケルとかあのあたりは避けるべきでしょう。DateFormatあたりは調べておいた方が良さそうです。文字コード変換なども諦めて自前実装しましょう。と、このあたりはマルチプラットフォーム前提だとしょうがないですね。どの言語でも似たようなものです。

ChakaraCore + TypeScript

ChakraCoreに手を出したのは5割ぐらいはTypeScriptしてみたかったからです。
アプリ組み込みでどんな感じに楽できるのかやってみましょう。

TypeScriptインストール

TypeScriptは簡単に言ってしまえば動的言語を静的言語にして、ヒューマンエラーをコンパイラで出来るだけ検出させる仕組みです。
function型も引数による定義付けが(違う引数のfunction型を弾く)可能です。

  • TypeScript

https://www.typescriptlang.org/
nodeJSのバージョン管理ツール上で提供されていますので、npm経由で取得します。詳細は省略します。オフィシャルに書いてありますし。
nodeJsがまだの場合はインストールが必要です。

Editor

https://code.visualstudio.com/
対応エディタはいろいろありますが、とりあえずVSCodeでも使ってみましょう。デフォルトで入れた瞬間から対応していますし、リアルタイムにコード補間とエラーチェックまでしてくれます。どのあたりまで補完&エラー補助してくれるのかも試す意味で使ってみましょう。

コンパイル

まず最小構成を作ってコンパイルしてみます。

  • main.ts
function main(s:string):void{
    console.log(s);
}
main();

VSCodeのターミナルからTypeScriptコンパイラである「tsc」を使用し、JSを出力させます。

tsc main.ts
  • main.js
function main(s) {
    console.log(s);
}
main("hallo world :)");

ここまでが基本です。
日本語も使うと思うので保存の際は一応bomをつけておきましょう。
VSCodeだと右下の文字コードをクリックすると保存形式の案内が出ます。

組み込み関数の定義

プログラム(アプリ)側から関数を組み込んだ場合、そのような関数は定義されていませんよ?とエラーが発生します。
どういった関数を組み込んだかを定義する .d.ts ファイルの作成が必要になります。
専用の定義構文が必要なのですが、ts からコンパイラで自動生成してくれる機能があるので、それを使うと簡単です。

  • app.ts
// void imp_print(string s);
function imp_print(s:string){ }

ターミナルから -d オプションでコンパイル

tsc -d app.ts
  • app.d.ts
// void imp_print(string s);
declare function imp_print(s: string): void;

これを先ほどの main.ts に読み込ませます。

  • main.ts
/// <reference path="./app.d.ts" />
function main():void{
  imp_print("hello world :)");
}
main();

これで組み込み関数 imp_print が使用可能&インテリセンスが利きます。
VSCodeの場合は、なぜかもう一度コンソールで .d.ts を生成しなおさなくても、.ts があればそちらから情報をとってくるみたいです。
とはいえ、作業中は恩恵にあずかり、最後はちゃんと生成したほうがいいでしょう。

jsdoc(ドキュメントコメント)

jsdocコメントもつけちゃうと、.d.ts でもインテリセンスの際に説明文が出ます。

  • app.ts
/**
 * 組み込み。コンソール出力。
 * @param {string} s 文字列
 */
function imp_print(s:string):void{ }

ファイル分割と結合

早速難物がやってきました。
開発においてはモジュールは分割して表記、管理します。ただし、JavaScriptは今のところまだまだモジュール組み込みの環境が整っていないのもあり最終的に1つのファイルにまとめる必要があります。これはChakraCoreにおいても同様です。


単に結合すれば良いかというと大きく分けて二種類が存在します。
TypeScriptコンパイラが持つ単純な結合方法、commonJS形式(AMD形式など)によるrequire命令を使用する方法。
この2つの手法をミックスできれば問題では無いのですが、TypeScriptコンパイラはそれを許していません。また、2つそれぞれの方法で最終的に同じようなJSを出力しようとすると、ソースの表記が若干変わってくる点も問題です。コンパイラマクロでもあれば良かったのですが…。


というわけで、JSとしてもライブラリを出力したい!webとnodeJS両対応にしたいなども考え出すと、非常に頭が痛い問題です。

commonJS方式、require使用

この方式は最終的に require によるファイルのインポート命令に展開され、ファイルは結合されません。なので出力されるJSが元に近いので(この段階では)可読性があります。
また今後もファイルインポート周りで手が入る手はずなので将来性は保証されていると思います。
最大の欠点は、このままではChakraCoreが読み込んでくれないということです……。


まず、sub.ts を main.ts にインポートする構成を作成。

  • sub.ts
/// <reference path="./app.d.ts" />
export function sub_test():void{
    mp_print("sub test :)");
}
  • main.ts
// main.ts
/// <reference path="./app.d.ts" />
import * as sub from "./sub";
function main(s:string):void{
    imp_print(s);
    sub.sub_test();     //sub.ts
}
main("hallo world :)");

sub.ts は export を忘れずに。export が1つも無いと main.ts 側でエラーが出ます。最初なんだこれ?って思いました。
ではコンソールでコンパイルします。main.ts を指定するだけで関連するソースをすべてコンパイルしてくれます。ありがたい。

tsc main.ts -module commonjs
  • main.js
"use strict";
exports.__esModule = true;
/// <reference path="./app.d.ts" />
var sub = require("./sub");
function main(s) {
    imp_print(s);
    sub.sub_test(); //sub.ts
}
main("hallo world :)");

吐き出された main.js には require が挿入されています。が sub.js はこの段階では組み込まれていません。
また exports というのが追加されていますがこの子はモジュール単位の子でグローバルな情報としては使えません。


nodeJSならここで終わり。
JSエンジンにぶちこんで動かすには、これらを1つのファイルに結合しないといけません。
結合にはweb開発で主流な webpack とか browserify があります。

browserifyで結合

  • browserify

http://browserify.org/
何回書いても覚えられないつづりの browserify を試してみます。インストールはnpm任せです。
インストールの後、コンソールから結合します。

browserify main.js -o out.js
  • out.js
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
"use strict";
exports.__esModule = true;
/// <reference path="./app.d.ts" />
var sub = require("./sub");
function main(s) {
    imp_print(s);
    sub.sub_test(); //sub.ts
}
main("hallo world :)");

},{"./sub":2}],2:[function(require,module,exports){
"use strict";
/// <reference path="./app.d.ts" />
exports.__esModule = true;
function sub_test() {
    imp_print("sub test :)");
}
exports.sub_test = sub_test;

},{}]},{},[1]);

ごちゃごちゃっとしてますが、即時実行関数になって、独自 require 関数を定義の後、中のコードを実行してる感じです。ちゃんと動きます。
さて、これの問題は、main()などの位置がtsで書いたつもりの位置と微妙に異なることです。グローバルオブジェクト直下じゃありません。なので、プログラムから引いてくるのが困難なことです。
よく見ると「exports」というのが存在しますがこれもモジュール単位なので使えません。


結局なにも手を加えない状態ではアプリからは完全に孤立した空間になるので、アプリとの連携が絶望的です。
グローバルで値をやりとりするなら、JS側でグローバルを取得、そしてそこへ外に出したいものを手動で書くほかないかなと思います。
ただし、グローバルの取得はちょっと細工しないと環境依存の元なので、npmから取得するのが良いみたいです。

  • get-global

https://www.npmjs.com/package/get-global


もう一つは、グローバルを一切介さない手法にするのが、モダンかも知れません。
ユーザー専用の受け渡しテーブルを作ってやり取りするか(アプリ側が作成、またはJSで作成した場合はアプリ側へ通達)、またはアプリ側が呼ぶ取得用関数をJS側から通達するのが良いのかも知れません。

// entry call function from app
imp_entryappevent(app_oninitialize,"initialize");
imp_entryappevent(app_onupdate,"update");
...

webではクリーンな感じですが組み込みで使うには面倒な感じですね。

TypeScriptコンパイラで結合

次はTypeScriptコンパイラによる結合です。と言っても、もう使ってますが「/// 」になります。
require と違ってそのまま追加するだけなので、import のように指定の名前空間に変更することができません。
なので最終的に同じようなJSに展開させようとすると、ソース側に namespace{} を追記する必要があります。

  • sub.ts
/// <reference path="./app.d.ts" />
namespace sub {
    export function sub_test():void{
        imp_print("sub test :)");
    }
}
  • main.ts
/// <reference path="./app.d.ts" />
/// <reference path="./sub.ts" />
function main(s:string):void{
    imp_print(s);
    sub.sub_test();     //sub.ts
}
main("hallo world :)");

コンソールから--outFile を使って結合します。この時--module commonjsなどは使用できません。
import 構文などを使っているとエラーが発生します。

tsc main.ts -outFile out.js
  • out.js
/// <reference path="./app.d.ts" />
var sub;
(function (sub) {
    function sub_test() {
        imp_print("sub test :)");
    }
    sub.sub_test = sub_test;
})(sub || (sub = {}));
/// <reference path="./app.d.ts" />
/// <reference path="./sub.ts" />
function main(s) {
    imp_print(s);
    sub.sub_test(); //sub.ts
}
main("hallo world :)");

JSとして見るならかなりシンプルな展開です。ちゃんとグローバルオブジェクトに展開しています。逆に exports がなくなりました。
この時点で1つのファイルに結合されているので、ちょっとでも規模が大きくなると可読性は最低ですし、各ファイルのJSも出力されません。
プログラムから値引いてくる場合はこちらのほうがとても素直な印象です。

分割したい

手動でぶつ切り、後でプログラムでテキスト的に結合という手段がありますが、browserifyだともうちょっと賢い手法が使えます。
パラメータ -x で目的のモジュールを抜くことで分離が可能です。

browserify main.js -o out.js -x sub.js
  • main.js
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
"use strict";
//  application implement function 
/// <reference path="./imp.d.ts" />
exports.__esModule = true;
/// <reference path="./sub.ts" />
var sub = require("./sub");
var main;
(function (main_1) {
    main_1.g_value = "test code";
    function app_initialize() {
        imp_print("initialize");
    }
    function app_finalize() {
        imp_print("finalize");
    }
    function app_update() {
        imp_print("update");
    }
    function main() {
        sub.test("aaaaa");
        return "script run";
    }
    imp_function(app_initialize, "initialize");
    imp_function(app_finalize, "finalize");
    imp_function(app_update, "update");
    main();
})(main = exports.main || (exports.main = {}));

},{"./sub":"/sub.js"}]},{},[1]);

sub.ja 相当のコードが抜かれても、requireなどはちゃんと残ってます。
で、sub.jsを突っ込めば動くかというと動かなくて、require が独自定義というわけなのでsub.jsを準じた感じに変換しないといけないわけです。公開モジュールというわけだ。

browserify -r sub.js -o sub_r.js

エラー。ファイルがありません。うん????

browserify -r ./sub.js -o sub_r.js

ハメですかねこれ。ハメですね。
webであれば

<script src="sub_r.s" />
<script src="out.js" />

みたいな感じで動くようになるので、要するに out.js の最初にこのコードを付け加えれば動きます。


たまにエラーが出ました。
原因は不明ですが公開モジュールの名前が変になったことがあります。これだと sub_r.s の1行目の453列あたりに表記されていますので確認すると良いでしょう。
正解のケースでは「./sub.js」という名前になっていますが、エラーが出た場合は違う書式になっているかも知れません。

filename : out.js
error : ScriptError
description : Cannot find module '/sub.js'
message : Cannot find module '/sub.js'

とエラーに要求名が書かれるのでそれに合わせれば解決できるでしょう。
「:」で強引にモジュール名を変えることも可能です。

browserify -r ./sub.js:./hogehoge.js -o sub_r.js

整合性がとれなくなるので、使うことはあんまり無いとは思いますけど。

所感

TypeScriptを使えばJSの面倒なところも全部見てくれるぜ!って訳でもなく、良くも悪くもしっかりJSの上に乗っかってる感があります。
個人的にはどうにかcommonJS型(モジュール形式でファイル分割)で開発したいのですが、nodeJS以外のマルチプラットフォームも考慮するのであればTypeScritコンパイルで結合して1つのファイルにする方が無難かな……というところで落ち着きました。
散々browseなんとかの説明までしておいて申し訳ないです。いつか役に立ちますって…たぶん:Q