背景
在垃圾簡訊過濾應用 SMSFilters
中,需要使用 Jieba
分詞庫來対簡訊進行分詞,然後使用 TF-IDF
來進行處理,但是 Jieba
分詞庫是 C++ 寫的,這就意味著需要在Swift中整合 C++ 庫。
在官方文件 "Using Swift with Cocoa and Objective-C" 中,Apple只是介紹了怎麼將 Swift 程式碼跟 Objective-C 程式碼做整合,但是沒有提C++,後來在官方文件中看到了這樣一段話:
You cannot import C++ code directly into Swift. Instead, create an Objective-C or C wrapper for C++ code.
也就是不能直接匯入 C++ 程式碼,但是可以使用 Objective-C 或者 C 對 C++ 進行封裝。所以專案中使用 Objective-C 做封裝,然後在 Swift 中呼叫,下面就是這個過程的實踐,Demo 程式碼見 SwiftJiebaDemo。
整合過程
分成三步:
- 引入C++檔案;
- 用 Objective-C 封裝;
- 在 Swift 中 呼叫 Objective-C;
引入C++檔案
Demo中使用的是"結巴"中文分詞的 C++ 版本 yanyiwu/cppjieba。將其中的 include/cppjieba
和依賴 limonp
合併,並加入 dict
中的 hmm_model
和 jiaba.dict
作為基礎資料,並暴露 JiebaInit
和 JiebaCut
介面:
//
// Segmentor.cpp
// iosjieba
//
// Created by yanyiwu on 14/12/24.
// Copyright (c) 2014年 yanyiwu. All rights reserved.
//
#include "Segmentor.h"
#include <iostream>
using namespace cppjieba;
cppjieba::MixSegment * globalSegmentor;
void JiebaInit(const string& dictPath, const string& hmmPath, const string& userDictPath)
{
if(globalSegmentor == NULL) {
globalSegmentor = new MixSegment(dictPath, hmmPath, userDictPath);
}
cout << __FILE__ << __LINE__ << endl;
}
void JiebaCut(const string& sentence, vector<string>& words)
{
assert(globalSegmentor);
globalSegmentor->Cut(sentence, words);
cout << __FILE__ << __LINE__ << endl;
cout << words << endl;
}
複製程式碼
以及
//
// Segmentor.h
// iosjieba
//
// Created by yanyiwu on 14/12/24.
// Copyright (c) 2014年 yanyiwu. All rights reserved.
//
#ifndef __iosjieba__Segmentor__
#define __iosjieba__Segmentor__
#include <stdio.h>
#include "cppjieba/MixSegment.hpp"
#include <string>
#include <vector>
extern cppjieba::MixSegment * globalSegmentor;
void JiebaInit(const std::string& dictPath, const std::string& hmmPath, const std::string& userDictPath);
void JiebaCut(const std::string& sentence, std::vector<std::string>& words);
#endif /* defined(__iosjieba__Segmentor__) */
複製程式碼
目錄如下:
$ tree iosjieba
iosjieba
├── Segmentor.cpp
├── Segmentor.h
├── cppjieba
│ ├── DictTrie.hpp
│ ├── FullSegment.hpp
│ ├── HMMModel.hpp
│ ├── HMMSegment.hpp
│ ├── Jieba.hpp
│ ├── KeywordExtractor.hpp
│ ├── MPSegment.hpp
│ ├── MixSegment.hpp
│ ├── PosTagger.hpp
│ ├── PreFilter.hpp
│ ├── QuerySegment.hpp
│ ├── SegmentBase.hpp
│ ├── SegmentTagged.hpp
│ ├── TextRankExtractor.hpp
│ ├── Trie.hpp
│ ├── Unicode.hpp
│ └── limonp
│ ├── ArgvContext.hpp
│ ├── BlockingQueue.hpp
│ ├── BoundedBlockingQueue.hpp
│ ├── BoundedQueue.hpp
│ ├── Closure.hpp
│ ├── Colors.hpp
│ ├── Condition.hpp
│ ├── Config.hpp
│ ├── FileLock.hpp
│ ├── ForcePublic.hpp
│ ├── LocalVector.hpp
│ ├── Logging.hpp
│ ├── Md5.hpp
│ ├── MutexLock.hpp
│ ├── NonCopyable.hpp
│ ├── StdExtension.hpp
│ ├── StringUtil.hpp
│ ├── Thread.hpp
│ └── ThreadPool.hpp
└── iosjieba.bundle
└── dict
├── hmm_model.utf8
├── jieba.dict.small.utf8
└── user.dict.utf8
複製程式碼
接下來開始在專案中整合。首先建立一個空專案 iOSJiebaDemo
,將 iosjieba
加入專案中。
單頁應用 | SwiftJiebaDemo | 新增 SwiftJiebaDemo |
---|---|---|
新增 iosjieba:
見程式碼: github.com/qiwihui/Swi…
C++ 到 Objective-C 封裝
這個過程是將 C++ 的介面進行 Objective-C 封裝,向 Swift 暴露。這個封裝只暴露了 objcJiebaInit
和 objcJiebaCut
兩個介面。
//
// iosjiebaWrapper.h
// SMSFilters
//
// Created by Qiwihui on 1/14/19.
// Copyright © 2019 qiwihui. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface JiebaWrapper : NSObject
- (void) objcJiebaInit: (NSString *) dictPath forPath: (NSString *) hmmPath forDictPath: (NSString *) userDictPath;
- (void) objcJiebaCut: (NSString *) sentence toWords: (NSMutableArray *) words;
@end
複製程式碼
//
// iosjiebaWrapper.mm
// iOSJiebaTest
//
// Created by Qiwihui on 1/14/19.
// Copyright © 2019 Qiwihui. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "iosjiebaWrapper.h"
#include "Segmentor.h"
@implementation JiebaWrapper
- (void) objcJiebaInit: (NSString *) dictPath forPath: (NSString *) hmmPath forDictPath: (NSString *) userDictPath {
const char *cDictPath = [dictPath UTF8String];
const char *cHmmPath = [hmmPath UTF8String];
const char *cUserDictPath = [userDictPath UTF8String];
JiebaInit(cDictPath, cHmmPath, cUserDictPath);
}
- (void) objcJiebaCut: (NSString *) sentence toWords: (NSMutableArray *) words {
const char* cSentence = [sentence UTF8String];
std::vector<std::string> wordsList;
for (int i = 0; i < [words count];i++)
{
wordsList.push_back(wordsList[i]);
}
JiebaCut(cSentence, wordsList);
[words removeAllObjects];
std::for_each(wordsList.begin(), wordsList.end(), [&words](std::string str) {
id nsstr = [NSString stringWithUTF8String:str.c_str()];
[words addObject:nsstr];
});
}
@end
複製程式碼
見程式碼: github.com/qiwihui/Swi…
Objective-C 到 Swift
在 Swift 中呼叫 Objecttive-C 的介面,這個在官方文件和許多部落格中都有詳細介紹。
- 加入
{project_name}-Bridging-Header.h
標頭檔案,即SwiftJiebaDemo_Bridging_Header_h
,引入之前封裝的標頭檔案,並在Targets -> Build Settings -> Objective-C Bridging Header
中設定標頭檔案路徑SwiftJiebaDemo/SwiftJiebaDemo_Bridging_Header_h
。
//
// SwiftJiebaDemo-Bridging-Header.h
// SwiftJiebaDemo
//
// Created by Qiwihui on 1/15/19.
// Copyright © 2019 Qiwihui. All rights reserved.
//
#ifndef SwiftJiebaDemo_Bridging_Header_h
#define SwiftJiebaDemo_Bridging_Header_h
#import "iosjiebaWrapper.h"
#endif /* SwiftJiebaDemo_Bridging_Header_h */
複製程式碼
- 將使用到 C++ 的 Objective-C 檔案修改為 Objective-C++ 檔案,即 將
.m
改為.mm
:iosjiebaWrapper.m
改為iosjiebaWrapper.mm
。
使用
使用時需要先初始化 Jiaba
分詞,然後再進行分詞。
class Classifier {
init() {
let dictPath = Bundle.main.resourcePath!+"/iosjieba.bundle/dict/jieba.dict.small.utf8"
let hmmPath = Bundle.main.resourcePath!+"/iosjieba.bundle/dict/hmm_model.utf8"
let userDictPath = Bundle.main.resourcePath!+"/iosjieba.bundle/dict/user.dict.utf8"
JiebaWrapper().objcJiebaInit(dictPath, forPath: hmmPath, forDictPath: userDictPath);
}
func tokenize(_ message:String) -> [String] {
print("tokenize...")
let words = NSMutableArray()
JiebaWrapper().objcJiebaCut(message, toWords: words)
return words as! [String]
}
}
複製程式碼
控制檯輸出結果:
可以看到,測試用例 小明碩士畢業於中國科學院計算所,後在日本京都大學深造
經過分詞後為
〔拼音〕["小明", "碩士", "畢業", "於", "中國科學院", "計算所", ",", "後", "在", "日本", "京都大學", "深造"]
,完成整合。
見程式碼: github.com/qiwihui/Swi…
遇到的問題
由於自己對於編譯連結原理不瞭解,以及是 iOS 開發初學,因此上面的這個過程中遇到了很多問題,耗時兩週才解決,故將遇到的一些問題記錄於此,以便日後。
"cassert" file not found
將 .m
改為 .mm
即可。
compiler not finding <tr1/unordered_map>
設定 C++ Standard Library
為 LLVM libc++
參考: mac c++ compiler not finding <tr1/unordered_map>
warning: include path for stdlibc++ headers not found; pass '-std=libc++' on the command line to use the libc++ standard library instead [-Wstdlibcxx-not-found]
Build Setting -> C++ Standard Library -> libstdc++
修改為 Build Setting -> C++ Standard Library -> libc++
use of unresolved identifier
這個問題在於向專案中加入檔案時,Target Membership
設定不正確導致。需要將對於使用到的 Target 都勾上。
相關參考: Understanding The "Use of Unresolved Identifier" Error In Xcode
參考
- SwiftArchitect 對問題 “Can I have Swift, Objective-C, C and C++ files in the same Xcode project? ” 的回答
- SwiftArchitect 對問題 "Can I mix Swift with C++? Like the Objective - C .mm files" 的回答
- 在Swift程式碼中整合C++類庫