From 99ea5a081d0af6f39d7aebd4e2c3781615d61fdb Mon Sep 17 00:00:00 2001 From: "pls.153" Date: Fri, 11 Feb 2022 13:05:59 +0100 Subject: [PATCH] make example work on android; revisions --- .gitignore | 1 - cpp-lib/cpp/cpp.pro | 2 +- cpp-lib/cpp/lib.cpp | 2 +- cpp-lib/readme.md | 13 +++- doc/help.htm | 8 +- examples/9999/app.asd | 6 ++ examples/9999/app.pro | 53 +++++++++++++ examples/9999/app.qrc | 5 ++ examples/9999/build-android/.gitignore | 2 + examples/9999/build-ios/.gitignore | 2 + examples/9999/build/.gitignore | 2 + examples/9999/lisp/main.lisp | 2 +- examples/9999/lisp/package.lisp | 6 ++ examples/9999/make.lisp | 47 ++++++++++++ examples/9999/mkdirs.sh | 3 + examples/9999/ql-libs.lisp | 3 + examples/9999/qml/main.qml | 46 +++++++----- examples/9999/readme-build.md | 32 ++++++++ examples/9999/run.lisp | 9 ++- .../android/build-ecl/1-make-ecl-host.sh | 11 +++ .../android/build-ecl/2-make-ecl-android.sh | 20 +++++ platforms/android/cross-compile.lisp | 39 ++++++++++ platforms/shared/make.lisp | 75 +++++++++++++++++++ readme-build.md | 45 ++++++++--- doc/get-qt6.md => readme-qt.md | 0 readme.md | 5 +- ...start-swank.lisp => lqml-start-swank.lisp} | 0 slime/readme.md | 4 +- src/build-android/.gitignore | 2 + src/build-ios/.gitignore | 2 + src/build/.gitignore | 2 + src/cpp/ecl_ext.cpp | 4 +- src/cpp/ecl_ext.h | 12 +-- src/cpp/lqml.cpp | 13 ++-- src/cpp/lqml.h | 2 + src/cpp/main.cpp | 32 ++++---- src/cpp/marshal.cpp | 1 - src/cpp/marshal.h | 10 ++- src/cpp/qt_ecl.cpp | 1 - src/cpp/single_shot.cpp | 25 +++++++ src/cpp/single_shot.h | 24 +++--- src/lisp/ecl-ext.lisp | 32 ++++++++ src/lisp/ini.lisp | 11 ++- src/lisp/package.lisp | 4 +- src/lisp/qml.lisp | 9 ++- src/lqml-lib.pro | 61 +++++++++++++++ src/lqml.pro | 33 +++----- src/make.lisp | 44 ++++++++--- src/mkdirs.sh | 3 + 49 files changed, 637 insertions(+), 133 deletions(-) create mode 100644 examples/9999/app.asd create mode 100644 examples/9999/app.pro create mode 100644 examples/9999/app.qrc create mode 100644 examples/9999/build-android/.gitignore create mode 100644 examples/9999/build-ios/.gitignore create mode 100644 examples/9999/build/.gitignore create mode 100644 examples/9999/lisp/package.lisp create mode 100644 examples/9999/make.lisp create mode 100755 examples/9999/mkdirs.sh create mode 100644 examples/9999/ql-libs.lisp create mode 100644 examples/9999/readme-build.md create mode 100755 platforms/android/build-ecl/1-make-ecl-host.sh create mode 100755 platforms/android/build-ecl/2-make-ecl-android.sh create mode 100644 platforms/android/cross-compile.lisp create mode 100644 platforms/shared/make.lisp rename doc/get-qt6.md => readme-qt.md (100%) rename slime/{qml-start-swank.lisp => lqml-start-swank.lisp} (100%) create mode 100644 src/build-android/.gitignore create mode 100644 src/build-ios/.gitignore create mode 100644 src/build/.gitignore create mode 100644 src/cpp/single_shot.cpp create mode 100644 src/lisp/ecl-ext.lisp create mode 100644 src/lqml-lib.pro create mode 100755 src/mkdirs.sh diff --git a/.gitignore b/.gitignore index 3dd0d9c..6fd6f3f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ TODO lqml -build _* *.a *.fas* diff --git a/cpp-lib/cpp/cpp.pro b/cpp-lib/cpp/cpp.pro index d38aaca..e40fc9e 100644 --- a/cpp-lib/cpp/cpp.pro +++ b/cpp-lib/cpp/cpp.pro @@ -16,7 +16,7 @@ win32 { HEADERS += \ lib.h \ ../../src/cpp/marshal.h \ - ../../src/cpp/qt_ech.h + ../../src/cpp/qt_ecl.h SOURCES += \ lib.cpp \ diff --git a/cpp-lib/cpp/lib.cpp b/cpp-lib/cpp/lib.cpp index 8c91c3a..9e45f5d 100644 --- a/cpp-lib/cpp/lib.cpp +++ b/cpp-lib/cpp/lib.cpp @@ -20,7 +20,7 @@ QObject* ini() { return cpp; } -// functiones defined Q_INVOKABLE +// functions defined Q_INVOKABLE QVariant CPP::hello(const QVariant& arg) { diff --git a/cpp-lib/readme.md b/cpp-lib/readme.md index 77a10a8..9fef8cc 100644 --- a/cpp-lib/readme.md +++ b/cpp-lib/readme.md @@ -23,9 +23,18 @@ the argument and return type simply being defined as `QVariant`, you may also pass lists, because a `QVariant` can also be of type `QVariantList`, so this is a perfect fit for (nested) Lisp lists. -So, we pass a nested Lisp list, and it gets shown on Qt side with the -respective types. Then the `QVariantList` is returned to Lisp, where it is +So, we pass a nested Lisp list, and it gets converted and shown on Qt side with +the respective types. Then the `QVariantList` is returned to Lisp, where it is automatically converted back to a nested Lisp list. Really convenient! +From the second function -- which calls back to Lisp -- we can see that it +suffices to simply pass some intuitive, primitive C++ values to `ecl_fun`, +which will be converted automatically (using `QVariant`) to the appropriate +Lisp values. + +**Conclusion**: by only allowing `QVariant` arguments for calls between Lisp +and C++/Qt, we simplify things to a point where it becomes trivial, especially +considering nested lists on both sides. + diff --git a/doc/help.htm b/doc/help.htm index 27c55b8..be685f8 100644 --- a/doc/help.htm +++ b/doc/help.htm @@ -115,7 +115,7 @@ (qget *quick-view* |width|) -qjs (method-name item/name &rest arguments +qjs (method-name item/name &rest arguments) Fast and convenient way to call JS functions defined in QML. You may pass up to 10 arguments of the following types: @@ -123,6 +123,8 @@ mentioned arguments. N.B: Does not work with JS default arguments. + (qjs |drawLine| *canvas* x1 y1 x2 y2)) + qlater (function) @@ -137,7 +139,7 @@ The plugin will be reloaded (if supported by the OS) every time you call this function. If the UNLOAD argument is not NIL, the plugin will be unloaded (if supported by the OS). - N.B: This works only for Qt6 functions with the following signature: + N.B: This works only for Qt functions with the following signature: "QVariant foo(QVariant, ...)" ; max 10 QVariant arguments Since a QVariant can also be of type QVariantList, this is a perfect fit for (nested) Lisp lists. @@ -147,7 +149,7 @@ (define-qt-wrappers *c++*) ; Lisp wrapper functions -qlog (arg1 &optional arg2 arg3...) +qlog (arg1 &rest args) For log messages on android. diff --git a/examples/9999/app.asd b/examples/9999/app.asd new file mode 100644 index 0000000..c7eeb5f --- /dev/null +++ b/examples/9999/app.asd @@ -0,0 +1,6 @@ +(defsystem :app + :serial t + :depends-on () + :components ((:file "lisp/package") + (:file "lisp/main"))) + diff --git a/examples/9999/app.pro b/examples/9999/app.pro new file mode 100644 index 0000000..2e93dfb --- /dev/null +++ b/examples/9999/app.pro @@ -0,0 +1,53 @@ +# changes to these files will re-compile the Lisp library when running 'make' +LISP_FILES = \ + lisp/package.lisp \ + lisp/main.lisp \ + app.asd \ + make.lisp + +android { + lisp.commands = $$(ECL_ANDROID)/../ecl-android-host/bin/ecl \ + -norc -shell $$PWD/make.lisp +} else:ios { + lisp.commands = $$(ECL_IOS)/../ecl-ios-host/bin/ecl \ + -norc -shell $$PWD/make.lisp +} else:unix { + lisp.commands = ecl -shell $$PWD/make.lisp +} + +lisp.input = LISP_FILES +lisp.output = tmp/libapp.a + +QMAKE_EXTRA_COMPILERS += lisp + +QT += quick qml +TEMPLATE = app +CONFIG += no_keywords release +DEFINES += INI_LISP +INCLUDEPATH = /usr/local/include +LIBS = -L/usr/local/lib -lecl +DESTDIR = . +TARGET = app +OBJECTS_DIR = ./tmp +MOC_DIR = ./tmp + +linux: LIBS += -L../../../platforms/linux/lib -llqml -llisp -Ltmp -lapp +macx: LIBS += -L../../../platforms/macos/lib -llqml -llisp -Ltmp -lapp + +android { + QT += androidextras + INCLUDEPATH = $$(ECL_ANDROID)/include + LIBS = -L$$(ECL_ANDROID)/lib -lecl + LIBS += -L../../../platforms/android/lib -llqml -llisp -Ltmp -lapp + + ANDROID_ABIS = "arm64-v8a" + ANDROID_EXTRA_LIBS += $$(ECL_ANDROID)/lib/libecl.so + #ANDROID_PACKAGE_SOURCE_DIR = ../platforms/android/sources +} + +SOURCES += ../../src/cpp/main.cpp + +RESOURCES = app.qrc + +QMAKE_CXXFLAGS += -std=c++17 + diff --git a/examples/9999/app.qrc b/examples/9999/app.qrc new file mode 100644 index 0000000..8050b1b --- /dev/null +++ b/examples/9999/app.qrc @@ -0,0 +1,5 @@ + + + qml/main.qml + + diff --git a/examples/9999/build-android/.gitignore b/examples/9999/build-android/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/examples/9999/build-android/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/examples/9999/build-ios/.gitignore b/examples/9999/build-ios/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/examples/9999/build-ios/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/examples/9999/build/.gitignore b/examples/9999/build/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/examples/9999/build/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/examples/9999/lisp/main.lisp b/examples/9999/lisp/main.lisp index 38fa3d7..4b1101c 100644 --- a/examples/9999/lisp/main.lisp +++ b/examples/9999/lisp/main.lisp @@ -1,4 +1,4 @@ -(in-package :qml-user) +(in-package :app) (defvar *number* 0) diff --git a/examples/9999/lisp/package.lisp b/examples/9999/lisp/package.lisp new file mode 100644 index 0000000..36c9d20 --- /dev/null +++ b/examples/9999/lisp/package.lisp @@ -0,0 +1,6 @@ +(defpackage :app + (:use :cl :qml) + (:export + #:draw-number + #:paint)) + diff --git a/examples/9999/make.lisp b/examples/9999/make.lisp new file mode 100644 index 0000000..d88da62 --- /dev/null +++ b/examples/9999/make.lisp @@ -0,0 +1,47 @@ +(when (search "/ecl-android/" (first (ext:command-args))) + (pushnew :android *features*)) + +(require :asdf) + +(push (merge-pathnames "../") + asdf:*central-registry*) + +(setf *default-pathname-defaults* + (truename (merge-pathnames "../../../"))) ; LQML root + +(defvar *current* + (let ((name (namestring *load-truename*))) + (subseq name + (length (namestring *default-pathname-defaults*)) + (position #\/ name :from-end t)))) + +;; load all LQML symbols +(dolist (file (list "package" "x" "ecl-ext" "ini" "qml")) + (load (merge-pathnames file "src/lisp/"))) + +(defun cc (&rest args) + (apply 'concatenate 'string args)) + +#-android +(progn + (asdf:make-build "app" + :monolithic t + :type :static-library + :move-here (cc *current* "/build/tmp/") + :init-name "ini_app") + (let* ((from (cc *current* "/build/tmp/app--all-systems.a")) + (to "libapp.a") + (to* (cc *current* "/build/tmp/" to))) + (when (probe-file to*) + (delete-file to*)) + (rename-file from to))) + +#+android +(progn + (defvar *asdf-system* "app") + (defvar *ql-libs* (cc *current* "/ql-libs.lisp")) + (defvar *library-name* (cc *current* "/build-android/tmp/app")) + (defvar *init-name* "ini_app") + (defvar *epilogue-code* nil) + (load "platforms/shared/make")) + diff --git a/examples/9999/mkdirs.sh b/examples/9999/mkdirs.sh new file mode 100755 index 0000000..ad7c56f --- /dev/null +++ b/examples/9999/mkdirs.sh @@ -0,0 +1,3 @@ +mkdir build +mkdir build-android +mkdir build-ios diff --git a/examples/9999/ql-libs.lisp b/examples/9999/ql-libs.lisp new file mode 100644 index 0000000..1b7f49c --- /dev/null +++ b/examples/9999/ql-libs.lisp @@ -0,0 +1,3 @@ +;;; define here eventual Quicklisp dependencies +;;; e.g. (ql:quickload :alessandria) + diff --git a/examples/9999/qml/main.qml b/examples/9999/qml/main.qml index 6dfc47a..8f91f2e 100644 --- a/examples/9999/qml/main.qml +++ b/examples/9999/qml/main.qml @@ -1,17 +1,37 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 +import QtQuick.Window 2.15 import Lisp 1.0 Rectangle { - width: 220 - height: 320 + input.height + id: main + width: 200 + height: 300 + input.height color: "lavender" + TextField { + id: input + objectName: "input" + width: parent.width + horizontalAlignment: Qt.AlignHCenter + text: "0000" + inputMask: "9999" + inputMethodHints: Qt.ImhDigitsOnly + focus: true + + onTextChanged: Lisp.call("app:draw-number", Number(text)) + } + Canvas { id: canvas objectName: "canvas" - width: 220 - height: 320 + y: input.height + width: parent.width + height: { + var h = Qt.inputMethod.keyboardRectangle.y + h = (h === 0) ? main.height : h / Screen.devicePixelRatio + return (h - input.height) + } property var ctx @@ -36,23 +56,13 @@ Rectangle { onPaint: { ctx = getContext("2d") ctx.reset() - ctx.translate(110, 160) + ctx.translate(canvas.width / 2, canvas.height / 2) + var s = height / 340 + ctx.scale(s, s) - Lisp.call("qml-user:paint") + Lisp.call("app:paint") ctx.stroke() } } - - TextField { - id: input - objectName: "input" - width: parent.width - anchors.bottom: parent.bottom - horizontalAlignment: Qt.AlignHCenter - text: "0000" - inputMask: "9999" - - onTextChanged: Lisp.call("qml-user:draw-number", Number(text)) - } } diff --git a/examples/9999/readme-build.md b/examples/9999/readme-build.md new file mode 100644 index 0000000..d5c36c5 --- /dev/null +++ b/examples/9999/readme-build.md @@ -0,0 +1,32 @@ + +Run desktop +----------- + +``` +$ lqml run.lisp +``` + + +Build desktop app +----------------- + +``` +$ cd build + +$ qmake ../app.pro +$ make +``` + + +Build android APK +----------------- + +``` +$ cd build-android + +$ qmake-android ../app.pro +$ make apk + +$ adb install -r android-build/*.apk +``` + diff --git a/examples/9999/run.lisp b/examples/9999/run.lisp index 7a77ab1..861e5b0 100644 --- a/examples/9999/run.lisp +++ b/examples/9999/run.lisp @@ -1,12 +1,17 @@ (in-package :qml-user) -(load "lisp/main") +(require :asdf) + +(push (merge-pathnames "./") + asdf:*central-registry*) + +(asdf:operate 'asdf:load-source-op :app) (qset *quick-view* |x| 75 |y| 75) -;;; for Slime after copying 'qml-start-swank.lisp' from LQML sources +;;; for Slime after copying 'lqml-start-swank.lisp' from LQML sources ;;; to your Slime directory, which is assumed to be '~/slime/' (when (find "-slime" (ext:command-args) :test 'string=) diff --git a/platforms/android/build-ecl/1-make-ecl-host.sh b/platforms/android/build-ecl/1-make-ecl-host.sh new file mode 100755 index 0000000..852c608 --- /dev/null +++ b/platforms/android/build-ecl/1-make-ecl-host.sh @@ -0,0 +1,11 @@ +# build the host ECL, which will then be used +# to build the cross-compiled Android version +# (assumes a 64bit platform) + +./configure CFLAGS="-g -O2" LDFLAGS="-g -O2" CC=clang \ + --prefix=`pwd`/ecl-android-host \ + --disable-c99complex \ + --enable-manual=no +make +make install +rm -r build diff --git a/platforms/android/build-ecl/2-make-ecl-android.sh b/platforms/android/build-ecl/2-make-ecl-android.sh new file mode 100755 index 0000000..ea4756b --- /dev/null +++ b/platforms/android/build-ecl/2-make-ecl-android.sh @@ -0,0 +1,20 @@ +# use the previously built host ECL to build the android version +# requires NDK >= 19 +# you need to define ANDROID_NDK_TOOLCHAIN + +export AR=$ANDROID_NDK_TOOLCHAIN/bin/aarch64-linux-android-ar +export AS=$ANDROID_NDK_TOOLCHAIN/bin/aarch64-linux-android-as +export CC=$ANDROID_NDK_TOOLCHAIN/bin/aarch64-linux-android21-clang +export LD=$ANDROID_NDK_TOOLCHAIN/bin/aarch64-linux-android-ld +export RANLIB=$ANDROID_NDK_TOOLCHAIN/bin/aarch64-linux-android-ranlib +export STRIP=$ANDROID_NDK_TOOLCHAIN/bin/aarch64-linux-android-strip + +export ECL_TO_RUN=`pwd`/ecl-android-host/bin/ecl + +./configure --host=aarch64-linux-android \ + --prefix=`pwd`/ecl-android \ + --disable-c99complex \ + --enable-manual=no \ + --with-cross-config=`pwd`/src/util/android-arm64.cross_config +make +make install diff --git a/platforms/android/cross-compile.lisp b/platforms/android/cross-compile.lisp new file mode 100644 index 0000000..44e691d --- /dev/null +++ b/platforms/android/cross-compile.lisp @@ -0,0 +1,39 @@ +;;; please use NDK versions >= 19 (with prebuilt standalone toolchain) + +(in-package :cl-user) + +(pushnew :android *features*) +(pushnew :aarch64 *features*) + +(require :cmp) + +(defvar *ndk-toolchain* (ext:getenv "ANDROID_NDK_TOOLCHAIN")) +(defvar *ecl-android* (ext:getenv "ECL_ANDROID")) +(defvar *architecture* "aarch64-linux-android") + +(defun cc (&rest arguments) + (apply 'concatenate 'string arguments)) + +(setf c::*ecl-include-directory* (cc *ecl-android* "/include/") + c::*ecl-library-directory* (cc *ecl-android* "/lib/")) + +(defun ecl-config (flags) + (read-line (ext:run-program (cc *ecl-android* "/bin/ecl-config") + (list flags)))) + +(setf c::*cc* (let ((path (or (probe-file (cc *ndk-toolchain* "/bin/aarch64-linux-android21-clang")) + (error "clang compiler not found")))) + (namestring path)) + c::*ld* (cc *ndk-toolchain* "/bin/aarch64-linux-android-ld") + c::*ar* (cc *ndk-toolchain* "/bin/aarch64-linux-android-ar") + c::*ranlib* (cc *ndk-toolchain* "/bin/aarch64-linux-android-ranlib") + c::*cc-flags* (cc (ecl-config "--cflags") + " -DANDROID -DPLATFORM_ANDROID -O2 -fPIC -fno-common -D_THREAD_SAFE -I" + *ecl-android* "/build/gmp") + c::*ld-flags* (cc "-L" *ecl-android* "/lib -lecl -ldl -lm " + "-L" *ndk-toolchain* "/sysroot/usr/lib/aarch64-linux-android/") + c::*ld-rpath* nil + c::*ld-shared-flags* (cc "-shared " c::*ld-flags*) + c::*ld-bundle-flags* c::*ld-shared-flags*) + +(format t "~%*** cross compiling for 'aarch64' ***~%") diff --git a/platforms/shared/make.lisp b/platforms/shared/make.lisp new file mode 100644 index 0000000..6e0c562 --- /dev/null +++ b/platforms/shared/make.lisp @@ -0,0 +1,75 @@ +;;; cross-compile ASDF system, using the byte-codes compiler +;;; as an intermediate step + +;; optional vars, to be set in 'make.lisp' in your app dir +(defvar *epilogue-code* nil) +(defvar *ql-libs* nil) + +;;; *** (1) byte-compile ASDF system *** + +(si:install-bytecodes-compiler) + +(when *ql-libs* + (let ((quicklisp-init (merge-pathnames "quicklisp/setup.lisp" + (user-homedir-pathname)))) + (when (probe-file quicklisp-init) + (load quicklisp-init))) + (load *ql-libs*)) + +;;; load ASDF system and collect file names + +(defvar *source-files* nil) + +(defmethod asdf:perform ((o asdf:load-op) (c asdf:cl-source-file)) + (let ((source (namestring (asdf:component-pathname c)))) + (push (subseq source 0 (position #\. source :from-end t)) + *source-files*)) + (asdf::perform-lisp-load-fasl o c)) + +(asdf:load-system *asdf-system*) + +(setf *source-files* (nreverse *source-files*)) + +;;; *** (2) cross-compile *** + +;;; load and prepare cross-compiler + +(si:install-c-compiler) + +(load #+android (merge-pathnames "platforms/android/cross-compile")) + +(setf *load-verbose* nil + *compile-verbose* t) + +(setf c::*suppress-compiler-warnings* nil + c::*suppress-compiler-notes* nil + c::*compile-in-constants* t) + +(load (merge-pathnames "src/lisp/tr.lisp")) ; i18n + +(setf *break-on-signals* 'error) + +;;; compile/link manually (byte-compiled version is already loaded) + +(defvar *object-files* nil) + +(defun cc (&rest args) + (apply 'concatenate 'string args)) + +(dolist (file *source-files*) + (let ((src (cc file ".lisp")) + (obj (merge-pathnames (cc "src/.cache/" *architecture* file ".o")))) + (when (or (not (probe-file obj)) + (> (file-write-date src) + (file-write-date obj))) + (ensure-directories-exist obj) + (compile-file src :output-file obj :system-p t)) + (push obj *object-files*))) + +(setf *object-files* (nreverse *object-files*)) + +(c:build-static-library *library-name* + :lisp-files *object-files* + :init-name *init-name* + :epilogue-code *epilogue-code*) + diff --git a/readme-build.md b/readme-build.md index 3fe9f32..2a5b762 100644 --- a/readme-build.md +++ b/readme-build.md @@ -1,21 +1,46 @@ -Build ------ +Build executable +---------------- -Currently still using qmake, will be ported to CMake. +Currently still using qmake, will eventually be ported to CMake. -* make sure you have both **ECL** and **Qt6** installed -* make sure to use `qmake` from Qt6 - -**macOS**: please use ECL from development branch. +Please make sure you have both latest **ECL** (from development branch) and +either **Qt5.15** or **Qt6** installed (see [readme-qt](readme-qt.md)). ``` -$ cd src -$ mkdir build -$ cd build +$ cd src/build $ qmake ../lqml.pro $ make -j4 $ sudo make install ``` + +Build library +------------- + +To build the static library needed for building your own apps, do: + +* desktop +``` +$ cd src/build + +$ qmake ../lqml-lib.pro +$ make +``` +* android (note separate build directory) +``` +$ cd src/build-android + +$ qmake-android ../lqml-lib.pro +$ make +``` +* ios (note separate build directory) +``` +$ cd src/build-ios + +$ qmake-ios ../lqml-lib.pro +$ make +``` +The library files can be found under `platforms//lib/` + diff --git a/doc/get-qt6.md b/readme-qt.md similarity index 100% rename from doc/get-qt6.md rename to readme-qt.md diff --git a/readme.md b/readme.md index 97e821c..4540549 100644 --- a/readme.md +++ b/readme.md @@ -11,6 +11,10 @@ Description A lightweight ECL based QML-only binding to Qt5/Qt6. +This small project aims to simplify all the steps needed for building +cross-platform apps. The same sources can be used to build executables for both +desktop (Linux/macOS) and mobile (android/iOS). + License ------- @@ -33,7 +37,6 @@ port still lacks significant parts of mobile (as of Qt6.2). TODO ---- -* make example work on android * make example work on iOS * add item model example * add sokoban example diff --git a/slime/qml-start-swank.lisp b/slime/lqml-start-swank.lisp similarity index 100% rename from slime/qml-start-swank.lisp rename to slime/lqml-start-swank.lisp diff --git a/slime/readme.md b/slime/readme.md index 495f2f1..6e81072 100644 --- a/slime/readme.md +++ b/slime/readme.md @@ -2,7 +2,7 @@ Swank/Slime ----------- -Please put `qml-start-swank.lisp` in your `slime/` directory, and `.swank.lisp` -in your home directory. +Please put `lqml-start-swank.lisp` in your `slime/` directory, and +`.swank.lisp` in your home directory. Remember to always use `(qquit)` or `(qq)` to gracefully exit the Swank server. diff --git a/src/build-android/.gitignore b/src/build-android/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/src/build-android/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/src/build-ios/.gitignore b/src/build-ios/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/src/build-ios/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/src/build/.gitignore b/src/build/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/src/build/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/src/cpp/ecl_ext.cpp b/src/cpp/ecl_ext.cpp index 8fa78b4..62e37a3 100644 --- a/src/cpp/ecl_ext.cpp +++ b/src/cpp/ecl_ext.cpp @@ -214,7 +214,7 @@ cl_object qload_cpp(cl_object l_lib_name, cl_object l_unload) { /// qload-c++ /// The plugin will be reloaded (if supported by the OS) every time you call /// this function. If the UNLOAD argument is not NIL, the plugin will be /// unloaded (if supported by the OS). - /// N.B: This works only for Qt6 functions with the following signature: + /// N.B: This works only for Qt functions with the following signature: /// "QVariant foo(QVariant, ...)" ; max 10 QVariant arguments /// Since a QVariant can also be of type QVariantList, this is a perfect fit /// for (nested) Lisp lists. @@ -348,7 +348,7 @@ cl_object qsingle_shot2(cl_object l_msec, cl_object l_fun) { /// (qsingle-shot 1000 'one-second-later) ecl_process_env()->nvalues = 1; if (l_fun != ECL_NIL) { - new SingleShot(toInt(l_msec), l_fun); + new SingleShot(toInt(l_msec), l_fun); // see 'deleteLater()' in sources return l_msec; } error_msg("QSINGLE-SHOT", LIST2(l_msec, l_fun)); diff --git a/src/cpp/ecl_ext.h b/src/cpp/ecl_ext.h index 472f59b..6adaaa9 100644 --- a/src/cpp/ecl_ext.h +++ b/src/cpp/ecl_ext.h @@ -1,6 +1,8 @@ #ifndef ECL_EXT_H #define ECL_EXT_H +#undef SLOT + #include #include #include @@ -8,7 +10,7 @@ QT_BEGIN_NAMESPACE #define DEFUN(name, c_name, num_args) \ - ecl_def_c_function(ecl_read_from_cstring(name), (cl_objectfn_fixed)c_name, num_args); + ecl_def_c_function(ecl_read_from_cstring(name), (cl_objectfn_fixed)c_name, num_args); #define STRING(s) ecl_make_constant_base_string(s, -1) @@ -19,12 +21,12 @@ QT_BEGIN_NAMESPACE #define TERPRI() cl_terpri(0) #define STATIC_SYMBOL(var, name) \ - static cl_object var = cl_intern(1, ecl_make_constant_base_string(name, -1)); + static cl_object var = cl_intern(1, ecl_make_constant_base_string(name, -1)); #define STATIC_SYMBOL_PKG(var, name, pkg) \ - static cl_object var = cl_intern(2, \ - ecl_make_constant_base_string(name, -1), \ - cl_find_package(ecl_make_constant_base_string(pkg, -1))); + static cl_object var = cl_intern(2, \ + ecl_make_constant_base_string(name, -1), \ + cl_find_package(ecl_make_constant_base_string(pkg, -1))); #define LEN(x) fixint(cl_length(x)) diff --git a/src/cpp/lqml.cpp b/src/cpp/lqml.cpp index c8fbf64..1ce6c9e 100644 --- a/src/cpp/lqml.cpp +++ b/src/cpp/lqml.cpp @@ -7,7 +7,7 @@ #include #include -const char LQML::version[] = "22.1.1"; // Jan 2022 +const char LQML::version[] = "22.2.1"; // Feb 2022 extern "C" void ini_LQML(cl_object); @@ -31,7 +31,7 @@ static void logMessageHandler(QtMsgType, const QMessageLogContext& context, cons report += " function "; report += QString(context.function); } - __android_log_write(ANDROID_LOG_DEBUG, "[EQL5]", report.toLocal8Bit().constData()); + __android_log_write(ANDROID_LOG_DEBUG, "[LQML]", report.toLocal8Bit().constData()); } #endif @@ -44,11 +44,12 @@ LQML::LQML(int argc, char* argv[], QQuickView* view) : QObject() { qInstallMessageHandler(logMessageHandler); // see above #endif if (!cl_booted_p) { - cl_boot(argc, argv); } + cl_boot(argc, argv); + } iniCLFunctions(); ecl_init_module(NULL, ini_LQML); eval("(in-package :qml-user)"); - eval(QString("(setf *quick-view* (qt-object %1))") + eval(QString("(setf qml:*quick-view* (qml:qt-object %1))") .arg(reinterpret_cast(view))); } @@ -87,7 +88,7 @@ void LQML::eval(const QString& lisp_code, bool slime) { safe_eval_debug(lisp_code.toLatin1().constData()); } else { cl_object ret = safe_eval(lisp_code.toLatin1().constData()); - if (ecl_t_of(ret) == t_fixnum && (fix(ret) == EVAL_ERROR_VALUE)) { + if ((ecl_t_of(ret) == t_fixnum) && (fix(ret) == EVAL_ERROR_VALUE)) { qDebug() << "Error evaluating " << lisp_code; exit(-1); } @@ -96,7 +97,7 @@ void LQML::eval(const QString& lisp_code, bool slime) { void LQML::ignoreIOStreams() { // [Windows] print output would cause a gui exe to crash (without console) - eval("(eql::ignore-io-streams)"); + eval("(qml::ignore-io-streams)"); } void LQML::exec(lisp_ini ini, const QByteArray& expression, const QByteArray& package) { diff --git a/src/cpp/lqml.h b/src/cpp/lqml.h index fa100ad..aba4a37 100644 --- a/src/cpp/lqml.h +++ b/src/cpp/lqml.h @@ -1,6 +1,8 @@ #ifndef LQML_H #define LQML_H +#undef SLOT + #include #include #include diff --git a/src/cpp/main.cpp b/src/cpp/main.cpp index b2c4a0c..2a697d4 100644 --- a/src/cpp/main.cpp +++ b/src/cpp/main.cpp @@ -15,6 +15,10 @@ #define ADD_MACOS_BUNDLE_IMPORT_PATH #endif +#ifdef INI_LISP + extern "C" void ini_app(cl_object); +#endif + int catch_all_qexec() { int ret = 0; CL_CATCH_ALL_BEGIN(ecl_process_env()) { @@ -25,6 +29,7 @@ int catch_all_qexec() { } int main(int argc, char* argv[]) { + QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QGuiApplication app(argc, argv); //app.setOrganizationName("MyProject"); //app.setOrganizationDomain("my.org"); @@ -49,25 +54,26 @@ int main(int argc, char* argv[]) { exit(0); } +#ifdef INI_LISP + ecl_init_module(NULL, ini_app); +#endif + new QQmlFileSelector(view.engine(), &view); QString qml("qml/main.qml"); - QUrl url("qrc:///" + qml); // (1) try resources first (final app) - bool set = false; - if (QFile::exists(url.fileName())) { - set = true; + QUrl url; + if (QFile::exists(qml)) { // (1) try local file (development) + url = QUrl::fromLocalFile(qml); } else { - url = QUrl::fromLocalFile(qml); // (2) use local file (development) - if (QFile::exists(QDir::currentPath() + "/" + qml)) { - set = true; - } + url = QUrl("qrc:///" + qml); // (2) use resource file (final app) } - if (set) { - view.setSource(url); - if (view.status() == QQuickView::Error) { - return -1; - } + view.setSource(url); + if (view.status() != QQuickView::Error) { view.setResizeMode(QQuickView::SizeRootObjectToView); +#if (defined Q_OS_ANDROID) || (defined Q_OS_IOS) + view.show(); +#else QTimer::singleShot(0, &view, &QQuickView::show); +#endif } // load .eclrc diff --git a/src/cpp/marshal.cpp b/src/cpp/marshal.cpp index 84cc900..676fca0 100644 --- a/src/cpp/marshal.cpp +++ b/src/cpp/marshal.cpp @@ -1,5 +1,4 @@ #include "marshal.h" -#include #include #include diff --git a/src/cpp/marshal.h b/src/cpp/marshal.h index 259475a..7f9b343 100644 --- a/src/cpp/marshal.h +++ b/src/cpp/marshal.h @@ -1,6 +1,8 @@ #ifndef MARSHAL_H #define MARSHAL_H +#undef SLOT + #include #include #include @@ -12,12 +14,12 @@ QT_BEGIN_NAMESPACE #define STRING_COPY(s) (s ? ecl_make_simple_base_string(s, -1) : ECL_NIL) #define STATIC_SYMBOL(var, name) \ - static cl_object var = cl_intern(1, ecl_make_constant_base_string(name, -1)); + static cl_object var = cl_intern(1, ecl_make_constant_base_string(name, -1)); #define STATIC_SYMBOL_PKG(var, name, pkg) \ - static cl_object var = cl_intern(2, \ - ecl_make_constant_base_string(name, -1), \ - cl_find_package(ecl_make_constant_base_string(pkg, -1))); + static cl_object var = cl_intern(2, \ + ecl_make_constant_base_string(name, -1), \ + cl_find_package(ecl_make_constant_base_string(pkg, -1))); #define LEN(x) fixint(cl_length(x)) diff --git a/src/cpp/qt_ecl.cpp b/src/cpp/qt_ecl.cpp index ae438c6..174ebb9 100644 --- a/src/cpp/qt_ecl.cpp +++ b/src/cpp/qt_ecl.cpp @@ -1,7 +1,6 @@ #include "qt_ecl.h" #include "marshal.h" #include "ecl_ext.h" -#include #include QT_BEGIN_NAMESPACE diff --git a/src/cpp/single_shot.cpp b/src/cpp/single_shot.cpp new file mode 100644 index 0000000..cccd5db --- /dev/null +++ b/src/cpp/single_shot.cpp @@ -0,0 +1,25 @@ +#include "single_shot.h" + +QT_BEGIN_NAMESPACE + +SingleShot::SingleShot(int msec, void* fun) + : function(fun) { + id = startTimer(msec); +} + +void SingleShot::timerEvent(QTimerEvent*) { + killTimer(id); + const cl_env_ptr l_env = ecl_process_env(); + CL_CATCH_ALL_BEGIN(l_env) { + CL_UNWIND_PROTECT_BEGIN(l_env) { + cl_funcall(1, (cl_object)function); + } + CL_UNWIND_PROTECT_EXIT {} + CL_UNWIND_PROTECT_END; + } + CL_CATCH_ALL_END; + deleteLater(); +} + +QT_END_NAMESPACE + diff --git a/src/cpp/single_shot.h b/src/cpp/single_shot.h index 833244a..4952912 100644 --- a/src/cpp/single_shot.h +++ b/src/cpp/single_shot.h @@ -1,30 +1,24 @@ #ifndef SINGLE_SHOT_H #define SINGLE_SHOT_H +#undef SLOT + #include #include QT_BEGIN_NAMESPACE -struct SingleShot : public QObject { +class SingleShot : public QObject { + Q_OBJECT + +public: int id; void* function; - SingleShot(int msec, void* fun) : id(startTimer(msec)), function(fun) {} + SingleShot(int, void*); - void timerEvent(QTimerEvent*) { - killTimer(id); - const cl_env_ptr l_env = ecl_process_env(); - CL_CATCH_ALL_BEGIN(l_env) { - CL_UNWIND_PROTECT_BEGIN(l_env) { - cl_funcall(1, (cl_object)function); - } - CL_UNWIND_PROTECT_EXIT {} - CL_UNWIND_PROTECT_END; - } - CL_CATCH_ALL_END; - delete this; - } +protected: + void timerEvent(QTimerEvent*) override; }; QT_END_NAMESPACE diff --git a/src/lisp/ecl-ext.lisp b/src/lisp/ecl-ext.lisp new file mode 100644 index 0000000..9a51d3f --- /dev/null +++ b/src/lisp/ecl-ext.lisp @@ -0,0 +1,32 @@ +;;; needed for cross-compiling + +(in-package :qml) + +(defun %js (a b)) +(defun pixel-ratio ()) +(defun %qapropos (a b c)) +(defun qchildren (a)) +(defun qescape (a)) +(defun %qexec (a)) +(defun qexit ()) +(defun qfind-child (a b)) +(defun %qfind-children (a b c)) +(defun qfrom-utf8 (a)) +(defun %qinvoke-method (a b c)) +(defun %qload-c++ (a b)) +(defun %qlog (a)) +(defun %qml-get (a b)) +(defun %qml-set (a b c)) +(defun qobject-name (a)) +(defun qprocess-events ()) +(defun %qquit (a)) +(defun %qrun-on-ui-thread (a b)) +(defun %qget (a b)) +(defun %qset (a b)) +(defun %qsingle-shot (a b)) +(defun qtranslate (a b c)) +(defun qversion ()) +(defun qt-object-info (a)) +(defun %reload ()) +(defun root-item ()) +(defun %set-shutdown-p (a)) diff --git a/src/lisp/ini.lisp b/src/lisp/ini.lisp index 13acdde..b6dc52e 100644 --- a/src/lisp/ini.lisp +++ b/src/lisp/ini.lisp @@ -58,11 +58,9 @@ ;; check for LAMBDA, #'LAMBDA (if (find (first function) '(lambda function)) ;; hold a reference (will be called later from Qt event loop) - `(qrun (lambda () - (%qsingle-shot ,milliseconds (setf (symbol-function (intern ,(%reference-name))) ; lambda - ,function)))) - `(qrun (lambda () - (%qsingle-shot ,milliseconds ,function))))) ; 'foo + `(qrun* (%qsingle-shot ,milliseconds (setf (symbol-function (intern ,(%reference-name))) ; lambda + ,function))) + `(qrun* (%qsingle-shot ,milliseconds ,function)))) ; 'foo (defmacro qlater (function) "args: (function) @@ -217,7 +215,7 @@ ;;; for android logging (defun qlog (arg1 &rest args) - "args: (arg1 &optional arg2 arg3...) + "args: (arg1 &rest args) For log messages on android. (qlog 12) (qlog \"width\" 10 \"height\" 20) @@ -235,3 +233,4 @@ (alias qfun qinvoke-method) (alias qrun qrun-on-ui-thread) (alias qq qquit) + diff --git a/src/lisp/package.lisp b/src/lisp/package.lisp index 8811676..fb09a8a 100644 --- a/src/lisp/package.lisp +++ b/src/lisp/package.lisp @@ -1,5 +1,5 @@ (defpackage :qml - (:use :common-lisp) + (:use :cl) (:export #:*break-on-errors* #:*quick-view* @@ -51,6 +51,6 @@ #:!)) (defpackage :qml-user - (:use :common-lisp :qml)) + (:use :cl :qml)) (pushnew :qml *features*) diff --git a/src/lisp/qml.lisp b/src/lisp/qml.lisp index aadc3ae..77c8e4e 100644 --- a/src/lisp/qml.lisp +++ b/src/lisp/qml.lisp @@ -19,8 +19,8 @@ ;: function. The variable *CALLER* will be bound to the calling QQuickItem, ;; if passed with 'this' as first argument to 'Lisp.call()' / 'Lisp.apply()'. (let ((*caller* (if (zerop caller) - *caller* - (qt-object caller)))) + *caller* + (qt-object caller)))) (apply (string-to-symbol function) arguments))) @@ -166,12 +166,13 @@ ;;; JS calls (defmacro qjs (method-name item/name &rest arguments) - "args: (method-name item/name &rest arguments + "args: (method-name item/name &rest arguments) Fast and convenient way to call JS functions defined in QML. You may pass up to 10 arguments of the following types: T, NIL, INTEGER, FLOAT, STRING, VECTOR of octets, and (nested) lists of mentioned arguments. - N.B: Does not work with JS default arguments." + N.B: Does not work with JS default arguments. + (qjs |drawLine| *canvas* x1 y1 x2 y2))" `(qrun* (qfun (quick-item ,item/name) ,(if (symbolp method-name) (symbol-name method-name) diff --git a/src/lqml-lib.pro b/src/lqml-lib.pro new file mode 100644 index 0000000..ce4ed6d --- /dev/null +++ b/src/lqml-lib.pro @@ -0,0 +1,61 @@ +QT += quick qml +TEMPLATE = lib +CONFIG += staticlib no_keywords release +LIBS = -L/usr/local/lib -lecl +TARGET = lqml +OBJECTS_DIR = ./tmp +MOC_DIR = ./tmp + +linux { + INCLUDEPATH = /usr/local/include + DESTDIR = ../../platforms/linux/lib +} + +macx { + INCLUDEPATH = /usr/local/include + DESTDIR = ../../platforms/macos/lib +} + +android { + INCLUDEPATH = $$(ECL_ANDROID)/include + LIBS = -L$$(ECL_ANDROID)/lib -lecl + DESTDIR = ../../platforms/android/lib + ANDROID_ABIS = "arm64-v8a" +} + +ios { + INCLUDEPATH = $$(ECL_IOS)/include + LIBS = -L$$(ECL_IOS)/lib -lecl + DESTDIR = ../../platforms/ios/lib +} + +HEADERS += \ + cpp/marshal.h \ + cpp/ecl_ext.h \ + cpp/lqml.h \ + cpp/qml.h \ + cpp/qt_ecl.h \ + cpp/single_shot.h + +SOURCES += \ + cpp/marshal.cpp \ + cpp/ecl_ext.cpp \ + cpp/lqml.cpp \ + cpp/qml.cpp \ + cpp/qt_ecl.cpp \ + cpp/single_shot.cpp + +QMAKE_CXXFLAGS += -std=c++17 + +# compile Lisp code + +android { + QMAKE_POST_LINK = $$(ECL_ANDROID)/../ecl-android-host/bin/ecl \ + -norc -shell $$PWD/make.lisp +} else:ios { + QMAKE_POST_LINK = $$(ECL_IOS)/../ecl-ios-host/bin/ecl \ + -norc -shell $$PWD/make.lisp +} else:unix { + QMAKE_POST_LINK = ecl -shell $$PWD/make.lisp +} + diff --git a/src/lqml.pro b/src/lqml.pro index 725df18..3fd93af 100644 --- a/src/lqml.pro +++ b/src/lqml.pro @@ -1,40 +1,26 @@ -LISP_FILES = \ - make.lisp \ - lisp/x.lisp \ - lisp/package.lisp \ - lisp/ini.lisp \ - lisp/qml.lisp \ - lqml.asd - -lisp.output = liblqml.a -lisp.commands = ecl -shell $$PWD/make.lisp -lisp.input = LISP_FILES - -QMAKE_EXTRA_COMPILERS += lisp - QT += quick qml TEMPLATE = app CONFIG += no_keywords release -INCLUDEPATH += /usr/local/include -LIBS += -L/usr/local/lib -lecl -L. -llqml +INCLUDEPATH = /usr/local/include +LIBS = -L/usr/local/lib -lecl -llisp TARGET = lqml DESTDIR = . +OBJECTS_DIR = ./tmp +MOC_DIR = ./tmp linux { + LIBS += -L../../platforms/linux/lib target.path = /usr/bin } -osx { - CONFIG -= app_bundle +macx { + CONFIG -= app_bundle + LIBS += -L../../platforms/macos/lib target.path = /usr/local/bin } INSTALLS = target -win32 { - include(windows.pri) -} - HEADERS += \ cpp/marshal.h \ cpp/ecl_ext.h \ @@ -49,7 +35,10 @@ SOURCES += \ cpp/lqml.cpp \ cpp/qml.cpp \ cpp/qt_ecl.cpp \ + cpp/single_shot.cpp \ cpp/main.cpp QMAKE_CXXFLAGS += -std=c++17 +QMAKE_PRE_LINK = ecl -shell $$PWD/make.lisp + diff --git a/src/make.lisp b/src/make.lisp index b75d936..6404994 100644 --- a/src/make.lisp +++ b/src/make.lisp @@ -1,17 +1,41 @@ +(when (search "/ecl-android/" (first (ext:command-args))) + (pushnew :android *features*)) + (require :asdf) (push (merge-pathnames "../") asdf:*central-registry*) -(asdf:make-build "lqml" - :monolithic t - :type :static-library - :move-here "./" - :init-name "ini_LQML") +(setf *default-pathname-defaults* + (merge-pathnames "../../")) ; LQML root -(let ((from "lqml--all-systems.a") - (to "liblqml.a")) - (when (probe-file to) - (delete-file to)) - (rename-file from to)) +#-android +(progn + (asdf:make-build "lqml" + :monolithic t + :type :static-library + :move-here (format nil "platforms/~A/lib/" + #+linux "linux" + #+darwin "macos") + :init-name "ini_LQML") + (let* ((from (format nil "platforms/~A/lib/lqml--all-systems.a" + #+linux "linux" + #+darwin "macos")) + (to "liblisp.a") + (to* (format nil "platforms/~A/lib/~A" + #+linux "linux" + #+darwin "macos" + to))) + (when (probe-file to*) + (delete-file to*)) + (rename-file from to))) + +#+android +(progn + (defvar *asdf-system* :lqml) + (defvar *library-name* "platforms/android/lib/lisp") + (defvar *init-name* "ini_LQML") + (load "platforms/shared/make")) + +(terpri) diff --git a/src/mkdirs.sh b/src/mkdirs.sh new file mode 100755 index 0000000..ad7c56f --- /dev/null +++ b/src/mkdirs.sh @@ -0,0 +1,3 @@ +mkdir build +mkdir build-android +mkdir build-ios