Bakulog

獏の夢日記的な何か。

Baku.LibqiDotNetの互換性が一か月で消えた話

バージョンアップ報告的なアレです。

   

タイトルにも載っていますが、本記事は.NET用からAldebaran社のロボット(NaoとかPepper)を制御する目的で作ったラッパーライブラリであるBaku.LibqiDotNetに関連する記事です。後方互換性が無くなるアップデートをしつつ、情報整備やら何やらしてます、という雑多な進捗を本記事で紹介します。

 

もくじ

  1. ドキュメンテーションが整備されるらしい
  2. メソッド呼び出しを自然にしたかった
  3. その他細かいこと
  4. まとめ

   

ドキュメンテーションが整備されるらしい

これまで、Baku.LibqiDotNetに関する情報はだいたい3か所にまとまってました。

 

GitHubのコードにはサンプルも入っていますし、XMLドキュメントコメントも書ける場所にはすべて書いていたので、慣れている人はこれだけでも使えると思っています。しかし逆に言うと、C#に結構慣れてないと使えないライブラリになっているわけで、これはあまり良い話ではありません。

 

そこで今回、GitHubに2つページを上げてみました。上のほうは整備がまだあまり出来ていません。

 

上の2サイトは別々の目的で作っています。第一にチュートリアル側はMkDocsで記事をガリガリ書いて作っています。こっちにはチュートリアルならではの情報を載せていますし、今後も記事とかFAQを追加していく予定です。

 

いっぽうクラス構成はSHFB(Sandcastle Help File Builder)の出力をそのまま持ってきています。SHFBはC#XMLドキュメントコメントからヘルプ用のウェブページを吐ける非常に便利なツールなのですが、私の使い方ではXMLドキュメントコメントと等価な情報だけが入るようにしているので、そこまで目新しいネタは転がってません。更新もライブラリ自体の更新時以外は行わない予定です。

   

メソッド呼び出しを自然にしたかった

ドキュメンテーションの話は切り上げ、本題であるライブラリ更新の話題に移ります。本節ではその中でも重要な、後方互換性がなくなった部分の話をします。

 

Baku.LibqiDotNetのver1.0.1時点でqi Frameworkのサービスからメソッドを呼び出す構文はこんな感じでした。

ver session = QiSession.Create("tcp://pepper.local:9559");
var qiObj = session.GetService("HogeService");

int res = qiObj.Call("hogeMethod", new QiString("foo"), new QiInt32(1)).GetInt32();

 

この呼び出し方式はラップ元であるlibqi(というかlibqi-capi)の薄いラッパーとしては自然な実装でしたが、以下のような問題がありました。

  1. メソッド名と引数を同じ関数内に並べて書いてるので何か気持ち悪い
  2. メソッド引数にQiAnyValueの派生型を指定させるので構文がゴツくなる
  3. 戻り値はQiValue型変数なので変換処理が必要

 

このような問題を踏まえてインターフェースを改善しました。

//旧
int res = qiObj.Call("hogeMethod", new QiString("foo"), new QiInt32(1)).GetInt32();

//新
int res = (int)qiObj["hogeMethod"].Call("foo", 1);

 

第一の変更としてメソッド名をキーとして指定し、Callを呼ぶスタイルを採用しました。この方式だとメソッド名だけ特別扱いなのが丸わかりなため、見た目に健全です(実はこの変更で速度も上がってます)。

 

第二の変更として、Call関数の中身にQiHoge型の変数だけでなく、組み込み型と一部の配列も渡せるようになりました。このように書けるのは引数の型であるQiAnyValueに暗黙キャストが実装されたためです。たとえば文字列からのキャストは次のような構成でサポートされています。

//QiMethodはメソッド名でキー指定して得られる戻り値のクラス
//例:
//QiMethod method = tts["say"];
public class QiMethod
{
    public QiValue Call(params QiAnyValue[] args) 
    {
        //..
    }
    //..
}

public abstract class QiAnyValue
{
    public static implicit operator QiAnyValue(string s) => new QiString(s);

    //..
}

//実際に文字列を保持する側
public class QiString : QiAnyValue
{
    internal QiString(string s) { /* .. */ }

    //..
}

 

このキャスト実装によって、例えば次の関数呼び出し

var tts = session.GetService("ALTextToSpeech");
tts["say"].Call("Hello, World!");

これが下記と同じように扱われます。

var tts = session.GetService("ALTextToSpeech");
tts["say"].Call(new QiString("Hello, World!"));

 

つまり、引数はQiAnyValueの派生型引数かライブラリ側で暗黙キャストを認めたものだけ渡せます。この設計で型の安全性と融通が両立されています(たぶん)。ちなみに組み込み型だけでなくバイナリデータ(byte[])と頻繁に使いそうな配列3種類(int[], double[], string[])についても暗黙キャストをサポートしています。

 

関数の戻り値であるQiValueでも明示キャストを実装しました。こちらについてはキャストを使わず"To"系の関数でも変換が出来ます。なお細かい変更ですが、型変換の関数名は旧版で"Get"だったのが新版では"To*"に変更されているので気を付けて下さい。

たとえば整数へのキャストは"ToInt32()"関数とintの明示キャストの2通りでサポートされています。

//旧版
public class QiValue
{
    public int GetInt32() { /* .. */ }
    //...
}

//新版
public class QiValue
{
    public int ToInt32() { /* .. */ }
    public static explicit operator int(QiValue qv) => qv.ToInt32();
    //...
}

 

キャストと変換関数の使い分けはお好みでやってください。

//こう書ける
int res = (int)qiObj["getSomeNumber"].Call();
//これでもOK
int res = qiObj["getSomeNumber"].Call().ToInt32();

   

その他細かいこと

バグ修正

旧版のBaku.LibqiDotNetでは以下の条件が重なるとQiObject.Callによる関数呼び出しに失敗するという問題がありました。

  • 関数のオーバーロードが2つ以上ある
  • 実引数として辞書型(QiMap)やタプル型(QiTuple)を渡す

 

この条件を満たす関数は滅多に無いのですが、例えばモーションつきで発話する便利関数のALAnimatedSpeech::sayなどがこれに該当し、地味に手痛い問題になっていました。古い方のライブラリでもこの問題は回避できるんですが、若干めんどくさいコードが必要になります。

 

この問題は実装ミスに由来していたので、新しいバージョンでは修正済みです1

  

小さい変更

以下のようなことをこっそりやってます。

  • クラス定義のうちsealed宣言したほうがよさそうなものをsealed
  • アンマネージライブラリの探索フォルダを追加する機能を提供(Baku.LibqiDotNet.Path名前空間)
  • QiObjectBuilder.AdvertiseMethodの支援として関数引数のシグネチャを作るヘルパークラスを追加


ラストのQiObjectBuilderに関してはテストの域を出ていないので、ご利用の際は十分ご注意ください。

   

まとめ

互換性のマイナス以外は色々いい感じになってきてますよ、という話でした。使ってくださってる方には申し訳ないのですが、私はポリシーとして後方互換性は気にすると疲れるので基本無視という方針を持っています。今後も大きなバージョンアップは後方互換なしでやる可能性が高いので、ご利用に際しては十分お気を付けください。

 


  1. オーバーロード解決まわりの処理は現状でも不安定なので、不具合等あればお早めにご連絡下さい。