Skip to content

Latest commit

 

History

History
2732 lines (2073 loc) · 89.1 KB

CwrapperToQtLibrary.org

File metadata and controls

2732 lines (2073 loc) · 89.1 KB

C Wrappers for C++ Libraries and Interoperability

C Wrappers for C++ Libraries and Interoperability

Overview

One of the main advantages of the C language is the the possibility of calling libraries written in C from other programming languages via FFI (Foreign Function Interface), which allows calling C functions from C shared libraries. Unlike C, calling C++ libraries directly via FFI is not feasible as C++ lacks a standard ABI - Application Binary interface. A solution for this issue is to create a C wrapper for the C++ library using opaque pointer, incomplete types and extern C annotation.

Calling C++ from C

This section presents how to call a statically linked C++ cod from C.

File: cppcode.hpp

  • Header file that can be used by a C or C++ code. All code between #ifndef __cplusplus … #edif is ignored by a C compiler.
#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

File: cppcode.cpp

  • Implementation of classes and C interface functions.

Implementation of class RunStat member functions:

  ... ... ... ...

 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++;
 }

... ...  ... ...  ... ...  ... ...

Implementation of the C-interface functions for class std::string.

//--------- 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;
}

Implementation of the C-interface functions for class RunStat.

//---------- 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();
}

File: main.c

  • C client code that uses C-interface functions export by the C++ file cppcode.
#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;
}

File: 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)

Manual compilation:

# 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

Program output:

./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

Calling Qt5 Widgets library from many languages

Overview

The technique used in this sample code is the hourglass-design pattern, that exposes C++ classes and methods through a thin C-89 API for ensuring better ABI stability, allowing C++ client codes built with any other C++ compiler to link against the library and making the it friendlier to other programming languages via FFI - foreign-function interfaces or linking against C-compatible functions. In this pattern, classes are only accessible to client code as opaque types or void pointers and member functions (class methods) are only accessible through functions without name mangling, that are annoted with extern “C”. A C++ SDK (Software Development Kit) wrapping the thin C-89 API can also be provided for making the library usage easier by C++ client codes. This sample code provides a C wrapper to the QT5 Widgets GUI library. The hourglass design-patter is not exclusive to C++, it can also be applied other programming languages such as Rust, D - DLang-language, Nim, Pascal and Zig as those languages allow exposing C-compatible functions without name mangling via a extern “C” like annotations.

This C-wrapper allows calling Qt5 widgets library from C, Python, Common Lisp, Nim, Rust, Julia language and also D-language.

Hourglass Design pattern Schema for a C++ library:

SDK means Software Development Kit 
                                                              C++ library with stable ABI 
+-------------------+                                         +------------------------+
|                   |      Library                 Library    | Library's Private C++  |
| C++ client code   +<<<- (C++ SDK) -(+)----<<-- (C-89 API) --+ Implementation         |
|                   |     header-only            extern "C"   | no exposed             |
+-------------------+    library wrapper             |        +------------------------+
                                                     |
+---------------------+                             \ / 
| Client code in      |                              |
| higher level        |       FFI Foreign-Function   | 
| language            +<<<--- Interface Wrapper <<---/               
| (C#, Python, Ruby)  |       or Linkage against     |
+---------------------+       the  C-89 API          |
                                                     |
+---------------------+                              +
| Rust Client Code    |                             /
|                     +<<<-- (Rust SDK) <<-----<<<-/   
|                     |
+---------------------+                       

This technique can be used for many purposes, including:

  • Build C++ libraries that can be distributed as pre-compiled binaries and can be used with any other C++ compiler as long as the client code only uses the C-89 thin API (Application Programming Interface).
  • Shipping proprietary, closed source libraries, that can be linked against by C++ source code built with any other compiler without any ABI issues as long as the client code only links against the C-89 thin API.
  • Making it easier for client code written in high level languages, such as Python, Julia, Ruby or C# (CSharp) to use the library through a FFI - Foreign Function Interface or linkage against the C89 thin API.
  • Create C-bindings to already existing C++ libraries for allowing them to be used via FFI - Foreign-Function Interfaces.
  • Implement C libraries in C++, Rust, D (Dlang), Nim or Zig programming languages.

Known uses of the hourglass technique:

  1. Tensorflow library for Machine Learning
    • Leading machine learning library with bindings to many languages including Python, JavaScript, C++, Java, Julia, Matlab, R, Ruby, Rust and so on.
    • Note: Despite Tensorflow be implemented in C++, the library only exposes a C-89 interface.
    • See: https://www.tensorflow.org/api_docs
  2. TileDB
    • Embeddable database that can store time-series, geospatial data, arrays, matrices, sparse matrices and tensors (multi dimensional arrays).
  3. Botan C++ TLS crytography library
    • C++ library alternative to OpenSSL for TLS (Transport Layer Security) or SSL (Socket Layer Security) for encryption of data at transit of TCP or UDP network protocols. This library has C-89 API and bindings for Python, Ruby, Java, Rust, Haskell and Odin programming languages. Note that the Python binding uses ctypes for loading the C-89 thin API symbols from the botan shared library.
  4. Squirrel
    • An embedded scripting language, for adding scripting capabilities to C or C++ applications, written in C++, but only exposed as a C-89 library.

Futhere reading:

Documentation of features used in this experiment

QT5 Documentation for widgets classes used in the sample code:

Python 3 Ctypes FFI Documentation:

Julia language:

C# (Csharp) for Dotnet Core:

D Language (DLang:

Nim Programming Language:

Lisp - SBCL (Steel Bank Common Lisp):

Wrapper Files

Source code for C-wrapper

File: Makefile

# Build C wrapper to Qt library
wrapper:
        cmake -H. -B_build -DCMAKE_BUILD_TYPE=Debug
        cmake --build _build --target all 

# Run python client code
python:
        env LD_LIBRARY_PATH=$(PWD)/_build ./pywrapper.py

# Build and run dotnet client code
dotnet:
        env LD_LIBRARY_PATH=$(PWD)/_build dotnet build myapp.csproj
        env LD_LIBRARY_PATH=$(PWD)/_build ./bin/myapp 

# Build rust client
rust:
        rustc rustclient.rs -l qtwrapper -L_build/

File: 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  REQUIRED)

#=============== Target Configurations ============#

include_directories(.)

# --------------------------------------------------#
          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 )      

       add_executable( client2 client.cpp)
target_link_libraries( client2  qtwrapper )      

File: qtwrapper.cpp

  • C-wrapper or C-interface for QT5 C++ GUI library
#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);  }
                    );
}

File: qtwrapper.h (header for the C-wrapper or 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) );

Considerations:

  1. (extern “C”) enforces C linkage, in other words, the function with this annotation must have a C compatible signature and use C compatible types.
  2. The C++ operator new throws exception when there is not enough memory, it throws std::bad_alloc. As C does not support exceptions, it is necessary to use the operator new (std::nothrow) that returns null pointer instead of throwing an exception.
  3. Due to the lack of C support for exceptions, any exception throw in the functions annotated with extern “C” should be returned as as an additional function parameter. Moreover, C++ exceptions are from different compilers may not even be compatible due to the lack of standard C++ ABI.
  4. As C functions does not support closures, they cannot keep state, therefore it is necessary to add an extra void pointer parameter (void*) in the function pointers arguments and in the functions that takes them as parameters such as, qt_button_onClicked(). This additional (void*) pointer allows the calling code to pass and keep state.
  5. Despite that this technique, for creating a C-wrapper (C interface), was used for Qt5 Widgets library, this approach can be used for any other C++ library such as: Boost-Libraries, WxWidgets GUI library, Windows MFC (Microsoft Foundation Classes) and so on.
  6. This C-wrapper technique can also be used for creating C-libraries in C++, which can be loaded from any programming languages (Python, Ruby, Lisp, …) via FFI (Foreign Function Interface) or by linking against the C-wrapper shared library in the case of compiled language such as Golang, Rust or D-lang.
  7. If Qt had C-binding similar to this one, shipped with Qt on every Linux distribution that uses Qt or KDE, it would be possible to create Qt applications in any other programming language than C++, by loading the C-binding via FFI foreign function interface or by linking against the C-binding if the programming language is compiled and supports linking against C ABI (Application Binary Interface).
  8. Another benefit of a C-wrapper for C++ libraries is that a C-wrapper provides a stable ABI (Application-Binary Interface) which allows an application built with different C++ compiler to link against the library C-interface without any library recompilation and avoiding compiling the library with all possible C++ compilers that a client-code can possibly use.
  9. An alternative method for accessing the C++ libraries from higher level languages is by using SWIG (SWIG wrapper generator), which parses interface files and library header files for generating C-code for programming language-specific native interface API, which allows creating native code libraries in C that are loaded at runtime via dlopen() calls, on Unix-like systems, or LoadLibrary() calls on Windows. The terminology native-interface API was borrowed from Java’s JNI (Java Native Interface API) as unlike FFI (Foreign Function Interfaces), there is a lack of proper terminology for describing this feature. For instance, the Pythons’ native interface API allows creating Python modules (aka libraries) entirely in C. The disadvantage of using SWIG is that, it requires access to the library source code and compilation of generated binding code for every programming language that binding will be generated for while a C-wrapper needs compilation only once.

Building the wrapper file.

Note: this code was compiled and tested on a Linux - Fedora 32, 64 bits machine which contains pre-installed Qt5 libraries.

Fetch the sources:

$ 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

Build the project: (Note: Tested on Linux Fedora-32 64 bits x86-64)

$ cmake --config Debug -H. -Bbuild
$ cmake --build build --target

Check binaries:

$ 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

Show symbols exported by the C-wrapper shared library:

  • Note: On windows, the equivalent tool is dumpbin and on MacOSX, the equivalent tool is 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
  ... ... ... ... ... ... ... ... 
  ... ... ... ... ... ... ... ... 

Show exported symbols demangling (aka decoding) C++ mangled symbols:

 $ >> 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*)
   ... ... ... ... ... 
 ... ... ... ... ... 

C client code

File: client1.c (C-client code)

#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;
}

Run executable:

$ 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 
 ...  ...  ...  ...  ...  ... 
 ...  ...  ...  ...  ...  ...  ... 

User Interface Screenshot:

images/cwrapper-qt5-1.png

C++ client code

File: cpp-client.cpp

#include <iostream>
#include <functional>
#include <cassert>

// Indicates that symbols from this header file have C linkage (C ABI)
// rather than C++ ABI.
extern "C" {
    #include "qtwrapper.h"
}

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;

using ButtonCallck = std::function<void ()>;

// Implementation with type erasure and dynamic allocations
// Note: std::functions<> uses dynamic allocaiton underneath 
void onClicked(QPushButton* btn, ButtonCallck func)
{

    qt_button_onClicked(btn, &func, [](void* ctx){
        auto callback = reinterpret_cast<ButtonCallck*>(ctx);
        callback->operator()();
    });
}

// Implementation without dynamic allocation 
// DOES NOT WORK: segmentation fault 
template<typename Callback>
void onClicked_tpl(QPushButton* btn, Callback func)
{
    qt_button_onClicked(btn, &func, [](void* ctx){
        // auto callback = (Callback*) ctx;  
        auto callback = reinterpret_cast<Callback*>(ctx);
        //callback();
        callback->operator()();
    });
}

int main(int argc, char** argv)
{

    QApplication* qapp = qt_app_new2();

    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);

    QPushButton* btn1 = qt_QFormLayout_addWidgetAndLabel(form, WIDGET_QPUSH_BUTTON, "");
    qt_widget_setText(btn1, "Button 1");

    QLabel*  disp = qt_QFormLayout_addWidgetAndLabel(form, WIDGET_QLABEL, "Result");

    // qt_widget_setText(btn1, "Button 1");
    int counter = 0;

    onClicked(btn1, [&counter, disp]{
        std::cout << " [TRACE] (*) Counter = " << counter++ << "\n";
        auto text = std::string(" Counter set to = ") + std::to_string(counter);
        qt_widget_setText(disp, text.c_str());

        if( counter > 10 ){
            qt_msgbox_info(nullptr, "Notification", "Counter greater than 10");
        }
    });

    // Blocks current thread until window is closed. 
    qt_app_exec(qapp);

    // Dispose QT objects 
    qt_qobject_del(win);
    qt_qobject_del(qapp);
}

Screenshot:

images/cwrapper-qt5-cpp-client.png

Python3 Client Code

The following python script, loads the C shared library wrapper for Qt5 widgets using Python3 ctypes FFI (Foreign Function Interface). Although, Python3 already has PyQt and Pyside2 library bindings what make this code seems pointless, the objective of this code is to be a proof-of-concept about how to load C++ libraries via FFI (Foreign Function Interface).

Files:

File: pywrapper.py

#!/usr/bin/env python3
import ctypes 
from ctypes import (c_int, c_int, c_char, c_void_p)

# ===== Aliases for making life easier ===========#
#-------------------------------------------------#

c_void  = None                         # void 
c_pconst_char = ctypes.POINTER(c_char) # const char* 
c_pnull = c_void_p(0)                  # Null pointer 
QObject = c_void_p                     # void* - void pointer 
Qapp    = c_void_p 
QWidget = c_void_p
QLayout = c_void_p 

#  =========== Load shared library ================#
#--------------------------------------------------#

#  Arguments of: ctypes.cdll.LoadLibrary(<LIBRARY>)
#
#  On Linux, the operating system linker looks for
# the library at directories listed in $LD_LIBRARY_PATH
# environment variable or in default set of directories 
# such as /lib, /usr/lib and so on. 
#
#  On Windows, the operating system looks for the library 
# in C:\\Windows\System32 directory or in directories
# listed in %PATH% environment variable.
# 
# The argument of LoadLibrary() can also be the absolute
# path to the shared library. 
#

lib = ctypes.cdll.LoadLibrary("libqtwrapper.so")

#======= Set types of C subroutines ===================#
#------------------------------------------------------#

# -- Note: There is no keyword const in Python
# it is assumed that the user or calling code
# will never modify the following definitions. 
WIDGET_WINDOW          = 0
WIDGET_WINDOW_MAIN     = 2
WIDGET_QPUSH_BUTTON    = 3
WIDGET_QLABEL          = 4
WIDGET_QLINE_EDIT      = 5
WIDGET_QDOUBLE_SPINBOX = 6
LAYOUT_QFORM_LAYOUT    = 3

lib.qt_app_new2.argtypes = []
lib.qt_app_new2.restype  = Qapp 

lib.qt_app_exec.argtypes = [ Qapp ]
lib.qt_app_exec.restype  = c_void

lib.qt_qobject_del.argtypes = [ QObject ]
lib.qt_qobject_del.restype  = c_void

lib.qt_window_main.argtypes = []
lib.qt_window_main.restype  = QWidget

lib.qt_widget_setText.argtypes = [ QWidget, c_pconst_char  ]
lib.qt_widget_setText.restype  = c_void 

lib.qt_layout_new.argytpes = [ QWidget, c_int ]
lib.qt_layout_new.restype  = QLayout

lib.qt_msgbox_info.argtypes = [ QWidget, c_pconst_char, c_pconst_char ]
lib.qt_msgbox_info.restype = c_void

lib.qt_QFormLayout_addWidgetAndLabel.argtypes = [QWidget, c_int, c_pconst_char ]
lib.qt_QFormLayout_addWidgetAndLabel.restype  = QWidget

# Callback type: 
FnButtonCallback = ctypes.CFUNCTYPE(  c_void    # Return tupe 
                                    , c_void_p  # Type of first parameter (void*)
                                    )

lib.qt_button_onClicked.argtypes = [ QWidget, c_void_p, FnButtonCallback ]
lib.qt_button_onClicked.restype  = c_void 

#===== Create the Qt5 Widgets - GUI - Graphics User ==========#
#-------------------------------------------------------------#

qapp = lib.qt_app_new2()

window = lib.qt_window_main()
lib.qt_widget_setText(window, "Qt5 Widgets Window from Python3 FFI".encode("UTF-8"))

form = lib.qt_layout_new(window, LAYOUT_QFORM_LAYOUT)

label_output = lib.qt_QFormLayout_addWidgetAndLabel(
                form, WIDGET_QLABEL, b"Result" )

btnA = lib.qt_QFormLayout_addWidgetAndLabel(
                form, WIDGET_QPUSH_BUTTON, b"Label A" )

lib.qt_widget_setText(btnA, "Button A".encode("UTF-8") )

btnB = lib.qt_QFormLayout_addWidgetAndLabel(
                 form, WIDGET_QPUSH_BUTTON, b"Label B")

lib.qt_widget_setText(btnB, b"Button B" )

entry = lib.qt_QFormLayout_addWidgetAndLabel(
                        form, WIDGET_QLINE_EDIT, b"Information:")

spinbox = lib.qt_QFormLayout_addWidgetAndLabel(
                   form, WIDGET_QDOUBLE_SPINBOX, b"Price:")


print(" [PYTHON] Qt5 GUI Created Ok. ")

#===== Install callbacks (event handlers) ===================#
#------------------------------------------------------------#

counter = 0

# Note: Python lacks multi-line lambdas
def btnA_callback(ctx: c_void_p) -> c_void:
    global counter 
    counter = counter + 1
    text = f" [PYTHON] Button A Clicked => Counter = {counter} "
    text = text.encode("UTF-8")
    print(text)
    lib.qt_widget_setText(label_output, text)
    lib.qt_widget_setText(entry, text)    

obj_btnA_callback = FnButtonCallback(btnA_callback)
lib.qt_button_onClicked(btnA, c_pnull, obj_btnA_callback)

def btnB_callback(ctx: c_void_p) -> c_void:
    global counter
    counter = counter - 1
    text = f" [PYTHON] Button B Clicked => Counter = {counter} "
    text = text.encode("UTF-8")    
    print(text)
    lib.qt_widget_setText(label_output, text)
    lib.qt_widget_setText(entry, text)        
    lib.qt_msgbox_info( window, b"Information:", text)

obj_btnB_callback = FnButtonCallback(btnB_callback)
lib.qt_button_onClicked(btnB, c_pnull, obj_btnB_callback)


#====== Run QT event loop blocking current thread ============#
#-------------------------------------------------------------#
print(" [PYTHON] Running QT event loop => Main thread blocked!. ")
lib.qt_app_exec(qapp)

#===== Dispose C++ objects loaded via FFI =====================#
#--------------------------------------------------------------#

# Note: Only the root object (wihout any parent element) needs
# to be disposed (aka released from memory).
lib.qt_qobject_del(window)

lib.qt_qobject_del(qapp)


print(" [PYTHON] Application terminated gracefully. Ok. ")

Running

 # Directory where there are the *.so shared libraries files (shared objects)
 $ export LD_LIBRARY_PATH=$PWD/_build 

 $ python3 pywrapper.py
 [TRACE] Create QAppliction Object Ok
 [PYTHON] Qt5 GUI Created Ok. 
 [PYTHON] Running QT event loop => Main thread blocked!. 
b' [PYTHON] Button A Clicked => Counter = 1 '
b' [PYTHON] Button A Clicked => Counter = 2 '
b' [PYTHON] Button A Clicked => Counter = 3 '
b' [PYTHON] Button A Clicked => Counter = 4 '
b' [PYTHON] Button A Clicked => Counter = 5 '
.... ... ...   .... ... ...   .... ... ...   .... ... ... 
.... ... ...   .... ... ...   .... ... ...   .... ... ... 
b' [PYTHON] Button B Clicked => Counter = 9 '
b' [PYTHON] Button B Clicked => Counter = 8 '
b' [PYTHON] Button A Clicked => Counter = 9 '
b' [PYTHON] Button A Clicked => Counter = 10 '
b' [PYTHON] Button A Clicked => Counter = 11 '
 [PYTHON] Application terminated gracefully. Ok. 

Or:

$ env LD_LIBRARY_PATH=$PWD/_build python3 pywrapper.py

Or:

$ chmod +x pywrapper.py 
$ env LD_LIBRARY_PATH=$PWD/_build ./pywrapper.py

Screenshot

images/cwrapper-qt5-6.png

Julia Client Code (loader.jl)

File: loader.jl (Julia language client code)

#!/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()

Run julia script / attempt 1 -> error:

$ julia loader.jl 
ERROR: LoadError: could not load library "libqtwrapper"
libqtwrapper.so: cannot open shared object file: No such file or directory
Stacktrace:
... ... .... ... ... 
... ... ... ... ... 

Run julia script / attempt 2 -> works:

  • Note: LD_LIBRARY_PATH only works on Linux. The equivalent of this variable for windows is the PATH environment variable and for MacOSX, the equivalent is DYLD_LIBRARY_PATH.
  • The libqtwrapper.so shared library can be installed on Linux, by moving the library file to /lib directory.
  $ 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
--------------------------------- 
... ... ... ... 

The previous Julia script could also be run in the following way. The advantage of this method is that, the variable LD_LIBRARY_PATH is only set for the command: ‘$ julia loader.jl’.

$ env LD_LIBRARY_PATH=build:$LD_LIBRARY_PATH julia loade.jl 

The library can be consumed without setting the environment variable LD_LIBRARY_PATH on Linux by moving the library file to /usr/lib, /usr/local/lib or /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
... .... .... 
... .... .... 

User Interface Screenshot:

images/cwrapper-qt5-2.png

C# - Dotnet Core Client Code

This section contains the source code of a C# (dotnet) core client code that loads the C-wrapper shared library (libqwrap.so) for Qt5 widgets framework through dotnet Pinvoke dotnet feature.

File: myapp.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

</Project>

File: Program.cs - C# Csharp / core source code (dotnet core)

using System;
using System.Runtime.InteropServices;

namespace myapp
{

    class Wrapper 
    {
        public const int WIDGET_WINDOW = 0;
        public const int WIDGET_WINDOW_MAIN = 2;
        public const int WIDGET_QPUSH_BUTTON = 3;
        public const int WIDGET_QLABEL = 4;
        public const int WIDGET_QLINE_EDIT = 5;
        public const int WIDGET_QDOUBLE_SPINBOX = 6;
        public const int LAYOUT_QFORM_LAYOUT = 3;


        [DllImport("qtwrapper")]
        public static extern IntPtr qt_app_new2();

        [DllImport("qtwrapper")]
        public static extern IntPtr qt_app_exec(IntPtr self);


        [DllImport("qtwrapper")]
        public static extern IntPtr qt_qobject_del(IntPtr self);

        [DllImport("qtwrapper")]
        public static extern IntPtr qt_widget_new(IntPtr parent, int type);

        [DllImport("qtwrapper")]
        public static extern IntPtr qt_window_main();

        [DllImport("qtwrapper")]
        public static extern void qt_widget_setText(IntPtr self, string text);

        [DllImport("qtwrapper")]
        public static extern void qt_msgbox_info(IntPtr parent, string title, string text);

        [DllImport("qtwrapper")]
        public static extern IntPtr qt_layout_new(IntPtr parent, int type); 

        [DllImport("qtwrapper")]
        public static extern IntPtr qt_QFormLayout_addWidgetAndLabel(IntPtr parent, int type, string label); 

        // A C# delegate is similar to a C++ function pointer type. 
        public delegate void button_callback(IntPtr ctx);

        [DllImport("qtwrapper")]
        public static extern void qt_button_onClicked(IntPtr self, IntPtr ctx, button_callback callback); 

 
    }

    class Program
    {

        static void Main(string[] args)
        {
                Console.WriteLine(" [TRACE] Program started Ok. ");

            // Create a QApplication* C++ object 
            // This object must be instantiated before any other Qt Widgets object. 
            IntPtr qapp = Wrapper.qt_app_new2();
            Console.WriteLine(" [TRACE] qapp (IntPtr) value = {0} ", qapp);


            // Create a Qt Window object QWidget* 
            IntPtr qwindow = Wrapper.qt_window_main();
            Wrapper.qt_widget_setText(qwindow, "My QtWidget window in C# dotnet core on Linux!");


            IntPtr form = Wrapper.qt_layout_new(qwindow,  Wrapper.LAYOUT_QFORM_LAYOUT);
            IntPtr btn1 = Wrapper.qt_QFormLayout_addWidgetAndLabel(form , Wrapper.WIDGET_QPUSH_BUTTON , "");
            Wrapper.qt_widget_setText(btn1, "Increment");

            IntPtr btn2 = Wrapper.qt_QFormLayout_addWidgetAndLabel(form, Wrapper.WIDGET_QPUSH_BUTTON, "");
            Wrapper.qt_widget_setText(btn2, "Messagebox");
          
            IntPtr text_entry = Wrapper.qt_QFormLayout_addWidgetAndLabel( form, Wrapper.WIDGET_QLINE_EDIT, "Result"); 

            // ------------ Set event handlers --------------// 
            //-----------------------------------------------// 

            int counter = 0;

            // Set button 1 event handler 
            Wrapper.qt_button_onClicked(btn1, IntPtr.Zero, (IntPtr ctx) => 
            {
                String s = String.Format("Button clicked {0} times.", counter++);
                Console.WriteLine(" [TRACE] QtPushButton cliked Ok. => " + s);
                Wrapper.qt_widget_setText(text_entry, s);
            });

            Wrapper.qt_button_onClicked(btn2, IntPtr.Zero, (IntPtr ctx) => 
            {
                Wrapper.qt_msgbox_info(IntPtr.Zero , "System Notification" , "Batch workload finished Ok." );
            });

            // Run application and block current thread. 
            Wrapper.qt_app_exec(qapp);

            // ---- Dispose C++ objects -------------------//
            // --------------------------------------------//
            Wrapper.qt_qobject_del(qwindow);
            Wrapper.qt_qobject_del(qapp);
            Console.WriteLine(" [TRACE] Program terminated successfully Ok. ");
        }
    }
}

Building

The following command sets the LD_LIBRARY_PATH environment variable for allowing the linker (aka loader) to find the shared library (libqtwrapper.so on Linux or BSD or qtwrapper.dll on Windows). On Windows, the operating system will linker will look for the shared library on the same directory where is the executable, then on predefined directories such as C:\Windows\System32\, and finally on directories listed in the %PATH% enviroment variable.

export LD_LIBRARY_PATH=$HOME/_build/

Adding the shared library directory path to the %PATH% enviroment variable has the same effect of setting LD_LIBRARY_PATH on Linux. This setting can be performed on cmd.exe shell via the as in the following code block. The shared library can be located without setting the PATH variable by moving it to the System32 directory or placing it on the same directory where is the executable.

set PATH="%cd%\_build\path\to\shared\library\directory";%PATH%

Build from command line:

$ ~/dotnet/dotnet build myapp.csproj
Microsoft (R) Build Engine version 16.9.0+57a23d249 for .NET
Copyright (C) Microsoft Corporation. All rights reserved.

  Determining projects to restore...
  All projects are up-to-date for restore.
  myapp -> /home/unixuser/Documents/temp-projects/qt5-cwrapper/bin/Debug/net5.0/myapp.dll

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:01.06

Check the executable:

 $ tree ./bin
./bin
└── Debug
    └── net5.0
        ├── myapp
        ├── myapp.deps.json
        ├── myapp.dll
        ├── myapp.pdb
        ├── myapp.runtimeconfig.dev.json
        ├── myapp.runtimeconfig.json
        └── ref
            └── myapp.dll

3 directories, 7 files

 $ ldd bin/Debug/net5.0/myapp
        linux-vdso.so.1 (0x00007fff251d4000)
        libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fa56e3e0000)
        libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fa56e3da000)
        libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007fa56e1f9000)
        libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fa56e0aa000)
        libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007fa56e08f000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa56dea5000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fa56e64b000)

Running the application:

 $ file bin/Debug/net5.0/myapp
bin/Debug/net5.0/myapp: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, 
interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=1894b9678ab18e8406448464668a8949e4f9d2b5, stripped

 $ ~/Documents/temp-projects/qt5-cwrapper master
 $ bin/Debug/net5.0/myapp
 [TRACE] Program started Ok. 
 [TRACE] Create QAppliction Object Ok
 [TRACE] qapp (IntPtr) value = 94225009981200 
 [TRACE] QtPushButton cliked Ok. => Button clicked 0 times.
 ... ... ... .. 
 ... ... ... ... ... ... ... ..

Running the application via dotnet core tool:

$ ~/dotnet/dotnet run --project=myapp.csproj

Screenshot

images/cwrapper-qt5-5.png

D language - D-lang Client Code

Note: This client code load function pointers from the C-wrapper shared library at runtime via Dlopen() API. The benefit of this approach over linking directly, at-compile time, is that application can build on machines without Qt installation or on dockers images without X11 installed.

Files:

File: d_client.d (Dlang / D-language client code)

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;
}

Run file d_client.d as a script:

$ 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. 
  ... ... ... ... ... ... ... ...   

Build and run executable:

$ 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.
 ... ... ...  ... ... ...  ... ... ...  ... ... ... 
 ... ... ...  ... ... ...  ... ... ...  ... ... ... 

User Interface Screenshot:

images/cwrapper-qt5-3.png

Nim language - Nim Client Code

File: nim_app.nim

# Necessary for defining lambdas in Nim, otherwise compilation errors happen.
import sugar

# For converting number to string
import strutils

const qlib = "libqtwrapper.so"

type 
    QApplication   = pointer 
    QObject        = pointer
    QLineEdit      = pointer 
    QWidget        = pointer  
    QPushButton    = pointer
    QFormLayout    = pointer 
    QDoubleSpinBox = pointer
    Context        = pointer
    QLayout        = pointer 

type 
    callback_clicked = proc(ctx: Context): void
    callback_onValueChanged = proc(ctx: Context): void


const 
    WIDGET_WINDOW           = 1 
    WIDGET_WINDOW_MAIN      = 2 
    WIDGET_QPUSH_BUTTON     = 3
    WIDGET_QLABEL           = 4
    WIDGET_QLINE_EDIT       = 5 
    WIDGET_QDOUBLE_SPINBOX  = 6
    LAYOUT_QFORM_LAYOUT     = 3


proc qt_app_new2(): QApplication {.importc: "qt_app_new2", dynlib: qlib}

proc qt_app_exec2(qapp: QApplication): void {.importc: "qt_app_exec", dynlib: qlib}

proc qt_qobject_del(self: QWidget): void  {.importc: "qt_qobject_del", dynlib: qlib}

# Set text and label
proc qt_widget_setText(self: QWidget, text: cstring): void 
    {.importc: "qt_widget_setText", dynlib: qlib}

# Factory function for instantiating any QT widget
proc qt_widget_new(parent: QWidget, ntype: cint): QPushButton
    {.importc: "qt_widget_new", dynlib: qlib}

proc qt_widget_show(self: QWidget): void
    {.importc: "qt_widget_show", dynlib: qlib}

#  void  qt_button_onClicked(QAbstractButton* self, void* ctx, void (* callback) (void*) );
proc qt_button_onClicked(self: QPushButton, ctx: Context, callback: callback_clicked ): void
    {.importc: "qt_button_onClicked", dynlib: qlib}

proc qt_msgbox_info(parent: QWidget, title: cstring, text: cstring): void
    {.importc: "qt_msgbox_info", dynlib: qlib}

# Retrieves text from QLineEdit widget
proc qt_QLineEdit_text(self: QLineEdit): cstring
    {.importc: "qt_QLineEdit_text", dynlib: qlib}

proc qt_layout_new(parent: QWidget, ntype: cint): QFormLayout
    {.importc: "qt_layout_new", dynlib: qlib}

proc qt_QFormLayout_addWidgetAndLabel(self: QFormLayout, nype: cint, label: cstring): QObject
    {.importc: "qt_QFormLayout_addWidgetAndLabel", dynlib: qlib}

proc qt_QDoubleSpinBox_value(self: QDoubleSpinBox): cdouble
    {.importc: "qt_QDoubleSpinBox_value", dynlib: qlib}

proc qt_QDoubleSpinBox_setValue(self: QDoubleSpinBox, value: cdouble): void 
    {.importc: "qt_QDoubleSpinBox_setValue", dynlib: qlib}


proc qt_QDoubleSpinBox_onValueChanged( self: QDoubleSpinBox
                                      ,ctx: Context
                                      ,callback: callback_onValueChanged 
                                      )
    {.importc: "qt_QDoubleSpinBox_onValueChanged", dynlib: qlib}


    #-------------------------------------------------#
    #             QT Application                      #
    #-------------------------------------------------#

echo " [TRACE] Staring application OK."

var app = qt_app_new2()

# The window is immediately shown after created.
var window = qt_widget_new(nil, WIDGET_WINDOW_MAIN)

# UFS - Universal function call => the object is the first function parameter
## Same as:  qt_widget_setText(window, "NIM + Qt5-Widgets Application")
window.qt_widget_setText("NIM + Qt5-Widgets Application")
# Instantiate QT widgets 
var form    = qt_layout_new(window, LAYOUT_QFORM_LAYOUT)
var buttonA = form.qt_QFormLayout_addWidgetAndLabel(WIDGET_QPUSH_BUTTON, "ButtonA's label")
var buttonB = form.qt_QFormLayout_addWidgetAndLabel(WIDGET_QPUSH_BUTTON, "ButtonB's label")
var buttonC = form.qt_QFormLayout_addWidgetAndLabel(WIDGET_QPUSH_BUTTON, "ButtonC's label")
var spinbox = qt_QFormLayout_addWidgetAndLabel(form, WIDGET_QDOUBLE_SPINBOX, "Price")
var entry   = qt_QFormLayout_addWidgetAndLabel(form, WIDGET_QLINE_EDIT, "User entry")

qt_widget_setText(buttonC, "ButtonC")
# Set button text using (UFS) universal function call
buttonA.qt_widget_setText("ButtonA")
buttonB.qt_widget_setText("ButtonB")

 #--------- Install event handlers (callbacks) -----------#

var counter = 0 

qt_button_onClicked(buttonA, nil, (ctx: Context) => (block: 
    counter = counter + 1
    echo " [INFO] ButtonA clicked => counter = " & counter.intToStr
    entry.qt_widget_setText "Counter changed to " & counter.intToStr
))

buttonB.qt_button_onClicked(nil, (ctx: Context) => (block: 
    counter = counter - 1    
    echo " [INFO] ButtonB clicked => counter = " & counter.intToStr
    entry.qt_widget_setText "Counter changed to " & counter.intToStr
))

buttonC.qt_button_onClicked(nil) do (ctx: Context):
    counter = 100
    qt_msgbox_info( nil
                  , "Button A notification"
                  , "[INFO] ButtonC => reset counter to = " & counter.intToStr)
    entry.qt_widget_setText " [BUTTON C] Counter changed to " & counter.intToStr
    echo " [BUTTON C] Counter changed to " & counter.intToStr

spinbox.qt_QDoubleSpinBox_onValueChanged(nil) do (ctx: Context) -> void:
    let value = spinbox.qt_QDoubleSpinBox_value
    # echo " [INFO] Spbinbox Value changed to ", value
    entry.qt_widget_setText "Spinbox value changed to = " &  $value

 #---------- Run QT applicaton ---------------------------#
 #

# Display root widget  
qt_widget_show(window)

# Run QT application and block current thread 
qt_app_exec2(app)

  # ---- Dispose QT objects -------------#
  # 
qt_qobject_del(window)
qt_qobject_del(app)

echo " [TRACE] Application terminated. Ok."

Copy the shared library to the project’s root directory:

 $ >> cp -v _build/libqtwrapper.so .
'_build/libqtwrapper.so' -> './libqtwrapper.so'

Compile and run in a single step:

$ nim c -r nim_app.nim

images/cwrapper-qt5-nim.png

Rust client code

File: rustclient.rs

use std::os;
use std::path::Path;

use std::os::raw::{c_int, c_char, c_long, c_void, c_uint, c_short};
use std::ffi::CString;
use std::mem;

type c_str  = *const c_char;

type LPVOID  = *mut c_void;
type QObject = LPVOID;
type QWidget = LPVOID;
type QLabel  = LPVOID;
type QApplication = LPVOID;
type QPushButton = LPVOID;

// void* null pointer
const null_ptr: LPVOID = std::ptr::null_mut();

const WIDGET_WINDOW:          c_int = 0;
const WIDGET_WINDOW_MAIN:     c_int = 2;
const WIDGET_QPUSH_BUTTON:    c_int = 3;
const WIDGET_QLABEL:          c_int = 4;
const WIDGET_QLINE_EDIT:      c_int = 5;
const WIDGET_QDOUBLE_SPINBOX: c_int = 6;
const LAYOUT_QFORM_LAYOUT:    c_int = 3;


// Function pointer type alias 
// type qt_object_del_t = unsafe extern "C" fn(this: QObject) -> c_void;

#[no_mangle]
extern "C" fn btn_callback1(this: QWidget, ctx: LPVOID) -> LPVOID 
{
    unsafe {
        let mut& counter = ctx as *mut c_int;
        *counter = *counter + 1;
        println!(" [TRACE] Button clicked Ok  => counter = {}", *counter);
    }
    return null_ptr;
}


fn main(){
    println!(" Hello world!");

    let qapp: QApplication = app_new(); 

    let button: QWidget = widget_new(null_ptr, WIDGET_QPUSH_BUTTON);
    widget_set_text(button, "Click me now!");

    unsafe{ qt_widget_show(button) };

    let mut counter: c_int = 0;

    unsafe{ qt_button_onClicked(button, counter as *mut _, btn_callback1); }

    // Run QT event loop and block current thread 
    app_exec(qapp);
}

// void (* callback) (void* ctx) )
type button_callback = unsafe extern "C" fn(this: QWidget, ctx: LPVOID) -> LPVOID;


#[link(name = "qtwrapper")]
extern "C" {

    fn qt_object_del(this: QObject) -> c_void;

    fn qt_app_new2() -> QApplication;
    fn qt_app_exec(this: QApplication) -> c_void;

    fn qt_widget_new(parent: QWidget, typ: c_int) -> QWidget;

    fn qt_widget_show(this: QWidget) -> c_void;

    fn qt_QPushButton_new(parent: QWidget, label: c_str) -> QPushButton;

    fn qt_msgbox_info(parent: QWidget, title: c_str, text: c_str) -> c_void;

        // void qt_widget_setText(QWidget* self, const char* text)
    fn qt_widget_setText(this: QWidget, text: c_str);

    // void  qt_button_onClicked(QAbstractButton* self, void* ctx, void (* callback) (void*) );

    fn qt_button_onClicked(this: QWidget, ctx: LPVOID, callback: button_callback) -> c_void;
    }

// -------- Safe wrappers to unsafe operations ------------//

fn app_new() -> QApplication
{
    let qapp: QApplication = unsafe { qt_app_new2() };
    return qapp;
}

fn app_exec(qapp: QApplication) 
{
    unsafe{ qt_app_exec(qapp) };
}

fn widget_set_text(this: QWidget, text: &str)
{
    let text = CString::new(text).unwrap(); 
    unsafe {
        qt_widget_setText(this, text.as_ptr() );
    }
}

fn widget_new(parent: LPVOID, typ: c_int) -> QWidget
{
    let wdg: QWidget = unsafe{ qt_widget_new(parent, typ) };
    return wdg;
}

Building:

$ rustc rustclient.rs -l qtwrapper -L_build/

Running:

$ ./rustclient 

Common Lisp client code

This common lisp client code, more specifically Stell Bank Common Lisp (SBCL), loads the shared library libqtwrapper.so by using CFFI foreign-function interface library.

Procedure for install SBCL on Fedora 32 and dependencies:

# 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)

File: 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)

Run Common Lisp Client Code:

 $ >> 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
 ... ... ... ... ...  

User interface screenshot:

images/cwrapper-qt5-4.png