2016年4月15日 星期五

swift 學習筆記 9. 讓 swift 和 c 交朋友

多數 linux 程式是用 c 寫的, 要讓 swift 程式可以呼叫 c 所寫的程式運作, 便需要透過 module 來做中間人. module 包含兩個重要檔案:  module.modulemap, 及 Package.swift(但該檔案的內容通常是空的, 但卻必須存在, 可以透過 touch Package.swift 來產生就可), 其他程式碼也都在此同一目錄下. 透過用 clang 來將 c 原始檔 compile 成目標檔, 再用 ar 封裝成程式庫(library)後放到 /usr/local/lib 下(但在OSX要放在 /usr/lib 底下). 最後交由 swift 讓他連結後產生執行檔.

編譯  c 程式可以寫一個 Makefile, 方便下指令 make 讓電腦自動去執行:

MYLIB = libmyfunction.a

$(MYLIB): myfunction.o
ar rcs $@ $^

myfunction.o: myfunction.c
clang -c $< -o $@

clean:
rm -f *.o *.a $(MYLIB)

install:    
sudo cp $(MYLIB) /usr/local/lib/$(MYLIB)


上述中 myfunction.c 及 myfunction.h 就是用 c 寫的程式碼. 最後如果要安裝到 /usr/local/lib 會需要用到 sudo 指令, 因此 user 必須要有寫入權限才可以, 對於將字串參數傳遞進 c 程式, 我試的結果要宣告程 const char * 才可以, 否則在 swift 編譯連結時會有錯誤訊息

一旦將程式除錯完畢, 就要準備好 swift 與 c 溝通的中間檔 module.modulemap,  範例內容如下:
         module Name [system] {
                header  "mymodule.h"
                link       "myfunction"
                export    *
         }
上述 header 指示要到那去尋找標頭檔 , 如果有多個標頭檔時, 可以另外寫一個標頭檔,裏面用 #include  指示將其它標頭檔包進來, 並將 module.modulemap 的 header 改成 umbrella header. 若是不想寫這種標頭檔, 那就把所有要包進來的檔放在一個目錄, 並將 module.modulemap 的標頭指示改成 umbrella 並提供該目錄名稱(最後一個字要加上/),
link 指示需要連結哪些程式庫(library)
最後全部需 export 出來讓 linker 可以找到裏面的程序符號, 以便相連結.
最開頭的 Name 就是將來寫 swift 程式要 import 進來的程式庫名稱, 他是可以自行命名的. 上述程序都完成後, 接著要將該 module所在的目錄用版本控制程式讓 git 來追蹤(因 swift 的 package manager 會用 git clone 指令來複製該模組), 底下列出一些常用的 git 指令, 需視需要加以添加
    git init
    git config user.name "YourName"
    git config user.email "name@somewhere"
    git add Package.swift module.modulemap Makefile
    git add example.c example.h
    git commit -m "This is an example"
    git tag 0.0.1

最後便是寫一個 swift 專案來測試程式, 該專案主目錄下的 Package.swift 需要作一些指示, 才能使用上述的 module, 範例內容如下:
         import PackageDescription
         let package = Package(
                     dependencies: [
                              .package(url:"to/module/path", version: Version(0,0,1) ..< Version(2,0,0) 
                       ]
              )
上述其實是 swift 語言的寫法, 他是說要使用到內部 PackageDescription 模組,將他含括進來運作,讓 package 初始化相依值( dependencices )  讓他成員的位置 (url)指向模組所在的目錄, 並且版本編號指定用 0.0.1 與 2.0.0 之間的檔案, 確定後把它下載到該專案下面(通常是目錄 Packages 內 )

而專案要寫的程式碼就在該專案下建一個目錄名稱, 定名為 Sources或 Source或 src 或 srcs 都可以, 將全部的原始碼放到該目錄下面. 最後下一個指令:
          swift  build
如果一切正常, 將會看到執行檔放在 .build/debug/ 目錄下



對於 c 常用的指標, 在 swift 語言定義比較嚴格, 對於參數的指標(&) , 僅可以在要傳給程序時才能使用, 而指標的宣告型別是 UnsafePointer<TYPE> UnsafeMutablePointer<TYPE>, 如果宣告成UnsafePointer<TYPE>, 在程序內就不得對它做寫入的動作. 而要傳進去的指標也只能是變數屬性, 如果傳進常數屬性指標將會產生錯誤(因常數屬性不得被修改).

   func show(ptr:UnsafePointer<Int>) {
       print("Pointer is:\(ptr)\n")
   }

   func inc(ptr: UnsafeMutablePointer<Int>) {
       ptr.memory += 1
   }
   var a=10
   show(&a)  // don't use constant property, show the address of the stored property
   inc(&a)     // don't use  constant property, increase 1 in the stored property

如果要對屬性指標直接處理, 可以用 withUnsafePointer(&var_name) {  $0.memory }及 withUnsafeMutablePointer(&var_name) { $0.memory }, 如果使用 withUnsafePointer(&var_name) { $0.memory }則在 closure 內就不得對 var_name 做寫入的動作. 而要傳進去的指標也只能是變數屬性, 如果傳進常數屬性指標將會產生錯誤(因常數屬性不得被修改). 另外還要注意的是 closure 所傳回值的型必須與 var_name 的型別相同

   var a=10
   let c=withUnsafeMutablePointer(&a) { (a) -> Int in
             a.memory += 1   // increase a
             return 1               // but return 1, return TYPE must be same as a
         }
   // now a=11, c=1

最後要提到的是如果用 c 寫的副程式使用了 variadic parameter 時, 像類似printf (const char *str, ...) 在最後的宣告有3個句點的副程式, 它是不能被 swift 程式直接呼叫的, 要解決這問題, 只能再寫一個 c 程式碼來橋接 swift 的程式, 這個程式是要給 swift 呼叫用的, 他必須要有明確數量的參數, 由該橋接副程式再去呼叫有 variadic parameter 的 C 副程式就可以了, 例如:

           int Myprintf(const char *fmt) // this function can be call by swift program
           {
                 // call a C function which need variadic parameters
                 if( fmt!=NULL )  printf(fmt);
           }


後記:

1. 實際上 C 與 Swift 都是將 Variadic parameters 一個一個塞到陣列裡去, 再將該陣列的指標傳過去的. 因此處理整數或符點數或字元時要注意兩邊的型別要一致才行

2. https://developer.apple.com/library/tvos/documentation/Swift/Reference/Swift_CVarArgType_Protocol/index.html
提到可將 c 所寫的副程式的 variadic parameters, 不要宣告成 int f( int, ...)的形式, 而是改用 int f(int, va_list arguments)的形式宣告, 這樣就可以直接接收不定參數陣列的指標, 之後再來加以運用.詳見下例:
         // C function
         #include "stdarg.h"      
         int SumOf(int count,va_list arglist)
         {
            int sum;
            for( sum=0;count>0;count--) sum += va_arg(arglist,int);
            return sum;
         }


         // Swift
  1. func swiftF(x: CInt, arglist: CVarArgType...) -> CInt {
  2. return withVaList(arglist) { SumOf(x, $0) }
  3. }
swiftF(10,1,2,3,4,5,6,7,8,9,10) // sum of 1...10, count=10
swiftF(5,1,2,3,4,5) // sum of 1..5, count=5

3. Swift 其實已內建許多 C 標準函式庫, 需要時需要將Foundation 及 Glibc 給 import 進來, 而 C 的字串型別在 swift 裡是 [CChar], 也就是字元陣列,要初始它可以利用它的建構式:
            var buf = [CChar](count:1024, repeatedValue:0)
上述 buf 定義了 1024 個字元陣列, 內容全是 0, 如果要將 buf 指標傳給 C 所寫的副程式, 用 &buf 就可以了.

4. 如果要將 C 的字串轉成 swift 的 String 型別時使用 String.fromCString(cstr) 來轉換, 他所轉初的字串型別是 String?, 也就是有可能是 nil 的字串型別.









 

沒有留言: