在iOS-Swift專案中整合CppJieba分詞

qiwihui發表於2019-01-16

背景

在垃圾簡訊過濾應用 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

整合過程

分成三步:

  1. 引入C++檔案;
  2. 用 Objective-C 封裝;
  3. 在 Swift 中 呼叫 Objective-C;

引入C++檔案

Demo中使用的是"結巴"中文分詞的 C++ 版本 yanyiwu/cppjieba。將其中的 include/cppjieba 和依賴 limonp 合併,並加入 dict 中的 hmm_modeljiaba.dict 作為基礎資料,並暴露 JiebaInitJiebaCut 介面:

//
//  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
create-single-view-app
swift-jieba-demo-1
swift-jieba-demo-2

新增 iosjieba:

iosjieba-1

見程式碼: github.com/qiwihui/Swi…

C++ 到 Objective-C 封裝

這個過程是將 C++ 的介面進行 Objective-C 封裝,向 Swift 暴露。這個封裝只暴露了 objcJiebaInitobjcJiebaCut 兩個介面。

//
//  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 的介面,這個在官方文件和許多部落格中都有詳細介紹。

  1. 加入 {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 */
複製程式碼

bridging-header-2

  1. 將使用到 C++ 的 Objective-C 檔案修改為 Objective-C++ 檔案,即 將 .m 改為 .mm: iosjiebaWrapper.m 改為 iosjiebaWrapper.mm

見程式碼:github.com/qiwihui/Swi…

使用

使用時需要先初始化 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]
    }
}

複製程式碼

控制檯輸出結果:

result

可以看到,測試用例 小明碩士畢業於中國科學院計算所,後在日本京都大學深造 經過分詞後為 〔拼音〕["小明", "碩士", "畢業", "於", "中國科學院", "計算所", ",", "後", "在", "日本", "京都大學", "深造"],完成整合。

見程式碼: github.com/qiwihui/Swi…

遇到的問題

由於自己對於編譯連結原理不瞭解,以及是 iOS 開發初學,因此上面的這個過程中遇到了很多問題,耗時兩週才解決,故將遇到的一些問題記錄於此,以便日後。

  1. "cassert" file not found

.m 改為 .mm 即可。

  1. compiler not finding <tr1/unordered_map>

設定 C++ Standard LibraryLLVM libc++

llvm

參考: mac c++ compiler not finding <tr1/unordered_map>

  1. 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++

  1. use of unresolved identifier

這個問題在於向專案中加入檔案時,Target Membership 設定不正確導致。需要將對於使用到的 Target 都勾上。

相關參考: Understanding The "Use of Unresolved Identifier" Error In Xcode

參考

相關文章