├── .gitignore ├── demo.qml ├── Makefile ├── README.md ├── eqml.erl └── eqml.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | eqml 2 | eqml.beam 3 | eqml.make 4 | eqml.moc 5 | eqml.o 6 | eqml.pro 7 | 8 | -------------------------------------------------------------------------------- /demo.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.2 2 | 3 | Rectangle { 4 | objectName: 'foo' 5 | width: 200 6 | height: 200 7 | color: "red" 8 | 9 | Rectangle { 10 | objectName: 'bar' 11 | anchors.horizontalCenter: parent.horizontalCenter 12 | anchors.verticalCenter: parent.verticalCenter 13 | width: 100 14 | height: 100 15 | color: "green" 16 | 17 | MouseArea { 18 | anchors.fill: parent 19 | onPressed: { parent.color = "blue" } 20 | onReleased: { parent.color = "green"; parent.clicked("bro") } 21 | } 22 | 23 | signal clicked(var txt) 24 | } 25 | 26 | function scramble(txt) { 27 | console.log("md5(\"" + txt + "\") = " + Qt.md5(txt)) 28 | } 29 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ifndef OUT 2 | OUT=. 3 | endif 4 | 5 | all: port eqml.beam 6 | 7 | eqml.beam: eqml.erl 8 | @erlc -o $(OUT) eqml.erl 9 | 10 | port: eqml.make 11 | @make -s -f eqml.make 12 | 13 | eqml.make: eqml.pro 14 | @qmake eqml.pro -o eqml.make 15 | 16 | eqml.pro: 17 | @echo "TARGET = $(OUT)/eqml" > eqml.pro 18 | @echo "QT += qml quick widgets" >> eqml.pro 19 | @echo "SOURCES += eqml.cpp" >> eqml.pro 20 | @echo "INCLUDEPATH += `erl -noshell -eval "io:format(code:lib_dir(erl_interface, include)),erlang:halt(0)."`">> eqml.pro 21 | @echo "LIBS += -L`erl -noshell -eval "io:format(code:lib_dir(erl_interface, lib)),erlang:halt(0)."`" >> eqml.pro 22 | @echo "LIBS += -lei_st" >> eqml.pro 23 | 24 | clean: 25 | @rm -f $(OUT)/eqml $(OUT)/eqml.beam eqml.pro eqml.make eqml.o eqml.moc 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Erlang ⇔ QML bindings 2 | 3 | This package is in a pre-alpha stage. 4 | 5 | Installation 6 | ------------ 7 | 8 | To try the eqml you'll need: 9 | 10 | * Erlang 11 | * Qt 5 12 | 13 | Demo 14 | ----- 15 | Compile eqml and run erlang: 16 | 17 | make 18 | erl 19 | 20 | At the erlang prompt enter the following: 21 | 22 | eqml:start("demo.qml"). 23 | You should see a red window with a green square. Let's see how you can change QML properties from Erlang. Execute: 24 | 25 | eqml:set(foo, color, "yellow"). 26 | Window color will change to yellow. Now let's check how you can subscribe to QML signals: 27 | 28 | eqml:connect1(bar, clicked, hello). 29 | receive A -> A end. 30 | Erlang console will hang, waiting for message to come. Now click on green square. Erlang will print: 31 | 32 | {hello,"bro"} 33 | That's it. Let's check the final feature, QML function invocation from Erlang: 34 | 35 | eqml:invoke(foo, scramble, "wtf"). 36 | QML will print to console: 37 | 38 | md5("wtf") = aadce520e20c2899f4ced228a79a3083 39 | -------------------------------------------------------------------------------- /eqml.erl: -------------------------------------------------------------------------------- 1 | -module(eqml). 2 | -export([ 3 | start/1, init/1, send/2, set/3, 4 | connect0/3, connect1/3, connect2/3, connect3/3, connect4/3, connect5/3, 5 | invoke/2, invoke/3, invoke/4, invoke/5, 6 | url/1, 7 | point/2, 8 | datetime/2 9 | ]). 10 | 11 | start(QmlFile) -> 12 | proc_lib:start(eqml, init, [QmlFile]). 13 | 14 | init(QmlFile) -> 15 | PortName = filename:join(filename:dirname(code:which(eqml)), "eqml"), 16 | Port = open_port({spawn, PortName ++ " " ++ QmlFile}, [{packet, 4}, binary, nouse_stdio]), 17 | erlang:register(eqml, Port), 18 | proc_lib:init_ack(ok), 19 | 20 | loop(Port). 21 | 22 | loop(Port) -> 23 | receive 24 | {Port, {data, Data}} -> 25 | dispatch(binary_to_term(Data)), 26 | loop(Port); 27 | Other -> 28 | io:format("eqml: ~p~n", [Other]), 29 | loop(Port) 30 | end. 31 | 32 | dispatch({signal, To, Slot}) -> 33 | list_to_pid(To) ! Slot; 34 | dispatch({signal, To, Slot, A}) -> 35 | list_to_pid(To) ! {Slot, A}; 36 | dispatch({signal, To, Slot, A, B}) -> 37 | list_to_pid(To) ! {Slot, A, B}; 38 | dispatch({signal, To, Slot, A, B, C}) -> 39 | list_to_pid(To) ! {Slot, A, B, C}; 40 | dispatch({signal, To, Slot, A, B, C, D}) -> 41 | list_to_pid(To) ! {Slot, A, B, C, D}; 42 | dispatch({signal, To, Slot, A, B, C, D, E}) -> 43 | list_to_pid(To) ! {Slot, A, B, C, D, E}; 44 | dispatch(Unknown) -> 45 | io:format("eqml: can't dispatch ~p~n", [Unknown]). 46 | 47 | set(Obj, Prop, Val) -> 48 | send(set, {Obj, Prop, Val}). 49 | 50 | connect0(Obj, Signal, Tag) -> connect(Obj, Signal, Tag, 0). 51 | connect1(Obj, Signal, Tag) -> connect(Obj, Signal, Tag, 1). 52 | connect2(Obj, Signal, Tag) -> connect(Obj, Signal, Tag, 2). 53 | connect3(Obj, Signal, Tag) -> connect(Obj, Signal, Tag, 3). 54 | connect4(Obj, Signal, Tag) -> connect(Obj, Signal, Tag, 4). 55 | connect5(Obj, Signal, Tag) -> connect(Obj, Signal, Tag, 5). 56 | 57 | connect(Obj, Signal, Tag, Order) -> 58 | send(connect, {Obj, Signal, Tag, Order, pid_to_list(self())}). 59 | 60 | invoke(Obj, Member) -> 61 | send(invoke0, {Obj, Member}). 62 | invoke(Obj, Member, Arg0) -> 63 | send(invoke1, {Obj, Member, Arg0}). 64 | invoke(Obj, Member, Arg0, Arg1) -> 65 | send(invoke2, {Obj, Member, Arg0, Arg1}). 66 | invoke(Obj, Member, Arg0, Arg1, Arg2) -> 67 | send(invoke3, {Obj, Member, Arg0, Arg1, Arg2}). 68 | 69 | send(Tag, Msg) -> 70 | erlang:port_command(eqml, erlang:term_to_binary({Tag, Msg})). 71 | 72 | url(Url) -> 73 | {url, Url}. 74 | point(X, Y) -> 75 | {point, {X, Y}}. 76 | datetime(Date, Time) -> 77 | {datetime, {Date, Time}}. 78 | -------------------------------------------------------------------------------- /eqml.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | class eqmlTerm 10 | { 11 | const char * _buf; 12 | int _index; 13 | int _type; 14 | int _arity; 15 | int _size; 16 | public: 17 | eqmlTerm(const eqmlTerm &); 18 | eqmlTerm(const char * buf, int index = 0) : _buf(buf), _index(index) 19 | { 20 | if (index == 0) 21 | ei_decode_version(_buf, &_index, NULL); 22 | 23 | ei_get_type(_buf, &_index, &_type, &_size); 24 | 25 | if (isTuple()) 26 | ei_decode_tuple_header(_buf, &_index, &_arity); 27 | } 28 | 29 | eqmlTerm operator[] (int elem) const 30 | { 31 | int idx = _index; 32 | for (int i = 1; i < elem; ++i) 33 | ei_skip_term(_buf, &idx); 34 | 35 | return eqmlTerm(_buf, idx); 36 | } 37 | 38 | QByteArray atom() const 39 | { 40 | int idx = _index; char p[MAXATOMLEN]; 41 | ei_decode_atom(_buf, &idx, p); 42 | return QByteArray(p); 43 | } 44 | 45 | bool isTuple() const 46 | { 47 | return _type == ERL_SMALL_TUPLE_EXT || _type == ERL_LARGE_TUPLE_EXT; 48 | } 49 | 50 | bool isDouble() const 51 | { 52 | return _type == ERL_FLOAT_EXT || _type == NEW_FLOAT_EXT; 53 | } 54 | 55 | bool isAtom() const 56 | { 57 | return 58 | _type == ERL_ATOM_EXT || 59 | _type == ERL_SMALL_ATOM_EXT || 60 | _type == ERL_ATOM_UTF8_EXT || 61 | _type == ERL_SMALL_ATOM_UTF8_EXT; 62 | } 63 | 64 | bool isInteger() const 65 | { 66 | return 67 | _type == ERL_INTEGER_EXT || 68 | _type == ERL_SMALL_INTEGER_EXT || 69 | _type == ERL_SMALL_BIG_EXT || 70 | _type == ERL_LARGE_BIG_EXT; 71 | } 72 | 73 | bool isString() const 74 | { 75 | return 76 | _type == ERL_LIST_EXT || 77 | _type == ERL_STRING_EXT || 78 | _type == ERL_NIL_EXT; 79 | } 80 | 81 | bool toBool() const 82 | { 83 | QByteArray a = atom(); 84 | if (a == "true" ) return true; 85 | if (a == "false") return false; 86 | 87 | qWarning("can't cast term to bool"); 88 | return false; 89 | } 90 | 91 | int toInteger() const 92 | { 93 | int idx = _index; long p; 94 | ei_decode_long(_buf, &idx, &p); 95 | return p; 96 | } 97 | 98 | double toDouble() const 99 | { 100 | int idx = _index; double p; 101 | ei_decode_double(_buf, &idx, &p); 102 | return p; 103 | } 104 | 105 | double toScalar() const 106 | { 107 | if (isInteger()) 108 | return toInteger(); 109 | if (isDouble()) 110 | return toDouble(); 111 | 112 | qWarning("can't cast term to scalar"); 113 | return 0.0; 114 | } 115 | 116 | QString toString() const 117 | { 118 | int idx = _index; QByteArray a(_size, 0); 119 | ei_decode_string(_buf, &idx, a.data()); 120 | return QString(a); 121 | } 122 | 123 | QByteArray toArray() const 124 | { 125 | int idx = _index; QByteArray a(_size, 0); 126 | if (isAtom()) 127 | ei_decode_atom(_buf, &idx, a.data()); 128 | else 129 | ei_decode_string(_buf, &idx, a.data()); 130 | 131 | return a; 132 | } 133 | }; 134 | 135 | class eqmlLink : public QObject 136 | { 137 | Q_OBJECT 138 | 139 | char _buf[1024]; 140 | int _index; 141 | QDataStream & _os; 142 | 143 | void end(int index) 144 | { 145 | _os << index; 146 | _os.writeRawData(_buf, index); 147 | } 148 | 149 | int push(int index, const QVariant & v) 150 | { 151 | switch (v.type()) 152 | { 153 | case QMetaType::Int: 154 | ei_encode_long(_buf, &index, v.toInt()); 155 | break; 156 | case QMetaType::Double: 157 | ei_encode_double(_buf, &index, v.toDouble()); 158 | break; 159 | case QMetaType::QString: 160 | ei_encode_string(_buf, &index, qPrintable(v.toString())); 161 | break; 162 | default: 163 | qWarning("can't cast QVariant to term"); 164 | } 165 | return index; 166 | } 167 | 168 | public: 169 | eqmlLink(QDataStream & os, QObject * obj, const eqmlTerm & term) 170 | : _index(0) 171 | , _os(os) 172 | { 173 | int order = term[4].toInteger(); 174 | 175 | QByteArray Args = "(" + QByteArray("QVariant").repeated(order) + ")"; 176 | Args.replace("tQ", "t,Q"); 177 | 178 | int signalIdx = obj->metaObject()->indexOfSignal(term[2].toArray() + Args); 179 | QMetaMethod signalMethod = obj->metaObject()->method(signalIdx); 180 | 181 | int slotIdx = metaObject()->indexOfSlot("link" + Args); 182 | QMetaMethod slotMethod = metaObject()->method(slotIdx); 183 | 184 | if (!QObject::connect(obj, signalMethod, this, slotMethod)) 185 | qWarning("connection fail"); 186 | 187 | ei_encode_version(_buf, &_index); 188 | ei_encode_tuple_header(_buf, &_index, order + 3); 189 | ei_encode_atom(_buf, &_index, "signal"); 190 | ei_encode_string(_buf, &_index, term[5].toArray().data()); 191 | ei_encode_atom(_buf, &_index, term[3].toArray().data()); 192 | } 193 | 194 | public slots: 195 | void link() 196 | { 197 | end(_index); 198 | } 199 | 200 | void link(const QVariant& a) 201 | { 202 | end(push(_index, a)); 203 | } 204 | 205 | void link(const QVariant& a, const QVariant& b) 206 | { 207 | end(push(push(_index, a), b)); 208 | } 209 | 210 | void link(const QVariant& a, const QVariant& b, const QVariant& c) 211 | { 212 | end(push(push(push(_index, a), b), c)); 213 | } 214 | 215 | void link(const QVariant& a, const QVariant& b, const QVariant& c, const QVariant& d) 216 | { 217 | end(push(push(push(push(_index, a), b), c), d)); 218 | } 219 | 220 | void link(const QVariant& a, const QVariant& b, const QVariant& c, const QVariant& d, const QVariant& e) 221 | { 222 | end(push(push(push(push(push(_index, a), b), c), d), e)); 223 | } 224 | }; 225 | 226 | class eqmlPipe : public QThread 227 | { 228 | Q_OBJECT 229 | public: 230 | void run() 231 | { 232 | QFile inFile; 233 | inFile.open(3, QIODevice::ReadOnly | QIODevice::Unbuffered); 234 | QDataStream inStream(&inFile); 235 | 236 | QByteArray buffer; 237 | quint32 len; 238 | 239 | next: 240 | inStream >> len; 241 | if (inStream.status() == QDataStream::Ok) 242 | { 243 | buffer.resize(len); 244 | if (inStream.readRawData(buffer.data(), len) >= 0) 245 | { 246 | emit packet(buffer); 247 | goto next; 248 | } 249 | } 250 | } 251 | signals: 252 | void packet(QByteArray a); 253 | }; 254 | 255 | class eqmlWindow : public QQuickView 256 | { 257 | Q_OBJECT 258 | 259 | typedef void (eqmlWindow::*Handle)(const eqmlTerm &); 260 | typedef QMap Registry; 261 | Registry registry; 262 | 263 | QFile outFile; 264 | QDataStream outStream; 265 | 266 | typedef QVariant (eqmlWindow::*VarFun)(const eqmlTerm &); 267 | typedef QMap VarMap; 268 | VarMap _varMap; 269 | 270 | QObject * find(const eqmlTerm & term) 271 | { 272 | QByteArray name = term.toArray(); 273 | QObject * root = rootObject(); 274 | 275 | if (root->objectName() == name) 276 | return root; 277 | if (QObject * obj = root->findChild(name)) 278 | return obj; 279 | 280 | qWarning("can't find child %s", name.data()); 281 | return NULL; 282 | } 283 | 284 | public: 285 | eqmlWindow(const QString & qmlFile) 286 | { 287 | connect(engine(), SIGNAL(quit()), SLOT(close())); 288 | setResizeMode(QQuickView::SizeRootObjectToView); 289 | 290 | setSource(QUrl::fromLocalFile(qmlFile)); 291 | show(); 292 | 293 | outFile.open(4, QIODevice::WriteOnly | QIODevice::Unbuffered); 294 | outStream.setDevice(&outFile); 295 | 296 | registry["connect"] = &eqmlWindow::onConnect; 297 | registry["set"] = &eqmlWindow::onSet; 298 | registry["invoke0"] = &eqmlWindow::onInvoke0; 299 | registry["invoke1"] = &eqmlWindow::onInvoke1; 300 | registry["invoke2"] = &eqmlWindow::onInvoke2; 301 | registry["invoke3"] = &eqmlWindow::onInvoke3; 302 | 303 | _varMap["url"] = &eqmlWindow::url; 304 | _varMap["point"] = &eqmlWindow::point; 305 | _varMap["datetime"] = &eqmlWindow::datetime; 306 | } 307 | 308 | QVariant var(const eqmlTerm & t) 309 | { 310 | if (t.isInteger()) 311 | return t.toInteger(); 312 | else if (t.isDouble()) 313 | return t.toDouble(); 314 | else if (t.isString()) 315 | return t.toString(); 316 | else if (t.isAtom()) 317 | return t.toBool(); 318 | else if (t.isTuple()) { 319 | QByteArray tag = t[1].toArray(); 320 | 321 | VarMap::iterator it = _varMap.find(tag); 322 | if (it != _varMap.end()) 323 | return (this->*it.value())(t[2]); 324 | else 325 | qWarning("unknown var \'%s\'", tag.data()); 326 | } 327 | 328 | qWarning("can't cast term to QVariant"); 329 | return QVariant(); 330 | } 331 | 332 | QVariant url(const eqmlTerm & t) 333 | { 334 | return QUrl(t.toString()); 335 | } 336 | 337 | QVariant point(const eqmlTerm & t) 338 | { 339 | return QPointF(t[1].toScalar(), t[2].toScalar()); 340 | } 341 | 342 | QVariant datetime(const eqmlTerm & t) 343 | { 344 | const eqmlTerm & date = t[1]; 345 | const eqmlTerm & time = t[2]; 346 | 347 | return QDateTime( 348 | QDate(date[1].toInteger(), date[2].toInteger(), date[3].toInteger()), 349 | QTime(time[1].toInteger(), time[2].toInteger(), time[3].toInteger()) 350 | ); 351 | } 352 | 353 | void onConnect(const eqmlTerm & term) 354 | { 355 | QObject * obj = find(term[1]); 356 | if (!obj) 357 | return; 358 | 359 | new eqmlLink(outStream, obj, term); 360 | } 361 | 362 | void onInvoke0(const eqmlTerm & t) 363 | { 364 | if (QObject * obj = find(t[1])) 365 | QMetaObject::invokeMethod(obj, t[2].toArray()); 366 | } 367 | 368 | void onInvoke1(const eqmlTerm & t) 369 | { 370 | if (QObject * obj = find(t[1])) 371 | QMetaObject::invokeMethod(obj, t[2].toArray(), 372 | Q_ARG(QVariant, var(t[3]))); 373 | } 374 | 375 | void onInvoke2(const eqmlTerm & t) 376 | { 377 | if (QObject * obj = find(t[1])) 378 | QMetaObject::invokeMethod(obj, t[2].toArray(), 379 | Q_ARG(QVariant, var(t[3])), Q_ARG(QVariant, var(t[4]))); 380 | } 381 | 382 | void onInvoke3(const eqmlTerm & t) 383 | { 384 | if (QObject * obj = find(t[1])) 385 | QMetaObject::invokeMethod(obj, t[2].toArray(), 386 | Q_ARG(QVariant, var(t[3])), Q_ARG(QVariant, var(t[4])), Q_ARG(QVariant, var(t[5]))); 387 | } 388 | 389 | void onSet(const eqmlTerm & t) 390 | { 391 | if (QObject * obj = find(t[1])) 392 | obj->setProperty(t[2].toArray(), var(t[3])); 393 | } 394 | 395 | public slots: 396 | void dispatch(QByteArray buffer) 397 | { 398 | eqmlTerm term(buffer.data()); 399 | QByteArray tag = term[1].toArray(); 400 | 401 | Registry::iterator it = registry.find(tag); 402 | if (it != registry.end()) 403 | (this->*it.value())(term[2]); 404 | else 405 | qWarning("unknown tag \'%s\', size=%d", tag.data(), tag.size()); 406 | } 407 | }; 408 | 409 | void eqmlLog(QtMsgType, const QMessageLogContext &, const QString & msg) 410 | { 411 | fprintf(stderr, "%s\r\n", qPrintable(msg)); 412 | } 413 | 414 | int main(int argc, char *argv[]) 415 | { 416 | qInstallMessageHandler(eqmlLog); 417 | QApplication app(argc, argv); 418 | 419 | if (app.arguments().size() < 2) { 420 | qWarning("No QML file specified"); 421 | return -1; 422 | } 423 | 424 | eqmlPipe pipe; 425 | eqmlWindow win(app.arguments().at(1)); 426 | 427 | app.connect(&pipe, SIGNAL(finished()), SLOT(quit())); 428 | win.connect(&pipe, SIGNAL(packet(QByteArray)), SLOT(dispatch(QByteArray))); 429 | 430 | pipe.start(); 431 | return app.exec(); 432 | } 433 | 434 | #include "eqml.moc" 435 | --------------------------------------------------------------------------------