NO IMAGE

Qt中關於JavaScript/QML和C 混合程式設計

宣告:本文很多內容都是個人學習和嘗試而得出的結果,難免會有疏漏和粗枝大葉,如有不正確或不嚴謹之處,還請指正。

感謝soloist的悉心教導和耐心講解,以及在工作和學習中對我鼓勵與幫助。

我使用的環境是: Qt 5.2.1 (Clang 5.0 (Apple), 64 bit),Qt Creator 3.1.1 (opensource)。

QML(Qt Markup Language):http://qt-project.org/doc/qt-5/qmlapplications.html

The Meta-Object System : http://qt-project.org/doc/qt-5/metaobjects.html

ABI(Application binary interface):http://en.wikipedia.org/wiki/Application_binary_interface

FFI(Foreign function interface):http://en.wikipedia.org/wiki/Foreign_function_call

 vc如何返回函式結果及壓棧引數:http://blog.csdn.net/soloist/article/details/1267147

1、前傳

 “不同語言之間進行混合程式設計是計算機領域一個火熱的話題,一些軟體技術和工具,如SWIG (Simplified Wrapper and Interface Generator)、COM(Component Object Model) 的目的就是為了解決不同語言之間的互通性。”

        “對於不同語言之間的互操作,主要解決三大問題:

(1)資料型別的識別(包括資料的儲存格式,訪問方式)。

(2)資料型別的轉換(型別系統如何互通)。

(3)物件生命週期管理。”

        “說到不同程式語言的混合呼叫,就涉及到兩種語言之間資料型別的互通,方法的呼叫,本質上就是要求兩種語言能夠互相通訊,打個比方,這就好比兩個國家之間的語言進行溝通,例如陽陽會講日語,龍龍會講法語,那麼他們怎麼才能進行溝通呢?一種方式是他們採用一種雙方都能聽懂的英語來溝通,另一種方式就是陽陽學會了說法語,龍龍學會了說日語。”

        “回到混合程式設計的話題,與此類似,一種方式就是兩種語言採用一種都支援的中間語言(如xml)進行通訊,這種方式的優點是語言間的耦合性較低,不需要對另一門語言的實現細節有了解,缺點是通過中間語言進行通訊所付出的代價;另一種方式就是充分了解另一門語言,包括資料型別,資料結構的組織,方法的呼叫形式等,這種方式的優點是互操作的效能高,缺點是需要底層實現繁瑣。”

        “說到為什麼要進行js與c 的混合開發,原因是他們各有自己的優缺點。js的物件模型比較靈活,支援物件屬性的動態建立和刪除,支援閉包,但是效能不如c 高,而且侷限於瀏覽器,否則需要有js引擎的支援。c 的優點就是效能比較高,但是是一門靜態型別語言,靈活性不足。結合二者的優點,我們可以將邏輯程式碼部分採用js來實現,大量計算部分採用c 來實現。”

2、Qt的物件模型的介紹

        C 的靜態特性不夠靈活,為了滿足GUI程式設計對執行時高效性和靈活性的要求,Qt對標準C 物件模型進行了擴充套件(http://qt-project.org/doc/qt-5/object.html),這些擴充套件都基於了Qt中兩個十分重要的兩個概念:

(1)QObject

(2)Meta-Object

        QObject是所有Qt物件的基類,也是Qt物件模型的核心,擴充套件的大部分能力都是基於QObject實現。Qt 元物件系統(Meta-Object System)負責Qt中基於signals和slots的物件通訊機制,執行期型別資訊以及Qt的屬性(Q_PROPERTY)系統,對於QObject以及它的子類所建立的物件,都會為其建立一個QMetaObject例項,該例項中儲存了所有的關於該物件的元資訊(屬性的型別,方法的簽名資訊等),這些都由Qt所實現的Meta-Object
Compiler(moc)來提供。

        moc會為每一個包含Q_OBJECT巨集的類建立一個新的C 原始檔,以“moc_”打頭,該檔案中包含類的元資訊,該檔案一般會被單獨編譯並與原類檔案連結使用,也可以被#include到原類檔案中使用。

關於moc,Qt官方文件中的介紹如下:

The meta-object system is based on three things:

(1)The QObject class provides a base class for objects that can take advantage of the meta-object system.

(2)The Q_OBJECT macro inside the private section of the class declaration is used to enable meta-object features, such as dynamic properties, signals, and slots.

(3)The Meta-Object Compiler (moc) supplies each QObject subclass with the necessary code to implement meta-object features.

        瞭解了這些,我們也就知道了Qt中Javascript和C 的混合呼叫採用的是前面所提到的第二種方式進行通訊,而不是通過中間語言。

3、JS呼叫C

        我想這就是FFI中所說的,“In most cases, a FFI is defined by a “higher-level” language, so that it may employ services defined and implemented in a lower level language, typically a systems language like
C or C . ”

Qt中,JS訪問C 有兩種方式:

(1)一種是將自定義的C 型別註冊到QML的型別系統中。

class MyClass : public QObject//注意:必須要求是QObject派生的類
{
Q_OBJECT //為了使用Meta-Object System,這個巨集是必須的
Q_PROPERTY(int name READ name WRITE setName)//定義可以在js中被查詢和修改的屬性
public:
Q_INVOKABLE void foo();//定義可以在js中訪問的方法
int name();
void setName(int n);
private:
int m_name;
};

qmlRegisterType<MyClass>(“mytype”, 1, 0, “MyClass”);//將MyClass註冊到QML的型別系統中,以便在qml中使用該型別。

//a.直接在qml檔案中建立元素
import QtQuick 2.2
import mytype 1.0
Window{
id:mywindow
MyClass{
id:myObj
name:“longlong”
Component.onComplete:{
console.log(myObj.name);
}
};
}
//b.JS中動態建立物件
var myObj = Qt.createQmlObject('import QtQuick 2.2;  import my 1.0;MyClass{name:“yangyang”}’,mywindow, “dynamicMyClassCreate");
//c.將MyClass寫入一個單獨的qml檔案,動態建立物件
//myclass.qml
import QtQuick 2.2
import mytype 1.0
MyClass{
name:”longlong-yangyang”
}
var myObj = Qt.createComponent("myclass.qml");

(2)另一種是將c 中定義的物件放到js的全域性變數中。

QQmlApplicationEngine engine;
MyClass object;//在C  程式碼中定義物件object
engine.rootContext()->setContextProperty("myObj", &object);//將object掛到QML engine的上下文中,這樣在任何QML檔案中,都可以通過myObj.property和myObj.method()的方式訪問myObj了。不過這種方式的效能是比較低。

“Note: Since all expressions evaluated in QML are evaluated in a particular context, if the context is modified, all bindings in that context will be re-evaluated. Thus, context properties should be used with care
outside of application initialization, as this may lead to decreased application performance.”——http://qt-project.org/doc/qt-5/qtqml-cppintegration-contextproperties.html

        Javascript訪問c 屬於指令碼語言對靜態語言的呼叫範疇,那麼涉及到屬性的訪問,方法的呼叫,要想做到這些事情js需要考慮哪些問題呢?

        首先,js要想要訪問屬性,需要了解c 的型別,資料的儲存結構,已經如何得到資料並轉換資料型別;另外,js想要呼叫c 中的方法,需要考慮如何找到方法的實現,如何像方法中傳遞引數,如何跳轉到方法中去執行c 的方法,如何從c 方法中返回到js中繼續執行。這裡我們來了解一下lua中呼叫c過程的一個例子:

a.c檔案中定義了add方法,如下:

    //a.c
int add(int a, int b){
return a b;
}

為了能讓lua中呼叫到c函式,lua要求提供一個wrap檔案,對add函式進行包裝,這裡定義到wrap_a.c檔案中,如下:

//wrap_a.c,這裡還是c檔案
int wrap_add(LuaState* L){
int tmp1 = Lua_checkValueInt(L,1);
int tmp2 = Lua_checkValueInt(L,2);
int result = add(tmp1, tmp2);
Lua_pushValue(L,result);
return 1;
}

         Lua規定,wrap函式的引數是LuaState*這樣一個引數,這裡就是lua中實現的c的呼叫過程,在呼叫wrap函式時,會分配一個LuaState(可以理解為一個棧),將引數放到LuaState中(壓棧過程),在wrap_add函式中,從LuaState取到引數並檢查型別,最後將返回值壓棧並返回返回值個數(Lua中支援多個返回值)。

        這樣我們在Lua中就可以以myModule.add(1,2);的方式呼叫c的介面(在這之前還需要建立一張表,如table = {“add”,wrap_add},來告訴lua直譯器函式名的對映)。當然,內部需要lua的執行時支援對c函式的呼叫,才能呼叫到wrap_add介面(關於這個過程涉及到更底層的abi層面的東西)。

        瞭解Lua對c的呼叫,我們再回過頭來思考Qt中Javascript對C 呼叫這個問題,我們考慮一下前面所提到的兩個問題:

(1)資料型別的識別(包括資料的儲存格式,訪問方式)。

(2)資料型別的轉換(型別系統如何互通)。

物件是採用C 中的類建立的,也是按照C 的資料儲存格式儲存的,JavaScript要想去訪問,就必須要了解這一切,才能夠認識C 中的資料,進而才能做資料型別轉換的事情。那麼JavaScript如何才能知道這些呢?答案只有一個:Meta-Object System。想想為了讓JS能夠認識C 的型別,我們做了什麼?類必須由QObject繼承而來,類中必須使用Q_OBJECT巨集,如果想要在QML中使用該型別還必須呼叫qmlRegisterType進行型別註冊,這一切都是為了讓JS引擎更懂我們定義的型別,也就是讓JS引擎知道我們定義型別的元資訊,有了元資訊,Qt就可以幫我們做很多像Lua要求c做的事情,而這些都不需要使用者來操心了。

4、C 呼叫 JS

我想這又是FFI中所說的,“Many FFIs also provide the means for the called language to invoke services in the host language as well.”

Qt中,C 建立JS的物件的方式如下:   

// Using QQmlComponent
QQmlEngine engine;
QQmlComponent component(&engine,
QUrl::fromLocalFile(“MyItem.qml”));//若要動態建立,可以使用 QUrl::setData介面
QObject *object = component.create();
...
delete object;

//讀寫屬性
rx2 = object->property(“qmlObjProperty");
object->setProperty("qmlObjProperty", wx2);

//呼叫方法

 QMetaObject::invokeMethod(object, "myMethodInt",
Q_RETURN_ARG(QVariant, returnedValue),
Q_ARG(QVariant,100));

C 去操作JavaScript,也需要關心類似的事情:型別的識別,型別的轉換。

QML中的物件的型別分為兩種:

(1)一種是C 中定義的型別,註冊到QML中。這種物件的資料實際是按照C 的格式佈局,這種物件從JavaScript中傳到C 中,C 只要做一下指標型別轉換,就可以像操作C 物件一樣使用了。

(2)另一種是JavaScript型別的物件。這種型別的物件,不能直接轉換為C 物件使用,Qt中提供一種型別QJSValue,用於包裝JavaScript型別的物件。

如果我們並不知道從QML中建立的物件是上面的哪一種,或者我們就不關心它是哪一種,我們就可以按照上面程式碼的方式去統一操作,這裡的細節都被封裝在了Component中,而不需要使用者去操心這些。但是,這樣我們也必須遵照Component建立物件的操作規則,屬性訪問只能通過getter和setter介面,方法呼叫只能通過invokeMethod介面。這裡需要說明的是,這裡需要JS提供一些支援,例如根據屬性名找到對應的屬性和方法,從外部去invoke一個方法等(Qt從Qt
5.2版本後採用Qt自己實現的V4JS引擎,之前使用v8引擎)。

5、補充:關於JavaScript呼叫C 定義函式的引數傳遞

        關於Qt中JavaScript和C 混合呼叫引數的內部的自動型別轉換,可以參考:http://qt-project.org/doc/qt-5/qtqml-cppintegration-data.html,這裡我們只探討js中定義的物件作為引數傳遞給c 時的情況。

呼叫方式:

cOjb.foo(param);//其中cObj為C  型別的物件(如上文描述,該物件可能是在c程式碼中建立然後掛在js的全域性上下文中供js訪問,也可能是在js中建立)

C 中函式定義方式:

void MyClass::foo(<Type> cParam){
//do something
}

分為以下幾種情況:

js中定義的物件作為函式引數傳遞給c 函式

C 函式形參Type:QObject *

C 函式形參Type:QJSValue

C 函式形參Type:QVariantMap

js中實參param型別:QML Base Type(我只試了Qt.rect)

不支援,QObject * = 0;

支援,通過propert方法獲取屬性

不支援,QVariantMap為空

js中實參param型別param:JS字面量物件

不支援,QObject * = 0;

支援,通過propert方法獲取屬性

支援,屬於值傳遞

js中實參param型別param:c 註冊進來的型別定義的物件或者QML元素

支援,可以將QObject*強轉成指定型別的指標使用

支援,通過propert方法獲取屬性

支援,屬於值傳遞