原文地址:caiorss.github.io/C-Cpp-Notes…
原文作者:caiorss.github.io/
发布时间:2021年2月8日
1 用于C++库和互操作性的C语言封装器
1.1 概述
C语言的主要优势之一是可以通过FFI(Foreign Function Interface,外函数接口)调用其他编程语言用C语言编写的库,可以从C共享库中调用C函数。与C语言不同,直接通过FFI调用C++库是不可行的,因为C++缺乏标准的ABI--应用二进制接口。解决这个问题的方法是使用不透明指针、不完全类型和外部C注释为C++库创建一个C包装器。
1.2 从C语言调用C++
本节介绍如何从C语言中调用静态链接的C++ cod。
- 完整的代码。GIST实例--从C语言调用C++代码
文件: cppcode.hpp
- 可以被C或C++代码使用的头文件。所有介于
#ifndef __cplusplus ... #edif之间的代码会被C编译器忽略。
#ifndef _CODECPP_
#define _CODECPP_
#ifdef __cplusplus
#define EXPORT_C extern "C"
#else
#define EXPORT_C
#endif
//============ C++ Only Header =================//
#ifdef __cplusplus // Enabled only for C++ compilers
#include <iostream>
class Runstat
{
/// Sum of sequence processed
double m_sum;
/// Sum of squares processed
double m_sumsq;
/// Size of sequence processed
size_t m_N;
public:
Runstat();
Runstat(Runstat const&) = delete;
Runstat& operator=(Runstat const&) = delete;
~Runstat();
void add(double x);
void reset();
size_t size() const;
double mean() const;
/// Standard deviation
double sdev() const;
};
#endif //-- End of __cplusplus definition //
// ========== C-interface for std::string container
typedef void* hString;
EXPORT_C hString string_new();
EXPORT_C hString string_new1 (const char* text);
EXPORT_C hString string_copy (hString self);
EXPORT_C void string_del (hString self);
EXPORT_C void string_add (hString self, const char* text);
EXPORT_C void string_disp (hString, const char* name);
//============ C-interface for class Runstat ============//
// Opaque pointer type alias for C-lang
typedef void* pStat;
EXPORT_C pStat Runstat_new();
EXPORT_C void Runstat_del (pStat self);
EXPORT_C void Runstat_add (pStat self, double x);
EXPORT_C double Runstat_mean(pStat self);
EXPORT_C double Runstat_sdev(pStat self);
EXPORT_C size_t Runstat_size(pStat self);
#endif
文件: cppcode.cpp
- 类和C语言接口函数的实现。
类RunStat成员函数的实现。
... ... ... ...
Runstat::Runstat()
{
std::cout << " [TRACE] Object created Ok." << std::endl;
m_sum = 0.0;
m_sumsq = 0.0;
m_N = 0;
}
Runstat::~Runstat()
{
std::cout << " [TRACE] Object deleted OK" << std::endl;
}
void
Runstat::add(double x)
{
m_sum += x;
m_sumsq += x * x;
m_N++;
}
...... .... ...... .... ...... ....
Runstat::~Runstat()
{
std::cout << " [TRACE] Object deleted OK" << std::endl;
}
...... .... ...... .... ...... ....
void
Runstat::add(double x)
{
m_sum += x;
m_sumsq += x * x;
m_N++;
}
... ... ... ... ... ... ... ...
类 std::string 的 C-接口函数的实现。
//--------- C-Interface for class std::string ------------------//
hString string_new()
{
return new std::string{};
}
hString string_new1(const char* text)
{
return new std::string(text);
}
// Copy constructor
hString string_copy(hString self)
{
std::string* p = reinterpret_cast<std::string*>(self);
return new std::string(*p);
}
void string_del(hString self)
{
delete reinterpret_cast<std::string*>(self);
}
void string_add(hString self, const char* text)
{
auto p = reinterpret_cast<std::string*>(self);
*p = *p + text;
}
void string_disp(hString self, const char* name)
{
auto p = reinterpret_cast<std::string*>(self);
std::cout << name << " / std::string{ " << *p << "} " << std::endl;
}
类RunStat的C-接口函数的实现。
//---------- C-Interface for class Runstat ---------------------//
pStat Runstat_new()
{
return new (std::nothrow) Runstat();
}
void Runstat_del(pStat self)
{
delete reinterpret_cast<Runstat*>(self);
}
void Runstat_add(pStat self, double x)
{
auto p = reinterpret_cast<Runstat*>(self);
p->add(x);
}
double Runstat_mean(pStat self)
{
Runstat* p = reinterpret_cast<Runstat*>(self);
return p->mean();
}
double Runstat_sdev(pStat self)
{
Runstat* p = reinterpret_cast<Runstat*>(self);
return p->sdev();
}
size_t Runstat_size(pStat self)
{
Runstat* p = reinterpret_cast<Runstat*>(self);
return p->size();
}
文件:main.c
- 使用C++文件cppcode导出的C接口函数的C客户端代码。
#include <stdio.h>
#include <stdlib.h>
#include "codecpp.hpp"
int main()
{
printf("\n == EXPERIMENT 1 - std::string C-wrapper ======\n");
hString str = string_new1("A C++ string in C");
string_disp(str, "str");
hString str2 = string_copy(str);
string_add(str, " - hello world");
string_disp(str, "str");
string_disp(str2, "str2");
string_del(str);
string_del(str2);
printf("\n == EXPERIMENT 2 - Class Runstat ======\n");
pStat obj = Runstat_new();
Runstat_add(obj, 10.0);
Runstat_add(obj, 4.0);
Runstat_add(obj, 25.0);
Runstat_add(obj, 16.0);
printf(" Number of Elements processed = %zu \n", Runstat_size(obj));
printf(" Mean = %.5f \n", Runstat_mean(obj));
printf(" Sdev = %.5f \n", Runstat_sdev(obj));
Runstat_add(obj, -50.0);
Runstat_add(obj, 80.0);
printf(" Mean = %.5f \n", Runstat_mean(obj));
printf(" Sdev = %.5f \n", Runstat_sdev(obj));
// Delete C++ Object
Runstat_del(obj);
return 0;
}
文件:CMakeLists.txt
cmake_minimum_required(VERSION 3.9)
project( CallCppFromC)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_VERBOSE_MAKEFILE ON)
add_executable(main main.c codecpp.cpp codecpp.hpp)
手动编译。
# Generate object code codecpp.o
$ g++ codecpp.cpp -o codecpp.o -c -std=c++1z -Wall -g
# Generate object-code main.o
$ gcc main.c -o main.o -c -g
# Link => generate main.bin
$ g++ main.o codecpp.o -o main.bin
程序输出。
./main.bin
== EXPERIMENT 1 - std::string C-wrapper ======
str / std::string{ A C++ string in C}
str / std::string{ A C++ string in C - hello world}
str2 / std::string{ A C++ string in C}
== EXPERIMENT 2 - Class Runstat ======
[TRACE] Object created Ok.
Number of Elements processed = 4
Mean = 13.75000
Sdev = 8.95824
Mean = 14.16667
Sdev = 41.69612
[TRACE] Object deleted OK
1.3 从C、Julia、D-lang和Lisp中调用Qt5 Widgets库。
1.3.1 概述
这个示例代码为QT5 Widgets GUI库提供了一个C封装器。这个C封装器允许从C语言、Julia语言和D语言中调用Qt5。
本实验中使用的功能文档
QT5示例代码中使用的widgets类的文档。
- QObject类 | Qt Core 5.15.0
- QWidget类 | Qt Widgets 5.15.0
- QApplication类 | Qt Widgets 5.15.0
- QPushButton类 - Qt Widgets 5.15.0
- QDoubleSpinBox类 - Qt Widgets 5.15.0
Julia语言
D语言。
Lisp - SBCL (Steel Bank Common Lisp):
- www.sbcl.org/ -- -- Steel Bank Common Lisp
- sbcl(1)。Steel Bank Common Lisp - Linux man page
- CFFI用户手册
- Interop迷你系列--使用CFFI调用C和C++代码Lisp(第一部分)
- 用cffi共享C语言库 - GitHub
- [实践][Common Lisp]hello CFFI (Common Lisp + C) - GitHub
- Buildapp - 使用SBCL或CCL创建可执行文件
1.3.2 包装文件
- GIST与所有来源。
C-wrapper的源代码
文件: CMakeLists.txt
cmake_minimum_required(VERSION 3.9)
project(Qt5_Widgets_Template)
#====== Global Configurations ==================#
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_VERBOSE_MAKEFILE ON)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
# Export ALL DLLs symbols on Windows without __declspec(xxxx) annotations.
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS true)
find_package(Qt5 COMPONENTS Core Widgets Network UiTools REQUIRED)
#=============== Target Configurations ============#
# --------------------------------------------------#
add_library( qtwrapper SHARED qtwrapper.cpp )
target_link_libraries( qtwrapper PRIVATE Qt5::Core Qt5::Gui Qt5::Widgets )
add_executable( client1 client1.c )
target_link_libraries( client1 qtwrapper )
文件: qtwrapper.cpp
- QT5 C++ GUI库的C-wrapper或C-interface。
#include <QtWidgets>
#include <QApplication>
#include <QtUiTools/QtUiTools>
#include <QSysInfo>
#include <QtConcurrent/QtConcurrent>
#include <functional>
#include <iostream>
#include <fstream>
#include <string>
#include <map>
/** extern "C" => Remove C++ name mangling and makes the function
* compatible with C. It enforces C linkage to the annotated symbol.
**-----------------------------------------------------------------*/
#define EXPORT_C extern "C"
/** Delete any instance of a derived class of QObject
* Note: It can be used for deleting instances QWidget, QApplication or QLayout.
*/
EXPORT_C
void qt_qobject_del(QObject* self)
{
delete self;
}
EXPORT_C
const char* qt_qobject_ClassName(QObject* self)
{
return self->metaObject()->className();
}
EXPORT_C
void qt_qobject_dumpObjectInfo(QObject* self)
{
self->dumpObjectInfo();
}
EXPORT_C
void qt_qobject_dumpObjectTree(QObject* self)
{
self->dumpObjectTree();
}
EXPORT_C
void qt_qobject_print(QObject* self)
{
qDebug() << " [QOBjec] " << self;
}
enum class WidgetType
{
Window = 1
, Window_main = 2
, QPushButton = 3
, QLabel = 4
, QLineEdit = 5
, QDoubleSpinBox = 6
};
using CtorFunction = std::function<QWidget* (QWidget* parent)>;
using CtorDatabase = std::map<int, CtorFunction>;
template<typename T> void register_ctor(CtorDatabase& db, int type)
{
db[type] = [](QWidget* parent){ return new (std::nothrow) T(parent); };
}
// Create an object of a QWidget given class, given its name
EXPORT_C
QWidget* qt_widget_new(QWidget* parent, int type)
{
// 'static' => Initialize static object only once.
static const CtorDatabase ctordb = []{
auto db = CtorDatabase{};
register_ctor<QWidget>(db, (int) WidgetType::Window);
register_ctor<QPushButton>(db, (int) WidgetType::QPushButton);
register_ctor<QLabel>(db, (int) WidgetType::QLabel);
register_ctor<QLineEdit>(db, (int) WidgetType::QLineEdit);
register_ctor<QDoubleSpinBox>(db, (int) WidgetType::QDoubleSpinBox);
db[(int) WidgetType::Window_main] = [](QWidget* parent){
QWidget* w = new (std::nothrow) QWidget(parent);
w->resize(500, 400);
w->setWindowTitle("MainWindow");
w->show();
return w;
};
return db;
}();
if(auto it = ctordb.find(type); it != ctordb.end())
{ return it->second(parent); }
return nullptr;
}
EXPORT_C
QWidget* qt_window_main()
{
QWidget* w = new (std::nothrow) QWidget{};
w->resize(500, 400);
w->setWindowTitle("MainWindow");
w->show();
return w;
}
EXPORT_C
QLayout* qt_layout_new(QWidget* parent, int type)
{
if(type == 1) return new (std::nothrow) QVBoxLayout(parent);
if(type == 2) return new (std::nothrow) QHBoxLayout(parent);
if(type == 3) return new (std::nothrow) QFormLayout(parent);
return nullptr;
}
EXPORT_C
QObject* qt_QFormLayout_addWidgetAndLabel(QFormLayout* self, int type, const char* label)
{
QWidget* wdg = qt_widget_new(nullptr, type);
if(wdg == nullptr){ return nullptr; }
self->addRow(label, wdg);
return wdg;
}
EXPORT_C
void qt_QFormLayout_addRowItem(QFormLayout* self, QWidget* field)
{
self->addRow(field);
}
EXPORT_C
QLabel* qt_QFormLayout_addLabel1(QFormLayout* self, const char* label_text)
{
auto btn = new QLabel(label_text);
self->addRow(btn);
return btn;
}
EXPORT_C
QApplication* qt_app_new(int argc, char** argv)
{
std::cout << " [TRACE] Create QAppliction Object Ok" << std::endl;
return new QApplication(argc, argv);
}
EXPORT_C
QApplication* qt_app_new2()
{
std::cout << " [TRACE] Create QAppliction Object Ok" << std::endl;
static int argc = 1;
static const char* argv [] = { "dummy_app" };
return new QApplication(argc, (char**) argv);
}
EXPORT_C
int qt_app_exec(QApplication* self)
{
return self->exec();
}
// -------- Wrappers for QWidget Class ------------------//
EXPORT_C
void qt_widget_show(QWidget* self)
{
self->show();
}
template<typename T>
static bool set_text(QWidget* self, const char* text)
{
auto obj = qobject_cast<T>(self);
// Early return on Error.
if(obj == nullptr ){ return false; }
obj->setText(text);
return true;
}
EXPORT_C
void qt_widget_setText(QWidget* self, const char* text)
{
if( set_text<QLabel*>(self, text)) return;
if( set_text<QLineEdit*>(self, text)) return;
if( set_text<QTextEdit*>(self, text)) return;
if( set_text<QMessageBox*>(self, text)) return;
if( set_text<QAbstractButton*>(self, text) ) return;
// logger().log() << " [TRACE] Called function " << __FUNCTION__ << std::endl;
self->setWindowTitle(text);
}
// -------- Wrappers for QPushButton Class ------------------//
// Install event handler (callback) for button clicked event
EXPORT_C
void qt_button_onClicked( // Pointer to button
QAbstractButton* self
// Context of the callback. Note: C does not support closures
// or stateful function pointer. All data needs to be passed
// as arguments
, void* ctx
// Pointer to callback function pointer.
, void (* callback) (void* ctx) )
{
QObject::connect(self, &QPushButton::clicked, [=]{
callback(ctx);
});
}
// Install event handler for
EXPORT_C
void qt_QLineEdit_onTextChanged( QLineEdit* self
, void* ctx
, void (* callback) (void* ctx, QLineEdit* self) )
{
QObject::connect(self, &QLineEdit::textChanged, [=]{
callback(ctx, self);
});
}
EXPORT_C
void qt_msgbox_info(QWidget* parent, const char* title, const char* text)
{
QMessageBox::information(parent, title, text);
}
// Note: The string has to be released after with free() function.
EXPORT_C
const char* qt_QLineEdit_text(QLineEdit* self)
{
return self->text().toLocal8Bit().constData();
}
EXPORT_C
double qt_QDoubleSpinBox_value(QDoubleSpinBox* self)
{
return self->value();
}
EXPORT_C
void qt_QDoubleSpinBox_setValue(QDoubleSpinBox* self, double value)
{
self->setValue(value);
}
EXPORT_C
void qt_QDoubleSpinBox_onValueChanged(
QDoubleSpinBox* self
, void* ctx
, void (* callback)(void* ctx) )
{
QObject::connect( self, qOverload<double>(&QDoubleSpinBox::valueChanged)
, [=](double x){ callback(ctx); }
);
}
文件:qtwrapper.h (C-wrapper或C-interface的头)
// Non standard directive, but supported by most compiler to includer the
// header only once
#pragma once
// Type aliases
typedef void ANY;
typedef void QObject;
typedef void QApplication;
typedef void QPushButton;
typedef void QWidget;
typedef void QAbstractButton;
typedef void QLineEdit;
typedef void QLayout;
typedef void QFormLayout;
typedef void QLabel;
void qt_qobject_del(QObject* self);
void qt_qobject_dumpObjectInfo(QObject* self);
void qt_qobject_dumpObjectTree(QObject* self);
void qt_qobject_print(QObject* self);
QApplication* qt_app_new(int argc, char** argv);
QApplication* qt_app_new2();
// Instantiate any QT widget by name
QWidget* qt_widget_new(QWidget* parent, int type);
void qt_widget_setText(QWidget* self, const char* text);
// QApplication
int qt_app_exec(QApplication* self);
// Any QtWidget
void qt_QWidget_show(QWidget* self);
void qt_QWidget_setToolTip(QWidget* self, const char* tooltip);
// QPushButton wrappers
QPushButton* qt_QPushButton_new(QWidget* parent, const char* label);
void qt_QPushButton_onClicked_test(QPushButton* self );
// Abstract PushbButton
void qt_button_onClicked(QAbstractButton* self, void* ctx, void (* callback) (void*) );
void qt_button_setText(QAbstractButton* self, const char* text);
const char* qt_QLineEdit_text(QLineEdit* self);
QLayout* qt_layout_new(QWidget* parent, int type);
QObject* qt_QFormLayout_addWidgetAndLabel(QFormLayout* self, int type, const char* label);
QLabel* qt_QFormLayout_addLabel1(QFormLayout* self, const char* label_text);
void qt_msgbox_info(QWidget* parent, const char* title, const char* text);
void qt_QLineEdit_onTextChanged( QLineEdit* self
, void* ctx
, void (* callback) (void* ctx, QLineEdit* self) );
考虑因素。
-
(extern "C")强制执行C链接,换句话说,带有这个注解的函数必须有一个C兼容的签名并使用C兼容的类型。
-
C++运算符new会在内存不足时抛出异常,它会抛出std::bad_alloc。由于C语言不支持异常,所以需要使用返回空指针的操作符new(std::nothrow)来代替抛出异常。
-
由于C语言不支持异常,在用extern "C "注释的函数中,任何异常抛出都应该作为一个额外的函数参数返回。此外,由于缺乏标准的C++ ABI,来自不同编译器的C++异常甚至可能不兼容。
-
由于C函数不支持闭包,不能保持状态,因此有必要在函数指针参数和将其作为参数的函数中增加一个额外的void指针参数(void*),例如qt_button_onClicked()。这个额外的(void*)指针允许调用代码传递和保持状态。
-
尽管这个用于创建C-wrapper(C接口)的技术是用于Qt5 Widgets库的,但这个方法可以用于任何其他C++库,例如:Boost-Libraries, WxW.C.C.C.C.C.C: Boost-Libraries, WxWidgets GUI library, Windows MFC (Microsoft Foundation Classes) 等等。
-
这种C-wrapper技术也可以用于在C++中创建C-库,它可以通过FFI(Foreign Function Interface)从任何编程语言(Python、Ruby、Lisp...)中加载,或者在Golang、Rust或D-lang等编译语言的情况下,通过对C-wrapper共享库的链接加载。
-
如果Qt有类似于这样的C-binding,在每一个使用Qt或KDE的Linux发行版上随Qt一起发布,那么就可以用C++以外的任何其他编程语言来创建Qt应用程序,通过FFI外函数接口加载C-binding,或者如果编程语言是编译语言,并且支持针对C ABI(应用程序二进制接口)的链接,则可以针对C-binding进行链接。
-
C-wrapper对C++库的另一个好处是,C-wrapper提供了一个稳定的ABI(Application-Binary Interface),它允许用不同的C++编译器构建的应用程序在不需要重新编译库的情况下链接到库的C-接口,避免了用客户端代码可能使用的所有可能的C++编译器编译库。
-
从更高层次的语言中访问C++库的另一种方法是使用SWIG(SWIG包装器生成器),它可以解析接口文件和库头文件,生成C代码,用于编程语言特有的native interface API,在类似Unix的系统上,可以用C语言创建本地代码库,在运行时通过dlopen()调用加载,在Windows上则通过LoadLibrary()调用加载。native-interface API这个术语是从Java的JNI(Java Native Interface API)借来的,因为与FFI(Foreign Function Interfaces)不同的是,目前还没有合适的术语来描述这个功能。例如,Pythons的原生接口API允许完全用C语言创建Python模块(也就是库),使用SWIG的缺点是,它需要访问库的源代码,并为每一种将生成绑定的编程语言编译生成绑定代码,而C-wrapper只需要编译一次。
1.3.3 构建包装器文件。
注:本代码是在Linux--Fedora 32、64位机器上编译和测试的,该机器包含预装的Qt5库。
获取源代码。
$ git clone https://gist.github.com/a59d0f1b17d286dad19842932c931e92 qt5-cwrapper
$ cd qt5-cwrapper
$ ls
client1.c CMakeLists.txt d_client.d loader.jl qtwrapper.cpp qtwrapper.h
构建项目。(注:在Linux Fedora-32 64位x86-64上测试)
$ cmake --config Debug -H. -build
$ cmake --build build --target
检查二进制文件。
$ file build/client1
build/client1: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2
, BuildID[sha1]=122650693cf9845a35afeb76bced113f3d92ed1a, for GNU/Linux 3.2.0, not stripped
$ file build/libqtwrapper.so
build/libqtwrapper.so: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), statically linked
, BuildID[sha1]=47381d2f687f8cb5f6bf80be737c0baa2b0e2cb1, not stripped
显示由C-wrapper共享库导出的符号。
- 注意:在windows下,对应的工具是dumpbin,在MacOSX上,对应的工具是otool。
$ nm -D build/libqtwrapper.so
... ... ... ... ... ... ...
... ... ... ... ... ... ...
0000000000012d9d T qt_QFormLayout_addRowItem
0000000000012d0c T qt_QFormLayout_addWidgetAndLabel
000000000001315a T qt_QLineEdit_onTextChanged
0000000000013284 T qt_QLineEdit_text
0000000000012741 T qt_qobject_ClassName
000000000001271a T qt_qobject_del
000000000001276a T qt_qobject_dumpObjectInfo
0000000000012785 T qt_qobject_dumpObjectTree
00000000000127a0 T qt_qobject_print
U qt_version_tag
0000000000012a47 T qt_widget_new
0000000000012fbb T qt_widget_setText
0000000000012fa0 T qt_widget_show
0000000000012b51 T qt_window_main
U strlen
U _Unwind_Resume
00000000000157a8 W _Z12qobject_castIP11QMessageBoxET_P7QObject
00000000000157cc W _Z12qobject_castIP15QAbstractButtonET_P7QObject
000000000001573c W _Z12qobject_castIP6QLabelET_P7QObject
0000000000015760 W _Z12qobject_castIP9QLineEditET_P7QObject
... ... ... ... ... ... ... ...
... ... ... ... ... ... ... ...
显示导出的符号,解密(也就是解码)C++杂乱的符号。
$ >> nm -D build/libqtwrapper.so | c++filt
... ... ... ... ...
... ... ... ... ...
000000000001276a T qt_qobject_dumpObjectInfo
0000000000012785 T qt_qobject_dumpObjectTree
00000000000127a0 T qt_qobject_print
U qt_version_tag
0000000000012a47 T qt_widget_new
0000000000012fbb T qt_widget_setText
0000000000012fa0 T qt_widget_show
0000000000012b51 T qt_window_main
U strlen
U _Unwind_Resume
00000000000157a8 W QMessageBox* qobject_cast<QMessageBox*>(QObject*)
00000000000157cc W QAbstractButton* qobject_cast<QAbstractButton*>(QObject*)
000000000001573c W QLabel* qobject_cast<QLabel*>(QObject*)
0000000000015760 W QLineEdit* qobject_cast<QLineEdit*>(QObject*)
... ... ... ... ...
... ... ... ... ...
1.3.4 C客户端代码
文件: client1.c (C-客户端代码)
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include "qtwrapper.h"
typedef struct CallbackContext
{
QLineEdit* lin;
int counter;
} CallbackContext;
// All callback (closure) state should be passed as arguments
void onclicked_callback(void* context)
{
CallbackContext* ctx = (CallbackContext*) context;
ctx->counter += 1;
const char* str = qt_QLineEdit_text(ctx->lin);
printf(" [TRACE] Button clicked => Counter incremented to: %d \n", ctx->counter);
printf(" [TRACE] QLineEdit Value = %s \n", str);
// free(str);
// qt_msgbox_info(NULL, "Message Info", "Button clicked Ok");
}
void lineEdit_Callback(void* ctx, QLineEdit* self)
{
const char* str = qt_QLineEdit_text(self);
QLabel* label = (QLabel*) ctx;
qt_widget_setText(label, str);
printf(" [LineEdit Callback] Text changed to %s \n", str);
}
void onclick_msgbox_callback(void* context)
{
QLineEdit* lin = (QLineEdit*) context;
const char* str = qt_QLineEdit_text(lin);
qt_msgbox_info(NULL, "Message Info", str);
// free(str);
}
const int WIDGET_WINDOW = 1;
const int WIDGET_WINDOW_MAIN = 2;
const int WIDGET_QPUSH_BUTTON = 3;
const int WIDGET_QLABEL = 4;
const int WIDGET_QLINE_EDIT = 5;
const int WIDGET_QDOUBLE_SPINBOX = 6;
const int LAYOUT_QFORM_LAYOUT = 3;
int main(int argc, char** argv)
{
// QApplication* qapp = qt_app_new(argc, argv);
QApplication* qapp = qt_app_new2();
// -------- Create GUI - Graphical User Interface ------//
QPushButton* win = qt_widget_new(NULL, WIDGET_WINDOW_MAIN);
qt_widget_setText(win, "Window Title");
assert(win != NULL);
QFormLayout* form = qt_layout_new(win, LAYOUT_QFORM_LAYOUT);
assert(form != NULL);
QLineEdit* lin = qt_QFormLayout_addWidgetAndLabel(form, WIDGET_QLINE_EDIT, "Input");
QPushButton* btn1 = qt_QFormLayout_addWidgetAndLabel(form, WIDGET_QPUSH_BUTTON, "");
QPushButton* btn2 = qt_QFormLayout_addWidgetAndLabel(form, WIDGET_QPUSH_BUTTON, "");
QLabel* disp = qt_QFormLayout_addWidgetAndLabel(form, WIDGET_QLABEL, "Display label");
qt_widget_setText(btn1, "Button 1");
qt_widget_setText(btn2 ,"Button 2");
// qt_widget_setToolTip(btn1, "Click at this button to play!");
qt_qobject_print(win);
// -------- Install Event Handlers --------------//
//
qt_QLineEdit_onTextChanged(lin, disp, lineEdit_Callback);
qt_button_onClicked(btn1, lin, &onclick_msgbox_callback);
CallbackContext ctx;
ctx.counter = 0;
ctx.lin = lin;
qt_button_onClicked(btn2, &ctx, &onclicked_callback);
// ---- Run QT event loop - blocking main thread ---//
qt_app_exec(qapp);
// ----- Dispose objects ---------------------------//
qt_qobject_del(win);
qt_qobject_del(qapp);
puts("\n [TRACE] Terminate application Ok. ");
return 0;
}
运行可执行文件。
$ build/client1
[TRACE] Create QAppliction Object Ok
[LineEdit Callback] Text changed to h
[LineEdit Callback] Text changed to h
[LineEdit Callback] Text changed to h
[LineEdit Callback] Text changed to h
... ... ... ... ... ...
... ... ... ... ... ... ...
用户界面截图:
图1:C客户端代码的用户界面。
1.3.5 Julia客户端代码(loader.jl)
文件: loader.jl (Julia语言客户端代码)
#!/usr/bin/env julia
# Refers to file libqexport.so
#
# On Linux:
# It is assumed that the LD_LIBRARY_PATH environment variable of current
# process contains the directory where libqtexport is located.
#
# On Windows:
# It is made the assumption that the PATH environmenrt variable of current
# process contains the directory where libqtexport is located.
#
#
const shlib = "libqtwrapper"
const QObject = Ptr{Cvoid}
const QWidget = Ptr{Cvoid}
const QLayout = Ptr{Cvoid}
const QApp = Ptr{Cvoid}
const QButton = Ptr{Cvoid}
const QDoubleSpinbox = Ptr{Cvoid}
const WIDGET_WINDOW = 1;
const WIDGET_WINDOW_MAIN = 2;
const WIDGET_QPUSH_BUTTON = 3;
const WIDGET_QLABEL = 4;
const WIDGET_QLINE_EDIT = 5;
const WIDGET_QDOUBLE_SPINBOX = 6;
const LAYOUT_QFORM_LAYOUT = 3;
# Wrapper to function:
# QWidget* qt_widget_new(QWidget* parent, const char* name)
function qt_widget_new(type::Int)::QWidget
return ccall( (:qt_widget_new, shlib)
# Function type signatrue
, QWidget, (QWidget, Cint)
# Function arguments (parent = null and name = name)
, C_NULL, type
)
end
# Delete any instance of QObject (QWidget, QApplictation, QLayout) and so on
function qt_qobject_del(obj::QObject)
ccall( (:qt_qobject_del, shlib)
, Cvoid, ( QObject, )
, obj )
end
function qt_qobject_print(self::QObject)
ccall( (:qt_qobject_print, shlib)
, Cvoid, (QObject,)
, self )
end
## => void qt_widget_setText(QWidget* self, const char* text);
function qt_widget_setText(self::QWidget, text::String)
ccall( (:qt_widget_setText, shlib)
# Function type signatrue
, Cvoid, (QWidget, Cstring)
# Function arguments (parent = null and name = name)
, self, text )
end
# extern QLayout* qt_layout_new(QWidget* parent, const char* name);
function qt_layout_new(type::Int, parent::QWidget)::QLayout
return ccall( (:qt_layout_new, shlib)
# Function type signatrue
, QLayout, (QWidget, Cint)
# Function arguments (parent = null and name = name)
, parent, type )
end
# QPushButton* qt_QFormLayout_addButton(QFormLayout* self, const char* label)
function qt_QFormLayout_addWidgetAndLabel(form::QLayout, type::Int, label::String)::QWidget
return ccall( (:qt_QFormLayout_addWidgetAndLabel, shlib)
# Function type signatrue
, QWidget, (QLayout, Cint, Cstring)
# Function arguments
, form, type, label )
end
function qt_qobject_ClassName(self::QObject)::String
res = ccall( (:qt_qobject_ClassName, shlib)
, Cstring, (QObject, )
, self )
return unsafe_string(res)
end
function QApplication_new()
# return ccall( (:qt_app_new2, shlib), Ptr{Cvoid}, ())
return ccall((:qt_app_new2, shlib), QApp, ())
end
function QApplication_exec(self)
ccall((:qt_app_exec, shlib), Cvoid, ( QApp, ), self )
end
function QPushButton_new(label::String)
# return ccall((:qt_QPushButton_new, shlib), ( Ptr{Cvoid}, Cstring ), C_NULL, label )
return btn = ccall( (:qt_QPushButton_new, shlib)
, Ptr{Cvoid}, (Ptr{Cvoid}, Cstring)
, C_NULL, label
)
end
function QWidget_show(self)
return ccall((:qt_QWidget_show, shlib), Cvoid, ( Ptr{Cvoid}, ), self)
end
function QPushButton_onClicked_test(self::QButton)
ccall((:qt_QPushButton_onClicked_test, shlib), Cvoid, ( QButton, ), self)
end
function qt_button_onClicked(self::QButton, handler)
callback = @cfunction $handler Cvoid ( Ptr{Cvoid}, )
ccall( (:qt_button_onClicked, shlib)
# Function return type signature
, Cvoid
# Function arguments
, ( QButton, Ptr{Cvoid}, Ptr{Cvoid} )
# Arguments passed to function
, self, C_NULL, callback
)
end
function qt_QDoubleSpinBox_value(self::QDoubleSpinbox)
return ccall( (:qt_QDoubleSpinBox_value, shlib)
, Cdouble, ( QDoubleSpinbox,)
, self)
end
# Wrapper:
# void qt_msgbox_info(QWidget* parent, const char* title, const char* text);
function qt_msgbox_info(title::String, text::String)
ccall( (:qt_msgbox_info, shlib)
, Cvoid, (Ptr{Cvoid}, Cstring, Cstring)
, C_NULL, title, text
)
end
# void qt_QDoubleSpinBox_onValueChanged(
# QDoubleSpinBox* self
# , void* ctx, void (* callback)(void* ctx))
function qt_QDoubleSpinBox_onValueChanged(self::QDoubleSpinbox, handler)
callback = @cfunction $handler Cvoid ( Ptr{Cvoid}, )
ccall( (:qt_QDoubleSpinBox_onValueChanged, shlib)
# Function return type signature
, Cvoid
# Function arguments
, ( QButton, Ptr{Cvoid}, Ptr{Cvoid} )
# Arguments passed to function
, self, C_NULL, callback
)
end
function demo_qt_gui_form()
qapp = QApplication_new()
window = qt_widget_new( WIDGET_WINDOW_MAIN )
qt_widget_setText(window, "Sample QT GUI in Julia Language")
form = qt_layout_new(LAYOUT_QFORM_LAYOUT, window)
entry1 = qt_QFormLayout_addWidgetAndLabel(form, WIDGET_QDOUBLE_SPINBOX, "Speed in m/s")
entry2 = qt_QFormLayout_addWidgetAndLabel(form, WIDGET_QDOUBLE_SPINBOX, "Acceleration m/s^2")
btn_run = qt_QFormLayout_addWidgetAndLabel(form, WIDGET_QPUSH_BUTTON, "")
btn_clean = qt_QFormLayout_addWidgetAndLabel(form, WIDGET_QPUSH_BUTTON, "")
label = qt_QFormLayout_addWidgetAndLabel(form, WIDGET_QLABEL, "")
qt_widget_setText(btn_run, "Run calculations");
qt_widget_setText(btn_clean, "Clean");
println(" [INFO] class of form object = ", qt_qobject_ClassName(form))
println(" [INFO] class of btn_run object = ", qt_qobject_ClassName(btn_run))
println(" [INFO] class of entry1 object = ", qt_qobject_ClassName(entry1))
println(" [INFO] class of entry2 object = ", qt_qobject_ClassName(entry2))
qt_widget_setText(btn_run, "RUN GUI")
qt_widget_setText(btn_clean, "CLEAN GUI")
qt_qobject_print(window)
update_calculations = (ctx) -> begin
speed = qt_QDoubleSpinBox_value(entry1)
accel = qt_QDoubleSpinBox_value(entry2)
z = 4.51 * speed + 9.81 * accel^2 / speed
out = string(" [JULIA] Result = ", round(z, digits = 4))
println(" speed = ", speed, " ; accel = ", accel)
println(out)
println(" --------------------------------- ")
qt_widget_setText(label, out)
end
qt_QDoubleSpinBox_onValueChanged(entry1, update_calculations)
qt_QDoubleSpinBox_onValueChanged(entry2, update_calculations)
n = 1
qt_button_onClicked(btn_run, (ctx) -> begin
n = n + 1
println(" [ JULIA ] I was clicked Ok. n = ", n, " times. ")
qt_msgbox_info("Notification", "Button was clicked Ok")
end)
qt_button_onClicked(btn_clean, (ctx) -> begin
println(" [TRACE] Button clean clicked Ok")
qt_widget_setText(label, "Button clean clicked")
end)
# --- Block main thread and run QT event loop ---//
QApplication_exec(qapp)
# ----- Dispose objects ----------
# Only the root widget (window) need to be removed
qt_qobject_del(window)
qt_qobject_del(qapp)
end
demo_qt_gui_form()
运行julia脚本/尝试1->错误。
$ julia loader.jl
ERROR: LoadError: could not load library "libqtwrapper"
libqtwrapper.so: cannot open shared object file: No such file or directory
Stacktrace:
... ... .... ... ...
... ... ... ... ...
运行julia脚本/尝试2->工作。
- 注意:LD_LIBRARY_PATH只在Linux上有效。这个变量在windows下的对应变量是PATH环境变量,在MacOSX上的对应变量是DYLD_LIBRARY_PATH。
- libqtwrapper.so共享库可以安装在Linux上,只要把库文件移到/lib目录下即可。
$ export LD_LIBRARY_PATH=build:$LD_LIBRARY_PATH
$ julia loader.jl
[TRACE] Create QAppliction Object Ok
[INFO] class of form object = QFormLayout
[INFO] class of btn_run object = QPushButton
[INFO] class of entry1 object = QDoubleSpinBox
[INFO] class of entry2 object = QDoubleSpinBox
speed = 1.0 ; accel = 0.0
[JULIA] Result = 4.51
---------------------------------
speed = 2.0 ; accel = 0.0
[JULIA] Result = 9.02
---------------------------------
speed = 3.0 ; accel = 0.0
[JULIA] Result = 13.53
---------------------------------
... ... ... ...
前面的Julia脚本也可以用下面的方法运行。这种方法的优点是,变量LD_LIBRARY_PATH只为命令设置。'$ julia loader.jl'.
$ env LD_LIBRARY_PATH=build:$LD_LIBRARY_PATH julia loade.jl
在Linux上,可以不设置环境变量LD_LIBRARY_PATH,通过将库文件移动到/usr/lib、/usr/local/lib或/lib来使用库。
# Install shared library to /usr/local/lib
$ sudo cp -v build/libqtwrapper.so /usr/lib
# Update library cache
$ sudo ldconfig -v
# Run the Julia script
$ julia loader.jl
[TRACE] Create QAppliction Object Ok
libGL error: MESA-LOADER: failed to open iris (search paths /usr/lib64/dri)
libGL error: failed to load driver: iris
libGL error: MESA-LOADER: failed to open iris (search paths /usr/lib64/dri)
libGL error: failed to load driver: iris
libGL error: MESA-LOADER: failed to open swrast (search paths /usr/lib64/dri)
libGL error: failed to load driver: swrast
[INFO] class of form object = QFormLayout
[INFO] class of btn_run object = QPushButton
... .... ....
... .... ....
用户界面截图。
图2:Julia语言客户端代码的用户界面。
1.3.6 D语言--D-lang客户端代码
文件:d_client.d (Dlang / D语言客户端代码)
import core.stdc.stdio;
import std.range: empty;
import str = std.string;
// Import dlopen
import DLL = core.sys.posix.dlfcn;
alias QLayout = void;
alias QObject = void;
alias QWidget = void;
alias QLabel = void;
alias QApplication = void;
alias QPushButton = void;
alias QFormLayout = void;
alias QAbstractButton = void;
const int WIDGET_WINDOW = 1;
const int WIDGET_WINDOW_MAIN = 2;
const int WIDGET_QPUSH_BUTTON = 3;
const int WIDGET_QLABEL = 4;
const int WIDGET_QLINE_EDIT = 5;
const int WIDGET_QDOUBLE_SPINBOX = 6;
const int LAYOUT_QFORM_LAYOUT = 3;
alias qapp_new_t = extern(C) QApplication* function ();
alias qapp_exec_t = extern(C) int function(QApplication* self);
alias qobject_del_t = extern(C) void function (QObject*);
alias qt_widget_new_t = extern(C) QWidget* function(QWidget* parent, int type);
alias qt_window_main_t = extern(C) QWidget* function();
alias qt_widget_setText_t = extern(C) void function(QWidget* self, const char* text);
alias qt_layout_new_t = extern(C) QLayout* function (QWidget* parent, int type);
alias qt_msgbox_info_t = extern(C) void function(QWidget* parent, const char* title, const char* text);
// QObject* qt_QFormLayout_addWidgetAndLabel(QFormLayout* self, int type, const char* label)
alias qt_QFormLayout_addWidgetAndLabel_t =
extern(C) QPushButton* function (QFormLayout* self, int type, const char* label);
alias qt_button_onClicked_t = extern(C) void function ( QAbstractButton* self
, void* ctx
, void function(void* self) );
struct callback_state
{
int counter = 0;
QLabel* label = null;
qt_widget_setText_t qt_widget_setText = null;
qt_msgbox_info_t qt_msgbox_info = null;
};
extern(C) void button_callback1(void* ctx)
{
import std.conv: to;
auto pstate = cast(callback_state*) ctx;
pstate.counter = pstate.counter + 1;
printf(" [TRACE] Button click event happened => state = %d. \n", pstate.counter);
string text = "Button clicked / counter = ";
text = text ~ to!string(pstate.counter);
pstate.qt_widget_setText(pstate.label, str.toStringz(text));
if(pstate.counter > 20){
pstate.qt_msgbox_info(null, "QT Event => Button Clicked", str.toStringz(text));
}
}
int main()
{
// ------------- Load Symbols from Shared Library -------------------//
void* dll = DLL.dlopen("./build/libqtwrapper.so", DLL.RTLD_GLOBAL | DLL.RTLD_LAZY);
if (!dll)
{
fprintf(stderr, " [ERROR] dlopen error: %s\n", DLL.dlerror());
return 1;
}
auto qapp_new = cast(qapp_new_t) DLL.dlsym(dll, "qt_app_new2");
auto qapp_exec = cast(qapp_exec_t) DLL.dlsym(dll, "qt_app_exec");
auto qt_qobject_del = cast(qobject_del_t) DLL.dlsym(dll, "qt_qobject_del");
auto qt_widget_new = cast(qt_widget_new_t) DLL.dlsym(dll, "qt_widget_new");
auto qt_layout_new = cast(qt_widget_new_t) DLL.dlsym(dll, "qt_layout_new");
auto qt_window_main = cast(qt_window_main_t) DLL.dlsym(dll, "qt_window_main");
auto qt_widget_setText = cast(qt_widget_setText_t) DLL.dlsym(dll, "qt_widget_setText");
auto qt_button_onClicked = cast(qt_button_onClicked_t) DLL.dlsym(dll, "qt_button_onClicked");
auto qt_msgbox_info = cast(qt_msgbox_info_t) DLL.dlsym(dll, "qt_msgbox_info");
auto form_add_item = cast(qt_QFormLayout_addWidgetAndLabel_t) DLL.dlsym(dll, "qt_QFormLayout_addWidgetAndLabel");
assert(form_add_item);
// ---------- Create QT GUI -----------------------------//
//
// Create an instance of class QApplication
auto qapp = qapp_new();
auto window = qt_widget_new(null, WIDGET_WINDOW_MAIN) ;
assert( window );
qt_widget_setText(window, "QT Widgets GUI in D Language");
auto form = qt_layout_new(window, LAYOUT_QFORM_LAYOUT);
auto btn = form_add_item(form, WIDGET_QPUSH_BUTTON, "");
auto label = form_add_item(form, WIDGET_QLABEL, "Display");
qt_widget_setText(btn, "Click ME NOW!!");
callback_state state;
state.counter = 10;
state.label = label;
state.qt_msgbox_info = qt_msgbox_info;
state.qt_widget_setText = qt_widget_setText;
// Install button event handler
qt_button_onClicked(btn, &state, &button_callback1);
// ------ Run QT Event Loop blocking main thread ------//
qapp_exec(qapp);
// ------ Dipose QT Objects ---------------------------//
//
qt_qobject_del(window);
qt_qobject_del(qapp);
return 0;
}
以脚本形式运行文件 d_client.d。
$ rdmd d_client.d
[TRACE] Create QAppliction Object Ok
[TRACE] Button click event happened => state = 11.
[TRACE] Button click event happened => state = 12.
[TRACE] Button click event happened => state = 13.
[TRACE] Button click event happened => state = 14.
[TRACE] Button click event happened => state = 15.
... ... ... ... ... ... ... ...
构建并运行可执行文件。
$ dmd d_client.d -g
$ ./d_client
[TRACE] Create QAppliction Object Ok
[TRACE] Button click event happened => state = 11.
[TRACE] Button click event happened => state = 12.
[TRACE] Button click event happened => state = 13.
... ... ... ... ... ... ... ... ... ... ... ...
... ... ... ... ... ... ... ... ... ... ... ...
用户界面截图:
图3:Dlang(D语言)的用户界面
1.3.7 通用Lisp客户端代码
这个通用lisp客户端代码,更确切的说是Stell Bank通用Lisp(SBCL),通过使用CFFI外函数接口库来加载共享库libqtwrapper.so。
在Fedora 32上安装SBCL的过程和依赖关系。
# Fedora as root
$ dnf install sbcl
# Download and install quicklisp
$ >> curl -O -L http://beta.quicklisp.org/quicklisp.lisp
$ >> rlwrap sbcl --load quicklisp.lisp
# Run sbcl and install quicklisp
$ >> rlwrap sbcl --load quicklisp.lisp
* (quicklisp-quickstart:install)
* (ql:add-to-init-file)
# Install CFFI
* (ql:quickload "cffi")
# Quit SBCL repl
* (quit)
文件: sbcl_client.lisp
;; ---- Experimental binding to Qt5 GUI Framework ----------;;
;;
(require :cffi)
(cffi:define-foreign-library qtw (:unix "libqtwrapper.so" ))
(cffi:use-foreign-library qtw )
;; ------- Define bindings ---------------------------..
(cffi:defcfun "qt_app_new2" :pointer)
(cffi:defcfun "qt_widget_new" :pointer (parent :pointer) (type :int))
;; Signature: void qt_widget_show(QWidget* self)
(cffi:defcfun "qt_widget_show" :void (self :pointer))
;; Signature: int qt_app_exec(QApplication* self)
(cffi:defcfun "qt_app_exec" :int (self :pointer))
;; Signature: void qt_qobject_del(QObject* self)
(cffi:defcfun "qt_qobject_del" :void (self :pointer))
;; Signature: void qt_widget_setText(QWidget* self, const char* text)
(cffi:defcfun "qt_widget_setText" :void (self :pointer) (text :string))
;; Signature: QLayout* qt_layout_new(QWidget* parent, int type)
(cffi:defcfun "qt_layout_new" :pointer (self :pointer) (type :int))
;; Signature: QObject* qt_QFormLayout_addWidgetAndLabel(
;; QFormLayout* self
;; , int type, const char* label)
(cffi:defcfun "qt_QFormLayout_addWidgetAndLabel"
:pointer (self :pointer) (type :int))
;; Callback
;; ---------------------------------------------------
;; Signature:
;; -----------------------------------------------------
;; void qt_button_onClicked(
;; QAbstractButton* self
;; , void* ctx
;; , void (* callback) (void* self) )
(cffi:defcfun "qt_button_onClicked"
:void (self :pointer) (ctx :pointer) (callback :pointer))
;; Signature: void qt_msgbox_info( QWidget* parent
;; , const char* title
;; , const char* text )
(cffi:defcfun "qt_msgbox_info"
:void (parent :pointer) (title :string) (text :string))
(defvar WIDGET-WINDOW-MAIN 2)
(defvar WIDGET-BUTTON 3)
(defvar WIDGET-LINE-EDIT 5)
(defvar LAYOUT-QFORM 3)
;; ----- Define variables --------------------------- ;;
;;
;; This function must always be called
;; before creating any widget.
(defvar qapp (qt-app-new2))
;; Create main window
(defvar window (qt-widget-new (cffi:null-pointer) WIDGET-WINDOW-MAIN))
(cffi:with-foreign-string (title "My LISP QT Window")
(qt-widget-settext window title))
(defvar form (qt-layout-new window LAYOUT-QFORM))
;; Note: Common Lisp is case insensitive
(defvar button (qt-QFormLayout-addWidgetAndLabel form WIDGET-BUTTON))
(defvar line-edit (qt-QFormLayout-addWidgetAndLabel form WIDGET-LINE-EDIT))
;; Set button label
(cffi:with-foreign-string (title "click me")
(qt-widget-settext button title))
(defvar counter 0)
;; Define button callback
;; void (* callback) (void* self) )
(cffi:defcallback button-callback
;; Return type of callback
:void
;; List containing callback arguments
( (ctx :pointer) )
;; Function main body
(progn
;; Increment counter
(setf counter (+ counter 1))
(cffi:with-foreign-string
(text (format nil "counter set to ~a" counter ))
(qt-widget-settext button text))
(cffi:with-foreign-string
(title "User notification")
(cffi:with-foreign-string
(text "Counter clicked")
(qt-msgbox-info window title text)))
(print (format t "Counter set to = ~D \n" counter))
))
;; --- End of button callback() ----- ;;
(qt-button-onClicked button (cffi:null-pointer)
(cffi:callback button-callback))
;; Run QT event loop and blocks current thread.
(qt-app-exec qapp)
;; Dispose C++ objects calling destructors
(qt-qobject-del window)
(qt-qobject-del qapp)
运行通用Lisp客户端代码。
$ >> export LD_LIBRARY_PATH=$PWD/build
$ >> sbcl --load sbcl_client.lisp
This is SBCL 2.0.1-1.fc32, an implementation of ANSI Common Lisp.
More information about SBCL is available at <http://www.sbcl.org/>.
SBCL is free software, provided as is, with absolutely no warranty.
It is mostly in the public domain; some portions are provided under
BSD-style licenses. See the CREDITS and COPYING files in the
distribution for more information.
[TRACE] Create QAppliction Object Ok
... ... ... ... ...
用户界面截图。
图4:通用Lisp(SBCL)客户端代码的用户界面。
通过www.DeepL.com/Translator(免费版)翻译