CGOでビルドしたライブラリをXamarin.Formsなどで利用してみる

  • 8
    いいね
  • 0
    コメント

この記事は[学生さん・初心者さん大歓迎!]Xamarin Advent Calendar 2016の10日目の記事です.

また本記事はC90で あおばやまどっとねっと の「アオバヤマノススメ vol.1」に寄稿した記事およびリポジトリを一部流用しています.

はじめに

モバイルでのクロスプラットフォーム開発では現在非常に勢いのあるXamarin.これを使うことによりAndroidやiOS,またWindows Phone(最近ではTizenとかも...)のアプリケーションをほぼ共通化しアプリ作成が行えるようになりました.
しかし実際のサービスはモバイルアプリで完結するということはなく,デスクトップ向けだったり,サーバーサイドのアプリケーションがあって成り立つということが多いです.
そうなると本当の意味でどのプラットフォーム,どの層にも共通化したいという欲求が出てきます.

FFIが提供されている言語であれば共通化は容易に出来そうで,それから呼び出すと言ったら,そうC/C++言語ですよね!じゃあこれからC/C++で開発しよう!となるとなかなか厳しいものがあります.

そんな厳しい状況を打破してくれるものはないものか,と考えると,そういえばGo言語ってC用のライブラリをビルドできなかったっけ...?という考えに至るわけです.

ということで,今回はGo言語を使ってC用のライブラリをビルドして,デスクトップにもモバイルにも対応するライブラリにして,それをXamarinから呼んでみるということに挑戦してみます.

今回使用するGo言語のライブラリ

はじめにデスクトップ向けにも作るとか言っておきながら,今回はGo Mobileというライブラリを使用します.実際はそれとCGOを組み合わせて使うからMobileオンリーというわけではないのですが,(こういう記事書いたり実際に使っていながら)そこまで詳しくないので仕組みについては省略...

Go Mobileを使うと何ができるのかと言うと,
* AndroidアプリやiOSアプリをGo言語で書ける
* Android向けにJavaバインディングしたaarファイルや,iOS向けにframeworkファイルをGo言語で書いたものを元に生成できる
みたいなのが出来ます.

ですが,今回はこの両方を使用いたしません(語弊があるけれどもaarとかframeworkとかには落とし込まない).

じゃあ結局何がしたい,何をするんだという話ですが,CGOを活用して,Android向けにはsoファイルを生成して,iOS向けにはaファイルを生成してDllImportを使ってネイティブコードを呼び出すということをします.Go Mobileはビルド環境を整えるために使います.

実際に作ってみる

今回は実用性を無視して,いわゆるHello World的なものを作ってみようと思います.
なおGo言語の実行環境やGo Mobileのインストールは完了しているものと仮定して進めます.

写経でHello Worldっぽいのを

まずはシンプルにHello Worldみたいに,何かしらの文字を出力するGo言語のライブラリを作ってみます.サンプルとしてはこんな感じ

package myfirstlibrary

import "fmt"

func MyHello(user string) string {
    return fmt.Sprintf("Hello, %s!", user)
}

何かしらの文字を入れたらHelloを付与して返してくれる関数ができました.しかしこれだけではCGOを使ってC用のライブラリに変換することはできません.これをGo言語で更にラップします.

まずは例をどうぞ(ファイル名はcwrap.goにでもしてみましょうか)

package main

import "C"
import "myfirstlibrary"

//export myCLibrary
func myCLibrary(user *C.char) *C.char {
    go_str := myfirstlibrary.MyHello(C.GoString(user))
    return C.CString(go_str)
}

func init () {}

func main () {}

上記のコードで行っていることですが,文字列,すなわちC言語でいうChar型の配列を引数に取り,その引数に対しC.GoStringという関数でGo言語で使用するstring型に変換を行っています.こうすることでまずはC言語 -> Go言語の領域での変換が可能となりました.
Go言語で使用するstring型の変数ができたため,これを先ほど作ったHelloを付与して文字列を返す関数に与えています.
返ってくるのはもちろんGo言語でのstring型です.しかしこれはC言語の領域に持っていくことは出来ないため,C.CString関数を通してC言語でのstring型(Char型の配列)に変換しています.

myCLibraryは上記の説明だけで事足りるかと思うのですが,その周りは先ほどと様子が違います.そうこれは先ほどから何度か出ているCGOを使うためのルールに沿ったものとなっているのです.さて,そのCGOのルールですが,

  • CGOをでライブラリを生成するにはpackage名はmainでなければならない
  • CGOをつかうために import "C" でCGOを使うことを明記
  • エントリーポイントとしてのmainを用意する(実際には呼ばれず,ロード時に呼ばれるのはinit)
  • 外部に公開するAPIはGo言語で記述した関数の上にコメントで //export 関数名 を記述

と言ったものです.

面倒なビルド作業

さて,ここからは実際に上で作ったソースをビルドしてみましょう.
ビルドは結構オプションを書かないといけないので,とりあえず下のものをコピペで...

Android

CGO_ENABLED=1 \
CC="${GOPATH}/pkg/gomobile/android-ndk-r12b/llvm/bin/clang" \
GOOS=android \
GOARCH=arm \
GOARM=7 \
CGO_CFLAGS="--sysroot=${GOPATH}/pkg/gomobile/android-ndk-r12b/arm/sysroot" \
CGO_LDFLAGS="--sysroot=${GOPATH}/pkg/gomobile/android-ndk-r12b/arm/sysroot" \
go build -buildmode=c-shared -pkgdir=${GOPATH}/gomobile/pkg_android_arm \
-o libs/android/armeabi-v7a/libmyhello.so cwrap.go

適宜ビルドするファイルのファイル名だったり,対象アーキテクチャの変更を行ってください.
これを叩くことによりAndroidのarmアーキテクチャ向けのshared libraryがビルドされます.nmとかで眺めてみると先ほどexportしたシンボルが見つかると思います.

続いてiOSです.これはもちろんMacが必要となります,Windowsの方ごめんなさい.

CGO_ENABLED=1 \
GOOS=darwin \
GOARCH=arm \
GOARM=7 \
CC=`xcrun --sdk iphoneos -f clang` \
CXX=`xcrun --sdk iphoneos -f clang` \
CGO_CFLAGS="-isysroot `xcrun --sdk iphoneos --show-sdk-path` \
-arch armv7 -miphoneos-version-min=8.0" \
CGO_LDFLAGS="-isysroot `xcrun --sdk iphoneos --show-sdk-path` \
-arch armv7 -miphoneos-version-min=8.0" \
go build -pkgdir=$GOPATH/pkg/gomobile/pkg_darwin_arm -buildmode=c-archive \
-tags=ios -o libs/ios/armv7/libmyhello.a cwrap.go

普通iOSアプリとかってxcode使うのでこのあたりのコマンドを探るのが本当に難儀しましたが,なんとかこれで動いたので大丈夫そうです(不安)一つ不安なのがアーキテクチャ周りで,最近のiPhoneとかこれで大丈夫なのかとか検証できてません,ダメだった場合アーキテクチャの変更などをしてビルドしてみてください.このあたりはCGOというよりも,iOS向けのライブラリをコマンドラインでどうやってビルドするかという感じなので専門の方に投げましょう(コラ

とりあえずシミュレータで試したいという方は

CGO_ENABLED=1 \
GOOS=darwin \
GOARCH=amd64 \
CC=`xcrun --sdk iphoneos -f clang` \
CXX=`xcrun --sdk iphoneos -f clang` \
CGO_CFLAGS="-isysroot `xcrun --sdk iphonesimulator --show-sdk-path` \
    -arch x86_64 -miphoneos-version-min=8.0" \
CGO_LDFLAGS="-isysroot `xcrun --sdk iphonesimulator --show-sdk-path` \
    -arch x86_64 -miphoneos-version-min=8.0" \
go build -pkgdir=$GOPATH/pkg/gomobile/pkg_darwin_amd64 -buildmode=c-archive \
    -tags=ios -o libs/ios/amd64/libmyhello.a cwrap.go

でどうぞ.

ビルドしたライブラリを使ってみよう

デスクトップ向けに試す

さて,これ本当にビルド出来てるの?という不安もあるでしょう,そういう時はこれをデスクトップ環境で同様に試してみればいいのです.

CGO_ENABLED=1 \
go build -buildmode=c-shared -o libmyhello.so cwrap.go

デスクトップ環境向けのビルドはオプションがすっきりしていていいですね!soファイルにしてしまえばPythonなどから試すことができるのでこれで試してみましょう.Windowsは...ごめんなさい,まだ完全対応できていないようです -> cmd/go: -buildmode=c-shared should work on windows #11058

Pythonでの実行例はこんな感じ

import ctypes
lib = ctypes.CDLL('./libmyhello.so')
ccall = lib.myCLibrary
# デフォルトではint値が返ってくるので,型の指定 
ccall.restype = ctypes.c_char_p
# Python2系
print ccall('Nico')
# Python3系
# print(ccall('Nico'.encode('ascii')).decode('utf-8'))

これでHello, Nico!と返ってくれば成功です,安心してXamarin向けにラップできます.

本命のXamarin/C# 向けにラップ

それでは上記の方法でビルド出来た各プラットフォーム向けのライブラリをラップしていきましょう.
DllImportとか思いっきりネイティブ触りそうなコードなのでSharedの方が楽かなーとも思いましたが,DependencyServiceとか使えばそこまで苦でもないのでPCLで作ってみます.

まずはDependencyServiceを使うためにインターフェースを共通部に生やして

using System;
namespace hogehoge
{
    public interface IGoLibrary
    {
        string CallMyGoLibrary(string user);
    }
}

AndroidやiOSのところに実装したものを生やす

using System;
using System.Runtime.InteropServices;
using Xamarin.Forms;
using hogehoge;
using hogehoge.Droid;
[assembly: Dependency(typeof(GoLibrary))]
namespace hogehoge.Droid
{
    public class GoLibrary: IGoLibrary
    {
        [DllImport("libmyhello.so", CharSet = CharSet.Auto,
                   CallingConvention = CallingConvention.Cdecl)]
        private static extern IntPtr myCLibrary(string user);

        public string CallMyGoLibrary(string user)
        {
            IntPtr pStr = myCLibrary(user);
            var str = Marshal.PtrToStringAuto(pStr);
            Marshal.FreeHGlobal(pStr);
            return str;
        }
    }
}

基本的にiOSとAndroidはネームスペース以外変わらないので省略しますが,iOSの場合ライブラリはstatic linkさせるため,

[DllImport("libmyhello.so", CharSet = CharSet.Auto,
                   CallingConvention = CallingConvention.Cdecl)]
を
[DllImport("__Internal", CharSet = CharSet.Auto,
                   CallingConvention = CallingConvention.Cdecl)]

と記述します.

ライブラリをリンクさせる

Android編

CGOでライブラリを作った時のディレクトリ構造を覚えているでしょうか.

libs
 |_ android
       |_ armeabi-v7a
              |_ libmyhello.so

みたいになっていると思います.XamarinでAndroid向けのライブラリをリンクする時もこんな感じの形で,

Droidプロジェクトのルート
       |_ libs
           |_ armeabi-v7a
                  |_ libmyhello.so

みたいな感じで自作のライブラリを設置します.自分で設定を書けばどこでも良さそう?ですが,JavaでAndroidアプリを開発する時の様式に習った方がわかりやすそうです.またディレクトリに設置した後にコンテキストメニューを出して,BuildActionをAndroidNativeLibraryに設定しておきましょう.

iOS編

何も気にせずにiOSのプロジェクトのコンテキストメニューを表示し,Add Native Referenceで追加すれば完了です.

おわりに

めちゃくちゃ駆け足で説明が足りないよ!そもそも図がないよ!!とツッコミが飛んできそうではありそうな感じではありますが,最低限Go言語で作ったライブラリをXamarinで利用する手順をお伝えしました.
途中でPythonを使ってGoで作ったライブラリを試していたように,モバイル向けだけではなく,デスクトップ向けにも同時にライブラリを作ることができます.実際これが役に立つ場面は少ない...と思いますが,一つの共通化の手段として覚えてくだされば幸いです.

今となっては.NET Coreがあるからそれ使えばいいじゃん!っていうこともありそうですが(ネイティブからそのコードとか呼べるようになったら教えて)

また今回は省略に省略を重ねて他のファイルやディレクトリの構造がわかりづらくなっています.過去のリポジトリの流用で内容が多少違いますが,

などを参考にして手順をトレースしていただければコードを書くことなく試すことができますので,ご覧になってみてください.