読者です 読者をやめる 読者になる 読者になる

SIN@SAPPOROWORKSの覚書

C#を中心に、夜な夜な試行錯誤したコードの記録です。

Windows Bridge for iOS 〜Objective-C(Xcode)で作成したプロジェクトを変換してみた〜

今日は、Objective-Cで作成たプロジェクトをWindows用のプロジェクトに変換してみました。
↓WinObjCなど、基本的な事については、昨日の記事をご参照ください。furuya02.hatenablog.com

1 vsimporter.exe

WinObjCに含まれるvsimporter.exeを使用することで、Objective-CのプロジェクトをWindows用のプロジェクトに変換できます。
vsimporter.exeの使用方法は、-help スイッチで確認できます。

c:\winobjc\bin>vsimporter.exe -help
Usage:  vsimporter.exe [-project projectname] [-target targetname ...] [-configuration configurationname] [-interactive] [setting=value ...]
        vsimporter.exe [-project projectname] -scheme schemename [-configuration configurationname] [-interactive] [setting=value ...]
        vsimporter.exe -workspace workspacename -scheme schemename [-configuration configurationname] [-interactive] [setting=value ...]
        vsimporter.exe -list [-project projectname | -workspace workspacename]

Program Options
    -usage                  print brief usage message
    -help                   print full usage message
    -interactive            enable interactive mode
    -loglevel LEVEL         debug | info | warning | error
    -list                   list the targets and configurations in the project
    -sdk SDKROOT            specify path to WinObjC SDK root (by default calculated from binary's location)
    -project PATH           specify project to process
    -workspace PATH         specify workspace to process
    -target NAME            specify target to process
    -alltargets             process all targets
    -scheme NAME            specify scheme to process
    -allschemes             process all schemes
    -configuration NAME     specify configuration to use
    -xcconfig FILE          apply build settings defined in FILE as overrides
    -format FORMAT          winstore8.1 | winphone8.1 | winstore10 (default)
    -version                print the tool version
  • formatでターゲットを指定することで、3種類のプロジェクトが生成可能です。

2 Xcodeでのプロジェクト作成

Xcodeの「Single View Application」から、テスト用のアプリを作成します。
なお、vsimporter.exeは、現時点では、Storyboardに対応できていないので、Xcode6でデフォルトで使用されるMain.Storyboardは削除して、ラベルをコードで貼りました。

f:id:furuya02:20150810030237p:plain

AppDelegate.h

#import <UIKit/UIKit.h>

@class ViewController;

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) UIWindow *window;
@property (strong, nonatomic) ViewController *viewController;

@end

AppDelegate.m

・・・省略・・・
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    self.window.backgroundColor = [UIColor whiteColor];
    self.viewController = [[ViewController alloc] init];
    self.window.rootViewController = self.viewController;
    [self.window makeKeyAndVisible];
    return YES;
}
・・・省略・・・

ViewController.m

・・・省略・・・
- (void)viewDidLoad {
    [super viewDidLoad];
    [self initLabel];
}

- (void)initLabel{
    CGRect rect = CGRectMake(60,100,200,30);
    UILabel *label = [[UILabel alloc] initWithFrame:rect];
    label.text = @"Hello World!";
    label .textAlignment = NSTextAlignmentCenter;
    label.textColor = [UIColor blackColor];
    [self.view addSubview:label];
}
・・・省略・・・

f:id:furuya02:20150810025354p:plain

3 Windows用プロジェクトの作成と実行

Xcodeで作成したプロジェクトを丸ごとWindows環境へ移して、vsimporter.exeを使用して、WindowsPhone用のプロジェクトを作成してみます。

コピーした時点でのフォルダの状態は次のとおりです。
f:id:furuya02:20150810030927p:plain
vsimporter.exeに-formatスイッチでWindowsPhoneを指定して実行した様子です。
f:id:furuya02:20150810030928p:plain
変換後のフォルダの状態です。
f:id:furuya02:20150810030930p:plain

生成されたslnファイルをダブルクリックしてVisualStudio2015を起動して、実行した様子です。
f:id:furuya02:20150810031442p:plain

4 Windows Bridge for iOS について補足

Bridgeは、次の4つのコンポーネントでできています。

1.Objective-C コンパイラ
 Objective-Cのコードを、ネイティブなユニバーサルWindowsアプリにコンパイルします
2.Objective-C ランタイム
 Objecttive-Cランタイムには、メッセージのディスパッチやデリゲーション、自動参照カウントなどが含まれています
3.iOS API ヘッダ/ライブラリ
 基本APIについては、すでに提供されていますが、さらに改善するために意見を待っているとの事です
4.VisualStudio IDEの統合
 Xcodeプロジェクトをインポートする

参考:Windows Bridge for iOS: Let’s open this up | Building Apps for Windows

ちなみに、このプロジェクトは、単に既存のObjecttiv-CのコードをWindowsコンパイルする事だけが目標ではないそうです。
例えば、次のように、Objective-CにコードからWindowsAPIもコールできるようです。
f:id:furuya02:20150810032150p:plain
上記では、Windows.Foundation.UriWindows.System.Launcherが利用されています。

参考:Windows Bridge for iOS: Let’s open this up | Building Apps for Windows

5 Windows Bridge for iOS の現状

現時点では、あくまで限定的なリリースで、完成度は、まだまだだと感じます。
今回の作業も、細いハッピーパスをくぐり抜けた感じが否定できません。

最終的なリリースは、今秋のVisualStudio2015のUpdateが予定との事です。
参考:Open Sourcing the Windows Bridge for iOS | Building Apps for Windows

なお、Androidの、テクニカルプレビューも一部の招待者で始まっており、8月末のパブリックベータが予定されているようです。
試してみたい方はサインアップしておくといいでしょう。
参考:Windows Bridge for Android - Windows アプリの開発

参考資料

Windows Bridge for iOS: Let’s open this up | Building Apps for Windows
Open Sourcing the Windows Bridge for iOS | Building Apps for Windows
Windows Bridge for Android - Windows アプリの開発

furuya02.hatenablog.com

Objective-CのコードがWindowsで動作する!「Windows Bridge for iOS」(WinObjcのサンプルをコンパイルしてみた)

Windows Bridge for iOS

5月のBuild2015のKeynoteでプレゼンされていた、Objective-CのコードがWindwos10で動くよ!という信じられないプロジェクトが遂に公開されました。
※当時「Project Islandwood」と呼ばれていたものは、「Windows Bridge for iOS」となったようです。
Windows Bridge for iOS - Windows app development

f:id:furuya02:20150808032804p:plain

blogs.windows.com

同プロジェクトは、オープンソースとしてGithubで公開されています。
f:id:furuya02:20150808032729p:plain

WinObjcとは

Githubで公開されている、WinObjCとは、Windows(VisualStudio)上にObjective-Cの開発環境を提供するものです。
iOSAPIとの互換性をサポートしています。

動作させるための要件は次のようになっています。
1 Windows10
2 VisualStudio2015(無償で利用可能な Visual Studio 2015 Communityでも大丈夫とアナウンスされています)
 なお、 
 ・言語としてVisual C++
 ・ユニバーサルアプリの開発ツール(すべて)
 ・Windows 8.1Windows Phone 8.0/8.1 の開発ツール(すべて)
が必要です。

サンプルアプリ

プロジェクトにはサンプルが2つ入っていましたので、とりあえずこれをコンパイルしてみました。
https://github.com/Microsoft/WinObjC/releases/download/0.1-preview/winobjc.zip

HelloUI

次の図は、winobjc.zipの中のsampleフォルダの中にある「HelloUI」のWinStore10で実行したものです。
f:id:furuya02:20150808023644g:plain
※プロジェクトは、ストアアプリ(8.1、10)及びWindows Phone 8.1用となっています。
f:id:furuya02:20150808030003p:plain

WOCCatalog

続いて同じくサンプルに含まれる「WOCCatalog」です。
WindowsPhone用のプロジェクトをコンパイル・実行してみました。

f:id:furuya02:20150808032505g:plain

こちらは、iOSの各種コントロールが、ブリッジされて置き換えられている様子がよく分かります。
iOSからの移植が前提なので、Xamarinと違って独自にコントロールを作成した感が滲みでています・・・特にPickerの力ずく感がっパねすw
これがネイティブなiOS利用者にどう映るのか・・・

参考リンク

Open Sourcing the Windows Bridge for iOS | Building Apps for Windows
Microsoft/WinObjC · GitHub
ニュース - Microsoft、iOSアプリをWindows 10に移植するWindows Bridgeを早期公開:ITpro

furuya02.hatenablog.com

SwiftyJSONをクラスオブジェクトで初期化する(Swiftにおけるリフレクション)

1 SwiftyJSON

SwiftyJSONは、Swiftで簡単にJSONを扱うライブラリとして、各所で紹介されており、機能も非常に充実しています。
しかし、何故か、JSON文字列をパースする処理ばかりで、その逆のJSON文字列へのシリアライズは見あたりませんでした。

つまり

// サンプルのクラス
class Parson{
    var name:String // 名前
    var age:Int // 年齢
    init(name:String,age:Int){
        self.name = name
        self.age = age
    }
}
// クラスオブジェクトからJSONオブジェクトを生成する
var json:JSON = JSON.Parse(Parson(name: "TARO", age: 25))
// JSONテキストを吐く
println(json.rawString())

みたいに書いて、

{
       "age" : 25,
       "name" : "TARO"
}

と出力してほしいのです。

ちょっと、悩んでいたのですが・・・もしかすると、Swiftでは、リフレクションが正式に仕様化されていないようですので、それが理由なのかも知れません。

しかし、一応、リフレクションは可能なようで、いくつか紹介されている記事を見つけましたので、
今回は、踏ん張って、SwiftyJSONの拡張メソッドとして、JSON文字列へのシリアライズ(あくまで触りだけ)を書いてみます。

Swift初めて、約1ヶ月の超初心者で突っ込みどころ満載ですw

2 クラスオブジェクトのシリアライズ

とりあえず、最初のParsonクラスをシリアライズできるように書いてみました。
reflect() で、オブジェクトの名前や値、型などが取得できるので、個別にJSONオブジェクトを作成してディクショナリに追加しました。

extension JSON {
    static func Parse(data:Any) -> JSON{
        var json:JSON=[:]
        let ref = reflect(data)
        for var i = 0; i < ref.count; ++i {
            let (key, mirror) = ref[i]
            //とりあえず、String,Int,Boolのみ対応
            if let n = mirror.value as? String {
                json[key] = JSON(n)
            } else if let n = mirror.value as? Int{
                json[key] = JSON(n)
            } else if let n = mirror.value as? Bool{
                json[key] = JSON(n)
            }
        }
        return json
    }
}

// クラスのオブジェクトからJSONオブジェクトを生成する
var json:JSON = JSON.Parse(Parson(name: "TARO", age: 25))
// JSONテキストを吐く
textView.text = json.rawString() // 表示

f:id:furuya02:20150730034829p:plain

なんとか動いているようです。

3 オプショナル型と階層構造への対応

続いて、先ほどのParsonクラスが、さらにParson型のプロパティを持つ場合への対応を書いてみます。
なお、これは、オプショナル型で定義し、nilの場合も処理できるようにしました。

クラスは、次のように変化します。

class Parson{
    var name:String
    var age:Int
    var mother:Parson? // オプショナル型のParson(母親の情報)
    init(name:String,age:Int){
        self.name = name
        self.age = age
    }
}

そして、拡張メソッドは、こんな感じでしょうか

extension JSON {
    static func Parse(data:Any) -> JSON{
        var json:JSON=[:]
        let ref = reflect(data)
        for var i = 0; i < ref.count; ++i {
            let (key, mirror) = ref[i]
            //nil検出(やや強引ですがAny型がnilと比較できなかったので・・・)
            if mirror.disposition == .Optional && "\(mirror.value)" == "nil" {
                 json[key] = nil
            } else {
                //とりあえず、String,Int,Boolのみ対応
                if let n = mirror.value as? String {
                    json[key] = JSON(n)
                } else if let n = mirror.value as? Int{
                    json[key] = JSON(n)
                } else if let n = mirror.value as? Bool{
                    json[key] = JSON(n)
                } else {
                    let subref = reflect(mirror.value)
                    let (k, m) = subref[0]
                    json[key] = Parse(m.value) // 再帰で同じメソッドにぶっ込む
                }
            }
        }
        return json
    }
}

    var taro = Parson(name: "TARO", age: 25) // motherプロパティは初期化していないのでnilになっている
    var json:JSON = JSON.Parse(taro)

    textView.text = json.rawString() // 表示

f:id:furuya02:20150730040810p:plain

    var taro = Parson(name: "TARO", age: 25)
    taro.mother = Parson(name:"HANAKO",age:50) // motherプロパティにParsonをセットする
    var json:JSON = JSON.Parse(taro)
        
    textView.text = json.rawString() // 表示

f:id:furuya02:20150730040832p:plain

4 配列への対応

調子に乗って、配列への対応も挑戦してみます。
配列の検出は、クラス名から行いました。

extension JSON {
    static func Parse(data:Any) -> JSON{
        var json:JSON=[:]
        let ref = reflect(data)
        // 配列検出
        var str = _stdlib_getDemangledTypeName(data)
        if str.hasPrefix("Swift.Array") {
            if let range = str.rangeOfString(".") {
                
                // クラス名もこんな感じで取れるけど、とりあえず必要ないか・・・
                //let separators = NSCharacterSet(charactersInString: "<.>")
                //var tmp = str.componentsSeparatedByCharactersInSet(separators)
                //var className = tmp[tmp.count-2]
                
                var ar:[JSON] = []
                for var i = 0; i < ref.count; ++i {
                    let (key, mirror) = ref[i]
                    var j = Parse(mirror.value)
                    ar.append(j)
                }
                json = JSON(ar) // 配列としてJSONに格納する
            }
        }else{ // 以下は、先ほどと変化なし
            for var i = 0; i < ref.count; ++i {
                
                let (key, mirror) = ref[i]
                
                //nil検出(やや強引ですがAny型がnilと比較できなかったので・・・)
                if mirror.disposition == .Optional && "\(mirror.value)" == "nil" {                        json[key] = nil
                } else {
                    //とりあえず、String,Int,Boolのみ対応
                    if let n = mirror.value as? String {
                        json[key] = JSON(n)
                    } else if let n = mirror.value as? Int{
                        json[key] = JSON(n)
                    } else if let n = mirror.value as? Bool{
                        json[key] = JSON(n)
                    } else {
                        let subref = reflect(mirror.value)
                        let (k, m) = subref[0]
                        json[key] = Parse(m.value) // 再帰
                    }
                }
            }
        }
        return json
    }
}

Parsonクラスは、前回と同じです。

    var ar:[Parson] = [] 
    var taro = Parson(name: "TARO", age: 25) 
    taro.mother = Parson(name:"HANAKO",age:50)
    ar.append(taro)// 配列の1件目
    ar.append(Parson(name: "KEISUKE", age: 21))// 配列の2件目
    var json:JSON = JSON.Parse(ar)

    textView.text = json.rawString() // 表示

f:id:furuya02:20150730041936p:plain

5 まとめ

現時点で、Int、Bool、String以外、まったく対応できていません。
Enumは、挑戦してみたのですが、型が取れずに挫折しました。

かなり、力ずくで、かつ適当なので、実用には程遠いですが、僅かでもリフレクションを書く際の参考になれば幸いです。

6 参考にさせて頂いたリンク

SwiftyJSON/SwiftyJSON · GitHub
MirrorType - NSHipster
Simple Reflection in Swift
Swiftでリフレクションを使ってインスタンスからプロパティの一覧を取得する - 酢ろぐ!
Swift Reflection (Swift 的反射) - 猫·仁波切
Safx: Swiftでリフレクションを使ってみる
Swift1.2でクラス名が取得できなくなった - Qiita

Xamarin.FormsでAzureモバイルサービスによるToDoアプリを作成するには?

【 Xamarin 記事一覧 】

BuildInsiderで連載されている「Xamarin逆引きTips」に寄稿させて頂きました。

Xamarin.FormsでAzureモバイルサービスによるToDoアプリを作成するには? - Build Insider

使用したコードは、下記にあります。github.com


【 Xamarin 記事一覧 】

Xamarin.FormsでTwitterクライアントを作成するには?

【 Xamarin 記事一覧 】

BuildInsiderで連載されている「Xamarin逆引きTips」に寄稿させて頂きました。

Xamarin.FormsでTwitterクライアントを作成するには? - Build Insider

使用したコードは、下記にあります。github.com


【 Xamarin 記事一覧 】

Xamarin.Formsでローカルデータベースを使用するには?

【 Xamarin 記事一覧 】

BuildInsiderで連載されている「Xamarin逆引きTips」に寄稿させて頂きました。

Error

使用したコードは、下記にあります。


github.com


【 Xamarin 記事一覧 】

Xamarin パケットモニターで見た、Build Hostとの通信

【 Xamarin 記事一覧 】

1.Build Host

Xamarin.VisualStudioでは、iOSのバイナリを作成するために、MacXcode)が必須となりますが、この時、WindowsMacの間の連携に使用されるのが「Build Host」です。
しかし、色々な事情で、この「BuildHost」との通信が、途絶えてしまうことが(よく)あります。
f:id:furuya02:20150512045740p:plain:w300
よく出てくるこの画面です・・・
再起動したり、バージョンを変更したりしているうちに、いつの間に繋がる・・・という印象で、私としては、上手く繋がらないときの対処方法が、今ひとつハッキリしていませんでした。
そこで、今回は、トラブル対処時の情報の一つとなるように、WindowsMac間の通信をパケットモニターで確認してみることにしました。

なお、本記事は、私が個人的に勝手に解釈したものであり、間違いを含んでいる可能性は十分にありますことを、予めご了承ください。

なお、パケットモニターには、Wiresharkを利用させて頂きました。
https://www.wireshark.org/

2.通信の種類

Build Hostとの通信には、大きく2種類があります。
一つは、「制御通信」で、もう一つは、「データ通信」です。
(この呼び方は、私が勝手に命名してしまってます)

「制御通信」は、リンクが開始してから終了するまで、ずっと繋がりっぱなしで、「データ通信」は、必要な情報を、Windows側からリクエストする毎に使用されています。


3.制御通信

Build Hostは、最初の接続口として、TCPの5000番ポートで待ち受けており、Pair開始からUnpairで終了するまで、ずっと繋がっています。

Wiresharkでは。ポート番号を頼りにして、次のようにフィルタすると簡単に確認できます。

tcp.port==5000

最悪、他にも5000番の通信がある場合、sourceとdestinationと加えて、こんな感じにすると見えてくると思います。

//Windowsが192.168.0.103、Macが192.168.0.102で動作している場合
tcp.port==5000 && (ip.addr==192.168.0.103 || ip.addr==192.168.0.102)

制御通信は、Windows側から、Macの5000番に対するTCPの3ウエイハンドシェークで始まっているのを確認できます。
f:id:furuya02:20150512053059p:plain
表面的には、図の状態で「Connect」ボタンを押したときです。
f:id:furuya02:20150512091920p:plain

そして、Buid Hostを止める(Unpairをクリックする)と、MacからのFINパケットで、この通信は終了しています。
f:id:furuya02:20150512054054p:plain
これも、表面的には、図の状態で「Unpair」ボタンを押したときです。
f:id:furuya02:20150512092920p:plain


下記は、同通信のTCPストリームを表示したもので、赤色が、Windowsからのもので青色がMacからのものです。f:id:furuya02:20150512052601p:plain

赤枠で囲んだ部分が、VisualStudioで最初にPINを入れるまで(リンク確率前)のものです。
PIN入力の完了後(リンク中)は、定期的にWindows側からKEEPALIVEが送られています。

f:id:furuya02:20150512053452p:plain

4.データ通信

先の制御通信のMac側からの最初のレスポンスにある、「MTBSERVERPORTS:数字;数字」が、データ通信の際のMac側の待ち受けポート番号となります。
f:id:furuya02:20150512055423p:plain
このポート番号は、制御通信ごとに変化(インクリメント)しています。
(いったん制御が切れると、次の制御開始時にポート番号は、変わっています)

事後、このポート番号を頼りに、データ通信のフィルタをすることになります。
(数値は、まずは最初のものが使用されているようで、2つ目のものが使用されている状況は、今のところ確認できていません。)

//ポート番号が49353の場合のフィルタの例
tcp.port==49353

データ通信は、HTTPプロトコルWindowsからリクエストされ、Macからは、XML形式でレスポンスされます。

以下は、いくつかのデータ通信をTCPストリームで表示したものです。

WindowsでPINを入力した時
Code="OK"でログインに成功している
f:id:furuya02:20150512064947p:plain
・PINの整合がとれリンクが確立した後の最初の通信(情報取得?)
f:id:furuya02:20150512055733p:plain
・シュミレータの一覧を取得している?
f:id:furuya02:20150512055738p:plain


5.トラブル時の状態

(1) BuidlHostが息をしていない場合

なんらかの理由で、BuildHostが正常に動作していない場合、Windowsからのハンドシェークが失敗して、制御通信が始まらないことになります。

図では、Windowsから5000番に対するSYNパケットに対して、MacがRSTを返しているのが確認できます。

f:id:furuya02:20150512054615p:plain

(2) PINを間違えている場合

Code="Error"になっています。
f:id:furuya02:20150512064955p:plain

(3) Bonjour Serviceが息をしていない

Xamarin.VisualStudioとBuild Hostとの間には、Bonjour Serviceというサービスが、介在しています。
擬似的に、このサービスを止めてみると・・・
f:id:furuya02:20150512094623p:plain
5000番への通信は、一切行われなくなります。
f:id:furuya02:20150512094630p:plain



【 Xamarin 記事一覧 】