will and way

ただの自分用メモを人に伝える形式で書くことでわかりやすくまとめてるはずのブログ

Cocos2d-xにおけるOS依存機能利用のための実装

この記事はqiita advent calendar cocos2d-x 2015の22日目の記事です。 qiita.com

C++完結はできない

Cocos2d-xでは主にクロスプラットフォームな開発を期待して選ぶ方が多いと思います。

しかし、プラットフォーム依存な所、例えば文字の入力課金OS情報ユーザー情報へのアクセスをする場合にはそのプラットフォームで提供されているSDKを書く必要が出てきます。

また、Cocos2d-xで書くよりもOS依存のSDKの力を借りてネイティブで書いたほうがパフォーマンス向上に苦労しないケース(スクロールビューやGridView, CollectionViewなど)も少なくない上に、ユーザーにとっても使い慣れたUIを提供することができるというメリットもあります。

iOSの場合はObjective-CC++への互換があるため、Objective-C++として書けばほとんど苦労せずにプラットフォーム依存な部分にアクセス可能ですが、Androidの場合はJavaでAndroidSDKをつかって書く必要がありますね。

今回はこのAndroidSDKをC++からどう使うか?そして、開発を加速させるためにネイティブを使う側(C++)からJavaObjective-Cも同じインタフェースで呼べるようにするための設計をまとめていきます。

JNI

まずはC++からJavaを呼び出すにはJNIという知識が必要になります。一言で言えばJavaと他の言語をブリッジする機能です。

例と簡単な説明は以下に挙げますが、詳しい説明はOracleのJNIのリファレンスを読んでください。

Calc.java

package jp.matsuokah;

class Calc {
  public static int add(int x, int y)
  {
    return x + y;
  }
}

上記のaddC++から呼ぼうとするにはいくつかのステップが必要です。 一番簡単なパターンで説明します。

Cocos2d-xで用意されているJNI関連のユーティリティを使った場合

struct calc
{
  int add(int x, int y)
  {
    cocos2d::JniMethodInfo methodInfo;
    if (!cocos2d::JniHelper::getStaticMethodInfo(methodInfo, "jp/matsuokah/Calc", "add", "(I)I"))
    {
       throw std::runtime_error{"method couldn't found"};
    }
    auto result = methodInfo.env->CallStaticIntMethod(methodInfo.classID, methodInfo.methodID);
    methodInfo.env->DeleteLocalRef(methodInfo.classID);
    return result;
  }
}

順に流れを説明すると

  1. 文字列で定義したクラス名、メソッド名、メソッドのシグネチャを手がかりにstatic methodの情報を取得
  2. なければ例外を投げる
  3. メソッドをコールする
  4. JNI関連のオブジェクトを開放する
  5. 返却
  1. 文字列で定義したクラス名、メソッド名、メソッドのシグネチャを手がかりにstatic methodの情報を取得

これだけで、なんじゃこりゃ状態ですよね。
特にメソッドの情報を取得するところや、オブジェクトの開放は書くたびに頭を悩ませてくれそうです。

シグネチャが変わっても文字列なので、ランタイムでエラーになるまで気づかないかもしれません。

しかし、クラス名やメソッド名の検出はできないものの、シグネチャはテンプレート引数から自動生成、オブジェクトの開放はstd::unique_ptrを使えば意識しなくて済みそうです。

ということで、業務ではcocos2d-xのJniHelperを置き換えるライブラリを開発しました。(公開はできませんが擬似コードで方針を幾つか書きたいと思います)

今では例えば、WebViewを使う際には

void WebViewRef::load(const std::string& uri)
{
  auto env = get_env();
  jni::call_method<void>(env, this->ref_.get(), webview::load, uri);
}

上記はuriをロードする場合のメソッドです。リターン値、メソッド名、メソッドの引数を指定するだけで済むようになったのでJavaのメソッド追加やシグネチャの追加がとても楽になりました。

ブリッジ部は他のOSも絡むので以下にiOSAndroidを含めたクラススタックを載せ、概要を説明した後で、詳細を説明します。

iOS/Android共通WebViewの全体像

f:id:matsuokah:20151222224859p:plain

共通インタフェース

  • void* native_ptrとしてAndroidはRef、iOSはControllerのポインタをもつ
  • ネイティブから伝搬する各イベントハンドラをセットできるようにしている
  • 元の共通インタフェースのthisポインタをRef及びControllerに渡すことで、ネイティブからイベントハンドラを発火する

共通インタフェースの各翻訳単位の実装

  • void* native_ptrを各翻訳単位でキャストしてメソッドをコールする

Refクラス(Androidのみ)

AndroidのJNI関連のラッパークラス。JNIオブジェクトの確保/開放、メソッドやクラス情報の取得をテンプレート実装したもの

ネイティブ実装

  • 上位のレイヤからのメソッドを実行するコントローラであったり、Viewであったり。
  • 窓口となるクラスでは共通インタフェースの生ポインタをコンストラクタで受け取り、イベントハンドラを発火させる

Androidの場合の擬似コード

WebView.cpp

using ref_t = WebViewRef;

WebView::WebView() ptr_:{new WebViewRef{this}} {}
WebView::~WebView() {delete ptr_;}
void WebView::load(const std::string& url)
{
  static_cast<ref_t*>(this->ptr_)->load(url);
}

bool WebView::isLoading()
{
  return static_cast<ref_t*>(this->ptr_)->isLoading();
}

WebViewRef.hpp

#include ...

namespace android
{
class WebViewRef
{
  ref_wrapper ref_; // ref_wrapperはjavaオブジェクト用のカスタムデリータを定義したunique_ptrのエイリアス
  WebViewRef(const void* ptr);
  ~WebViewRef();
  void load(const std::string& uri);
  bool isLoading();
}
}

JNIEXPORT void JNICALL Java_jp_matsuoka_

WebViewRef.hpp

#include ...
namespace android
{

// シグネチャ関連
struct webview
{
  // qualified_nameはクラス名
  static constexpr auto qualified_name = "jp/matsuokah/Webview";
  // 以下メソッド名
  static constexpr auto load = "load";
  static constexpr auto isLoading = "isLoading";
  .
  .
  .
}

WebViewRef::WebViewRef(const void* ptr)
{
  auto env = get_env();
  this->ref_ = make_ref(env, make_object<webview>(env, reinterpret_cast<std::int64_t>(ptr)));
}

WebViewRef::~WebViewRef()
{
  this->ref_ = nullptr;
}

void WebViewRef::load(const std::string& uri)
{
  auto env = get_env();
  jni::call_method<void>(env, this->ref_.get(), webview::load, uri);
}

bool WebViewRef::isLoading()
{
  auto env = get_env();
  return jni::call_method<bool>(env, this->ref_.get(), webview::isLoading);
}

// 他のメソッドも同様に定義する
.
.
.

}

WebView.java

package jp.matsuokah;

import ...

class WebView {
  private long ptr;  
  private native void onLoad(long ptr);

  WebView(long ptr) {
     this.ptr = ptr;
  }

  void load(String uri) {
  } 

  boolean isLoading() {
    return true; //スタブ実装
  }
}

WebViewRef.cppではjni関連のライブラリをフル活用しており、実際にコールするところの実装は2行で済んでいます。

と、AndroidではこのようにネイティブとCocosの共通的なブリッジ部を開発しておりました。iOSC++の感覚で実装できるので紹介は省きます。

最後はほぼ擬似コードにはなってしまいましたが 共通インタフェースで各プラットフォームのAPIをコールできそうな雰囲気は感じ取っていただけたかと思います。

このようにフレームワークっぽくしておくと、追加実装になった時もそれに準じて書くだけで、ブリッジしていけるので個人的には開発効率化ができたなーと感じています。

書いていて気付きましたが、pimplパターンを使えば、共通インタフェースをもう少し上手く書けそうですね。