ホームページ開発ツール>Xojo / Real Studio Trial and Error・CocoaのDeclareでシリアライズする

 Xojo / Real Studio Trial and Error

CocoaのDeclareでシリアライズする

目次
 はじめに

 以下は、Xojo Cocoaビルドについての話題です。

 Xojoは(当然ながら)標準でファイルIOの仕組みを持っていますが、変数の書き出しと読み込みは、TextOutputStream/TextInputStreamやBinaryStreamを使って自前で実装する必要があります。
 一方、Cocoaは標準でシリアライズの仕組みを持っていますので、使えるか、調べてみました。

 なお検証には、Xojo 2015 Release 4.1を用いています。(Mac mini mid 2010 + OS X 10.11.3 El Capitan)


 方針

 Cocoaのシリアライゼーションには、いくつかのAPIがありますが、いずれもプロパティリスト方式をとっています。
 プロパティリストにはバイナリとテキストがあり、テキストはXML形式となっています。
 ざっと調べたところ、以下が見つかりました。(他にもあるかも。)
 これらを踏まえて今回は、NSPropertyListSerializationを使ったシンプルなものと、NSKeyedArchiverを使った、より実用的なものを試してみることにしました。
 それぞれの仕様は以下の通りとしました。

 例1(NSPropertyListSerialization)
 例2(NSKeyedArchiver)

 Xojoでの実装(例1)
  1. Xojoで新規プロジェクトを作成
  2. Window1にPushButtonを二つ置き、一方のActionイベントにLoadPlist()、他方にSavePlist()と記述
  3. 以下をWindow1のメソッドに追加
    メソッド名: LoadPlist
    引数: なし
    戻り値型: なし
    
    // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。
    Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr
    
    Dim data As Ptr = NSClassFromString("NSData")  // クラスメソッドなので、まずNSDataクラスを取得
    Declare Function dataWithContentsOfFile Lib "Cocoa" Selector "dataWithContentsOfFile:" (receiver As Ptr, path As CFStringRef) As ptr
    data = dataWithContentsOfFile(data, "/Users/hoge/Desktop/Test.plist")  // ファイルパスの取得/指定方法は必要に応じて変更のこと
    
    Dim option As Integer = 0
    Dim format As New MemoryBlock(4)
    Dim err As New MemoryBlock(4)
    
    Dim pls As Ptr = NSClassFromString("NSPropertyListSerialization")  // クラスメソッドなので、まずNSPropertyListSerializationクラスを取得
    
    Declare Function propertyListWithData Lib "Cocoa" Selector "propertyListWithData:options:format:error:" (receiver As Ptr, data As Ptr, option As Integer, format As Ptr, err As Ptr) As Ptr
    Dim plist As Ptr = propertyListWithData(pls, data, option, format, err)  // 
    
    if err.UInt32Value(0) <> 0 then  // エラー時
        Declare Function domain Lib "Cocoa" Selector "domain" (receiver As Ptr) As CFStringRef
        Dim errDomain As String = domain(err.Ptr(0))
        Declare Function code Lib "Cocoa" Selector "code" (receiver As Ptr) As Integer
        Dim errCode As Integer = code(err.Ptr(0))
        Declare Function userInfo Lib "Cocoa" Selector "userInfo" (receiver As Ptr) As Ptr
        Dim errUserInfo As Ptr = userInfo(err.Ptr(0))
        Declare Function objectForKey Lib "Cocoa" Selector "objectForKey:" (receiver As Ptr, key As CFStringRef) As CFStringRef
        Dim errDesc As CFStringRef = objectForKey(errUserInfo, "NSDebugDescription")
        MessageDlg(errDomain+" : "+str(errCode), errDesc)
        
    else
        Declare Function count Lib "Cocoa" Selector "count" (receiver As Ptr) As Integer
        Dim cnt As Integer = count(plist)
        
        Declare Function objectForKey Lib "Cocoa" Selector "objectForKey:" (receiver As Ptr, key As CFStringRef) As CFStringRef
        Dim nam As CFStringRef = objectForKey(plist, "key1")  // key1は保存時に指定したキー
        
        msgBox nam
    end if
    
  4. 以下をWindow1のメソッドに追加
    メソッド名: SavePlist
    引数: なし
    戻り値型: なし
    
    // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。
    Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr
    
    Dim dic As Ptr = NSClassFromString("NSMutableDictionary")  // クラスメソッドなので、まずNSMutableDictionaryクラスを取得
    Declare Function getDictionary Lib "Cocoa" Selector "dictionary" (receiver As Ptr) As Ptr  // Return NSMutableDictionary*
    dic=getDictionary(dic)
    
    Declare Sub addObjectForKey Lib "Cocoa" Selector "setObject:forKey:" (receiver As Ptr, obj As CFStringRef, key As CFStringRef)
    addObjectForKey(dic, "sample2","key1")  // テストデータ
    
    Dim format As Integer = 100  // 注:保存形式にテキストを選択しているので、kCFPropertyListXMLFormat_v1_0を指定するべきだが、ここではその値である100を直接指定している。
    Dim err As New MemoryBlock(4)  // 注:エラー処理は省略している。
    Dim plist As Ptr = NSClassFromString("NSPropertyListSerialization")  // クラスメソッドなので、まずNSPropertyListSerializationクラスを取得
    Declare Function dataFromPropertyList Lib "Cocoa" Selector "dataFromPropertyList:format:errorDescription:" (receiver As Ptr, obj As Ptr, format As Integer, error As Ptr) As Ptr
    Dim data As Ptr = dataFromPropertyList(plist, dic, format, err)
    
    Declare Sub writeToFile Lib "Cocoa" Selector "writeToFile:atomically:" (receiver As Ptr, path As CFStringRef, flag As Boolean)
    writeToFile(data, "Users/hoge/Desktop/Test.plist",true)  // ファイルパスの取得/指定方法は必要に応じて変更のこと
    
    注)formatの値を、kCFPropertyListBinaryFormat_v1_0( = 200)にすればバイナリで保存可。
 実行してみたところ、シリアライズが機能することを確認しました。


 Xojoでの実装(例2)
  1. Xojoで新規プロジェクトを作成
  2. 新規クラスを作成(名前は、ここではデフォルトの「Class1」とした。)
  3. 以下をClass1のメソッドに追加
    メソッド名: AddArrayInteger
    引数: aryXojo() As Integer
    戻り値型: なし
    
    // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。
    Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr
    
    Dim ary As Ptr = NSClassFromString("NSMutableArray")  // クラスメソッドなので、まずNSMutableArrayクラスを取得
    Declare Function getArray Lib "Cocoa" Selector "array" (receiver As Ptr) As Ptr  // Return NSMutableArray*
    ary = getArray(ary)
    
    Dim num As Ptr = NSClassFromString("NSNumber")  // クラスメソッドなので、まずNSNumberクラスを取得
    Declare Function numberWithInteger Lib "Cocoa" Selector "numberWithInteger:" (receiver As Ptr, val As Integer) As Ptr  // Return NSNumber* 
    Declare Sub addObjectPtr Lib "Cocoa" Selector "addObject:" (receiver As Ptr, obj As Ptr)
    
    Dim i As Integer
    for i=0 to Ubound(aryXojo)
        addObjectPtr(ary, numberWithInteger(num, aryXojo(i)))  // 配列の値は整数だが、NSArrayにセットする際にはオブジェクト化する
    next
    
    addObjectPtr(OutAry, ary)  // 出力用Arrayに追加
    
  4. 以下をClass1のメソッドに追加
    メソッド名: AddArrayString
    引数: aryXojo() As String
    戻り値型: なし
    
    // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。
    Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr
    
    Dim ary As Ptr = NSClassFromString("NSMutableArray")  // クラスメソッドなので、まずNSMutableArrayクラスを取得
    Declare Function getArray Lib "Cocoa" Selector "array" (receiver As Ptr) As Ptr  // Return NSMutableArray*
    ary = getArray(ary)
    
    Declare Sub addObjectString Lib "Cocoa" Selector "addObject:" (receiver As Ptr, obj As CFStringRef)
    
    Dim i As Integer
    for i=0 to Ubound(aryXojo)
        addObjectString(ary, aryXojo(i))  // 文字列はそのままセットできる
    next
    
    Declare Sub addObjectPtr Lib "Cocoa" Selector "addObject:" (receiver As Ptr, obj As Ptr)
    addObjectPtr(OutAry, ary)  // 出力用Arrayに追加
    
  5. 以下をClass1のメソッドに追加
    メソッド名: AddDictionary
    引数: dicXojo() As Dictionary
    戻り値型: なし
    
    // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。
    Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr
    
    Dim ary As Ptr = NSClassFromString("NSMutableArray")  // クラスメソッドなので、まずNSMutableArrayクラスを取得
    Declare Function getArray Lib "Cocoa" Selector "array" (receiver As Ptr) As Ptr  // Return NSMutableArray*
    ary=getArray(ary)
    
    Dim dic As Ptr = NSClassFromString("NSMutableDictionary")  // クラスメソッドなので、まずNSMutableDictionaryクラスを取得
    Declare Function getDictionary Lib "Cocoa" Selector "dictionary" (receiver As Ptr) As Ptr  // Return NSMutableDictionary*
    
    Declare Sub setObjectForKey Lib "Cocoa" Selector "setObject:forKey:" (receiver As Ptr, obj As CFStringRef, key As CFStringRef)
    Declare Sub addObjectPtr Lib "Cocoa" Selector "addObject:" (receiver As Ptr, obj As Ptr)
    
    Dim i, j As Integer
    Dim val As String
    Dim dic2 As Ptr
    for i=0 to Ubound(dicXojo)
        dic2 = getDictionary(dic)
        
        for j=0 to 31// 最後が31なのは、キーが31までのため。
            if dicXojo(i).HasKey(j) then
                val = dicXojo(i).Value(j)
                setObjectForKey(dic2, val, Str(j))  // Dictionaryのキーは整数だが、NSDictionaryにセットする際には文字列化する
            end if
        next
        
        addObjectPtr(ary, dic2)
    next
    
    addObjectPtr(OutAry, ary)  // 出力用Arrayに追加
    
  6. 以下をClass1のメソッドに追加
    メソッド名: InitArray
    引数: なし
    戻り値型: なし
    
    // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。
    Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr
    
    OutAry = NSClassFromString("NSMutableArray")  // クラスメソッドなので、まずNSMutableArrayクラスを取得
    Declare Function getArray Lib "Cocoa" Selector "array" (receiver As Ptr) As Ptr  // Return NSMutableArray*
    OutAry = getArray(OutAry)
    
    Declare Sub addObjectString Lib "Cocoa" Selector "addObject:" (receiver As Ptr, obj As CFStringRef)
    addObjectString(OutAry, "ver1")  // バージョン番号を出力用Arrayに追加(ここでは文字列としたが、区別できるならなんでも良い)
    
  7. 以下をClass1のメソッドに追加
    メソッド名: LoadData
    引数: path As String
    戻り値型: なし
    
    // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。
    Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr
    
    Dim keyUarc As Ptr = NSClassFromString("NSKeyedUnarchiver")  // クラスメソッドなので、まずNSKeyedUnarchiverクラスを取得
    Declare Function unarchiveObjectWithFile Lib "Cocoa" Selector "unarchiveObjectWithFile:" (receiver As Ptr, path As CFStringRef) As Ptr
    Dim idx As Ptr = unarchiveObjectWithFile(keyUarc, path)  // ファイルから読み込み
      
    'Dim ary As Ptr = NSClassFromString("NSArray")  // クラスメソッドなので、まずNSArrayクラスを取得
    'Declare Function alloc Lib "Cocoa" selector "alloc" (receiver As Ptr) As Ptr
    'Declare Function initWithContentsOfFile Lib "Cocoa" selector "initWithContentsOfFile:" (receiver As Ptr, path As CFStringRef) As Ptr
    'Dim idx As Ptr = initWithContentsOfFile(alloc(ary), path)
    
    return idx
    
    注)上記ではコメントアウトによる選択式となっていますが、必要ならスイッチで切り換える等して下さい。

  8. 以下をClass1のメソッドに追加
    メソッド名: RestoreArrayInteger
    引数: dat As Ptr, idx As Integer, aryXojo() As Integer
    戻り値型: Integer()
    
    Declare Function objectAtIndexPtr Lib "Cocoa" Selector "objectAtIndex:" (receiver As Ptr, idx As Integer) As Ptr
    Dim ary As Ptr = objectAtIndexPtr(dat, idx)  // Return NSArray*
    
    Dim num As Ptr
    Declare Function intValue Lib "Cocoa" Selector "intValue" (receiver As Ptr) As Integer
    
    Dim i As Integer
    for i=0 to Ubound(aryXojo)
        num = objectAtIndexPtr(ary, i)  // この時点ではオブジェクト
        aryXojo(i) = intValue(num)  // 整数に戻す
    next
    
    return aryXojo
    
  9. 以下をClass1のメソッドに追加
    メソッド名: RestoreArrayString
    引数: dat As Ptr, idx As Integer, aryXojo() As String
    戻り値型: String()
    
    Declare Function objectAtIndexPtr Lib "Cocoa" Selector "objectAtIndex:" (receiver As Ptr, idx As Integer) As Ptr
    Dim ary As Ptr = objectAtIndexPtr(dat, idx)  // Return NSArray*
    
    Declare Function objectAtIndexString Lib "Cocoa" Selector "objectAtIndex:" (receiver As Ptr, idx As Integer) As CFStringRef
    
    Dim i As Integer
    for i=0 to Ubound(aryXojo)
        aryXojo(i) = objectAtIndexString(ary, i)  // 文字列はそのままセット
    next
    
    return aryXojo
    
  10. 以下をClass1のメソッドに追加
    メソッド名: RestoreDictionary
    引数: dat As Ptr, idx As Integer, dicXojo() As Dictionary
    戻り値型: Dictionary()
    
    Declare Function objectAtIndexPtr Lib "Cocoa" Selector "objectAtIndex:" (receiver As Ptr, idx As Integer) As Ptr
    Dim ary As Ptr = objectAtIndexPtr(dat, idx)  // Return NSDictionary*
    
    Declare Function count Lib "Cocoa" Selector "count" (receiver As Ptr) As Integer
    Dim imax As Integer = count(ary)
    
    Declare Function allKeys Lib "Cocoa" Selector "allKeys" (receiver As Ptr) As Ptr
    Declare Function objectAtIndexString Lib "Cocoa" Selector "objectAtIndex:" (receiver As Ptr, idx As Integer) As CFStringRef
    Declare Function objectForKey Lib "Cocoa" Selector "objectForKey:" (receiver As Ptr, key As CFStringRef) As CFStringRef
    
    Dim i, j, jmax, keyV As Integer
    Dim key As String
    Dim keys, dic As Ptr
    for i=0 to imax-1
        
        ReDim dicXojo(i)
        dicXojo(i) = new Dictionary
        
        dic = objectAtIndexPtr(ary, i)
        jmax = count(dic)
        keys = allKeys(dic)
        
        for j=0 to jmax-1
            key = objectAtIndexString(keys, j)  // キーの取得
            keyV = Val(key)  // キーを整数に戻す
            dicXojo(i).Value(keyV) = objectForKey(dic, key)  // キーを指定して、取得した値をセット
        next
        
    next
    
    return dicXojo
    
  11. 以下をClass1のメソッドに追加
    メソッド名: RestoreVersion
    引数: idx As Ptr
    戻り値型: String
    
    Declare Function objectAtIndexString Lib "Cocoa" Selector "objectAtIndex:" (receiver As Ptr, idx As Integer) As CFStringRef
    Dim str As String = objectAtIndexString(idx, 0)
    
    return str
    
  12. 以下をClass1のメソッドに追加
    メソッド名: SaveData
    引数: path As String
    戻り値型: なし
    
    // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。
    Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr
    
    // バイナリ形式の場合
    Dim keyArc As Ptr = NSClassFromString("NSKeyedArchiver")  // クラスメソッドなので、まずNSKeyedArchiverクラスを取得
    Declare Sub archiveRootObjectToFile Lib "Cocoa" Selector "archiveRootObject:toFile:" (receiver As Ptr, obj As Ptr, path As CFStringRef)
    archiveRootObjectToFile(keyArc, OutAry, path)  // ファイルに保存
    
    // テキスト形式の場合
    'Declare Sub writeToFile Lib "Cocoa" Selector "writeToFile:atomically:" (receiver As Ptr, path As CFStringRef, flag As Boolean)
    'writeToFile(OutAry, path,true)  // ファイルに保存
    
    注)上記ではコメントアウトによる選択式となっていますが、必要ならスイッチで切り換える等して下さい。

  13. 以下をClass1のプロパティに追加
    プロパティ名: OutAry
    データ型: Ptr
    標準値: なし
    
  14. Window1にPushButtonを置き、以下をActionイベントに追加
    InitData()
    
    Dim cls As Class1 = new Class1
    cls.InitArray()  // 初期化
    
    cls.AddArrayInteger(PrefInteger1)  // 整数型配列変数
    
    cls.AddArrayString(PrefString1)  // 文字列型配列変数1
    cls.AddArrayString(PrefString2)  // 文字列型配列変数2
    
    cls.AddDictionary(DataDict1)  // Dictionary
    
    cls.SaveData("Users/hoge/Desktop/Test.dat")  // ファイルパスの取得/指定方法は必要に応じて変更のこと
    
  15. Window1にPushButtonを置き、以下をActionイベントに追加
    Dim cls As Class1 = new Class1
    
    Dim dat As Ptr = cls.LoadData("Users/hoge/Desktop/Test.dat")  // ファイルパスの取得/指定方法は必要に応じて変更のこと
    
    Dim verStr As String = cls.RestoreVersion(dat)
    if verStr="ver1" then  // バージョンが上がったら、別途対応する処理を追加する
    
        PrefInteger1=cls.RestoreArrayInteger(dat,1,PrefInteger1)
        msgBox str(PrefInteger1(0))+" "+str(PrefInteger1(9))
    
        PrefString1=cls.RestoreArrayString(dat,2,PrefString1)
        msgBox PrefString1(0)+" "+PrefString1(9)
    
        PrefString2=cls.RestoreArrayString(dat,3,PrefString2)
        msgBox PrefString2(0)+" "+PrefString2(9)
    
        DataDict1=cls.RestoreDictionary(dat,4,DataDict1)
        msgBox DataDict1(0).Value(11)+" "+DataDict1(1).Value(11)
    
    end if
    
  16. 以下をWindow1のメソッドに追加
    メソッド名: InitData
    引数: なし
    戻り値型: なし
    
    // 整数型配列変数
    PrefInteger1(0)=10
    PrefInteger1(1)=11
    PrefInteger1(2)=12
    PrefInteger1(3)=13
    PrefInteger1(4)=14
    PrefInteger1(5)=15
    PrefInteger1(6)=16
    PrefInteger1(7)=17
    PrefInteger1(8)=18
    PrefInteger1(9)=19
    
    // 文字列型配列変数1
    PrefString1(0)="a"
    PrefString1(1)="b"
    PrefString1(2)="c"
    PrefString1(3)="d"
    PrefString1(4)="e"
    PrefString1(5)="f"
    PrefString1(6)="g"
    PrefString1(7)="h"
    PrefString1(8)="i"
    PrefString1(9)="j"
    
    // 文字列型配列変数2
    PrefString2(0)="0"
    PrefString2(1)="1"
    PrefString2(2)="2"
    PrefString2(3)="3"
    PrefString2(4)="4"
    PrefString2(5)="5"
    PrefString2(6)="6"
    PrefString2(7)="7"
    PrefString2(8)="8"
    PrefString2(9)="9"
    
    // Dictionary
    ReDim DataDict1(0)
    DataDict1(0)=new Dictionary
    DataDict1(0).Value(1)=0  // "next"
    DataDict1(0).Value(11)=2016  // "year"
    DataDict1(0).Value(12)=3  // "month"
    DataDict1(0).Value(13)=2  // "day"
    DataDict1(0).Value(14)=0  // "color"
    DataDict1(0).Value(15)=&hFFFFFF  // "bkclr"
    DataDict1(0).Value(21)="comment"  // "comment"
    DataDict1(0).Value(22)=""  // "style"
    DataDict1(0).Value(23)=1  // "paragraph align"
    DataDict1(0).Value(31)=2  // "btag"
    ReDim DataDict1(1)
    DataDict1(1)=new Dictionary
    DataDict1(1).Value(1)=0  // "next"
    DataDict1(1).Value(11)=2015  // "year"
    DataDict1(1).Value(12)=4  // "month"
    DataDict1(1).Value(13)=3  // "day"
    DataDict1(1).Value(14)=0  // "color"
    DataDict1(1).Value(15)=&hFFFFFF  // "bkclr"
    DataDict1(1).Value(21)="comment2"  // "comment"
    DataDict1(1).Value(22)=""  // "style"
    DataDict1(1).Value(23)=2  // "paragraph align"
    DataDict1(1).Value(31)=3  // "btag"
    
  17. 以下をWindow1のプロパティに追加
    プロパティ名: DataDict1(-1)
    データ型: Dictionary
    標準値: なし
    
  18. 以下をWindow1のプロパティに追加
    プロパティ名: PrefInteger1(10)
    データ型: Integer
    標準値: なし
    
  19. 以下をWindow1のプロパティに追加
    プロパティ名: PrefString1(10)
    データ型: String
    標準値: なし
    
  20. 以下をWindow1のプロパティに追加
    プロパティ名: PrefString2(10)
    データ型: String
    標準値: なし
    

 実行してみたところ、シリアライズが機能することを確認しました。


 おわりに

 視認性はテキストの方が上ですが、データ量が多くなると、やはりバイナリを選択せざるを得ない気はします。

 なお、今回も仕組みを理解するために、極力シンプルな書き方を心懸けています。
(例えば、allocした変数の一部は然るべきタイミングでReleaseするべきと思われますが、そのためにはコードを工夫する必要があります。)
 実用目的であれば、macoslibを利用させて頂くことを考えた方がいいでしょう。
(NSKeyedArchiver/NSCoding/NSPropertyListSerializationは、見つけられませんでしたが、NSDictionaryやNSArrayのwriteToFileはあります。)


 お世話になったサイト

 貴重な情報をご提供頂いている皆様に、お礼申し上げます。(以下、順不同)

 参考サイト(1):シリアライズ - Wikipedia
 参考サイト(2):プロパティリスト - Wikipedia


 更新履歴

 2016.03.09 新規作成


[Home]  [MacSoft]  [Donation]  [History]  [Privacy Policy]  [Affiliate Policy]