跳转到主要内容

原生代码与 Electron:C++ (Linux)

本教程建立在原生代码与 Electron 的通用介绍的基础上,并专注于使用 C++ 和 GTK3 为 Linux 创建原生插件。为了说明如何将原生的 Linux 代码嵌入到您的 Electron 应用中,我们将构建一个基本的原生 GTK3 GUI,它将与 Electron 的 JavaScript 进行通信。

具体来说,我们将使用 GTK3 作为我们的 GUI 界面,它提供了

  • 一套全面的 UI 组件,如按钮、输入框和列表
  • 跨桌面兼容性,适用于各种 Linux 发行版
  • 与 Linux 桌面的原生主题和辅助功能集成
注意

我们之所以特别使用 GTK3,是因为 Chromium(以及由此衍生的 Electron)在内部使用的就是 GTK3。使用 GTK4 会导致运行时冲突,因为 GTK3 和 GTK4 都将被加载到同一个进程中。如果 Chromium 升级到 GTK4,您也很可能能够轻松地将您的原生代码升级到 GTK4。

本教程对于已经熟悉 Linux 上 GTK 开发的人员将最有帮助。您应该已经掌握了 GTK 的基本概念,如控件、信号和主事件循环。为了简洁起见,我们不会花费太多时间解释我们使用的每个 GTK 元素或为它们编写的代码。这使得本教程能够真正帮助那些已经了解 GTK 开发并希望将其技能应用于 Electron 的开发者——而无需同时查阅完整的 GTK 文档。

注意

如果您还不熟悉这些概念,GTK3 文档GTK3 教程是入门的绝佳资源。GNOME 开发者文档也为 GTK 开发提供了全面的指南。

要求

与我们关于原生代码与 Electron 的通用介绍一样,本教程假设您已安装 Node.js 和 npm,以及编译原生代码所需的基本工具。由于本教程讨论编写与 GTK3 交互的原生代码,您将需要

  • 安装了 GTK3 开发文件的 Linux 发行版
  • pkg-config 工具
  • G++ 编译器和构建工具

在 Ubuntu/Debian 上,您可以使用以下命令安装它们

sudo apt-get install build-essential pkg-config libgtk-3-dev

在 Fedora/RHEL/CentOS 上

sudo dnf install gcc-c++ pkgconfig gtk3-devel

1) 创建一个包

你可以重用我们在 原生代码和 Electron 教程中创建的包。本教程不会重复其中描述的步骤。让我们首先设置我们的基本插件文件夹结构。

cpp-linux/
├── binding.gyp # Configuration file for node-gyp to build the native addon
├── include/
│ └── cpp_code.h # Header file with declarations for our C++ native code
├── js/
│ └── index.js # JavaScript interface that loads and exposes our native addon
├── package.json # Node.js package configuration and dependencies
└── src/
├── cpp_addon.cc # C++ code that bridges Node.js/Electron with our native code
└── cpp_code.cc # Implementation of our native C++ functionality using GTK3

我们的 package.json 应该如下所示

package.json
{
"name": "cpp-linux",
"version": "1.0.0",
"description": "A demo module that exposes C++ code to Electron",
"main": "js/index.js",
"scripts": {
"clean": "rm -rf build",
"build-electron": "electron-rebuild",
"build": "node-gyp configure && node-gyp build"
},
"license": "MIT",
"dependencies": {
"node-addon-api": "^8.3.0",
"bindings": "^1.5.0"
}
}

2) 配置构建

对于使用 GTK3 的 Linux 特定插件,我们需要正确配置我们的 binding.gyp 文件,以确保我们的插件仅在 Linux 系统上编译——最好在其他平台上不做任何操作。这包括使用条件编译标志,利用 pkg-config 自动定位和包含用户系统上的 GTK3 库和头文件路径,并设置适当的编译器标志以启用异常处理和线程支持等功能。该配置将确保我们的原生代码能够正确地与 Node.js/Electron 运行时以及提供原生 GUI 功能的 GTK3 库进行接口。

binding.gyp
{
"targets": [
{
"target_name": "cpp_addon",
"conditions": [
['OS=="linux"', {
"sources": [
"src/cpp_addon.cc",
"src/cpp_code.cc"
],
"include_dirs": [
"<!@(node -p \"require('node-addon-api').include\")",
"include",
"<!@(pkg-config --cflags-only-I gtk+-3.0 | sed s/-I//g)"
],
"libraries": [
"<!@(pkg-config --libs gtk+-3.0)",
"-luuid"
],
"cflags": [
"-fexceptions",
"<!@(pkg-config --cflags gtk+-3.0)",
"-pthread"
],
"cflags_cc": [
"-fexceptions",
"<!@(pkg-config --cflags gtk+-3.0)",
"-pthread"
],
"ldflags": [
"-pthread"
],
"cflags!": ["-fno-exceptions"],
"cflags_cc!": ["-fno-exceptions"],
"defines": ["NODE_ADDON_API_CPP_EXCEPTIONS"],
"dependencies": [
"<!(node -p \"require('node-addon-api').gyp\")"
]
}]
]
}
]
}

让我们仔细看看这个配置的关键部分,从 pkg-config 集成开始。binding.gyp 文件中的 <!@ 语法是一个命令扩展运算符。它执行括号内的命令,并使用命令的输出作为该位置的值。因此, wherever you see <!@ with pkg-config inside, know that we're calling a pkg-config command and using the output as our value. sed 命令会从包含路径中剥离 -I 前缀,使其与 GYP 的格式兼容。

3) 定义 C++ 接口

让我们在 include/cpp_code.h 中定义我们的头文件

include/cpp_code.h
#pragma once
#include <string>
#include <functional>

namespace cpp_code {

std::string hello_world(const std::string& input);
void hello_gui();

// Callback function types
using TodoCallback = std::function<void(const std::string&)>;

// Callback setters
void setTodoAddedCallback(TodoCallback callback);
void setTodoUpdatedCallback(TodoCallback callback);
void setTodoDeletedCallback(TodoCallback callback);

} // namespace cpp_code

这个头文件定义了

  • 一个基本的 hello_world 函数
  • 一个 hello_gui 函数来创建 GTK3 GUI
  • 用于待办事项操作(添加、更新、删除)的回调类型
  • 回调的设置函数

4) 实现 GTK3 GUI 代码

现在,让我们在 src/cpp_code.cc 中实现我们的 GTK3 GUI。我们将把它分解成可管理的部分。我们将从一些包含项和基本设置开始。

基本设置和数据结构

src/cpp_code.cc
#include <gtk/gtk.h>
#include <string>
#include <functional>
#include <chrono>
#include <vector>
#include <uuid/uuid.h>
#include <ctime>
#include <thread>
#include <memory>

using TodoCallback = std::function<void(const std::string &)>;

namespace cpp_code
{
// Basic functions
std::string hello_world(const std::string &input)
{
return "Hello from C++! You said: " + input;
}

// Data structures
struct TodoItem
{
uuid_t id;
std::string text;
int64_t date;

std::string toJson() const
{
char uuid_str[37];
uuid_unparse(id, uuid_str);
return "{"
"\"id\":\"" +
std::string(uuid_str) + "\","
"\"text\":\"" +
text + "\","
"\"date\":" +
std::to_string(date) +
"}";
}

static std::string formatDate(int64_t timestamp)
{
char date_str[64];
time_t unix_time = timestamp / 1000;
strftime(date_str, sizeof(date_str), "%Y-%m-%d", localtime(&unix_time));
return date_str;
}
};

在本节中

  • 我们包含了 GTK3、标准库组件和 UUID 生成所需的头文件。
  • 定义一个 TodoCallback 类型来处理与 JavaScript 的通信。
  • 创建一个 TodoItem 结构来存储我们的待办事项数据,包含
    • 一个 UUID 用于唯一标识
    • 文本内容和时间戳
    • 一个转换为 JSON 以便发送到 JavaScript 的方法
    • 一个用于显示格式化日期的静态辅助函数

toJson() 方法尤其重要,因为它允许我们的 C++ 对象被序列化以传输到 JavaScript。可能有更好的方法来实现这一点,但本教程关注的是将 C++ 用于 Linux 原生 UI 开发与 Electron 结合,因此我们在此不追求更优的 JSON 序列化代码。有很多 C++ JSON 库可供选择,它们各有优缺点。有关列表,请参阅https://www.json.org/json-en.html

值得注意的是,我们还没有真正添加任何用户界面——我们将在下一步进行。GTK 代码通常比较冗长,所以请耐心等待——尽管它篇幅较长。

全局状态和前向声明

在您 src/cpp_code.cc 中已有代码的下方,添加以下内容

src/cpp_code.cc
  // Forward declarations
static void update_todo_row_label(GtkListBoxRow *row, const TodoItem &todo);
static GtkWidget *create_todo_dialog(GtkWindow *parent, const TodoItem *existing_todo);

// Global state
namespace
{
TodoCallback g_todoAddedCallback;
TodoCallback g_todoUpdatedCallback;
TodoCallback g_todoDeletedCallback;
GMainContext *g_gtk_main_context = nullptr;
GMainLoop *g_main_loop = nullptr;
std::thread *g_gtk_thread = nullptr;
std::vector<TodoItem> g_todos;
}

在这里,我们

  • 前向声明我们稍后将使用的辅助函数
  • 在匿名命名空间中设置全局状态,包括
    • 用于 addupdatedelete 待办事项操作的回调
    • 用于线程管理的 GTK 主上下文和主循环指针
    • 指向 GTK 线程本身的指针
    • 一个用于存储我们待办事项的 vector

这些全局变量用于跟踪应用程序状态,并允许我们代码的不同部分相互交互。线程管理变量(g_gtk_main_contextg_main_loopg_gtk_thread)尤其重要,因为 GTK 需要在其自己的事件循环中运行。由于我们的代码将从 Node.js/Electron 的主线程调用,我们需要在一个单独的线程中运行 GTK,以避免阻塞 JavaScript 事件循环。这种分离确保了我们的原生 UI 保持响应,同时允许与 Electron 应用程序进行双向通信。回调使我们能够在用户与我们的原生 GTK 界面交互时将事件发送回 JavaScript。

辅助函数

接下来,我们将更多代码添加到我们已写代码的下方。在本节中,我们将添加三个静态辅助方法——并开始设置一些实际的原生用户界面。我们将添加一个用于通知回调的线程安全函数,一个用于更新行标签的函数,以及一个用于创建整个“添加待办事项”对话框的函数。

src/cpp_code.cc
  // Helper functions
static void notify_callback(const TodoCallback &callback, const std::string &json)
{
if (callback && g_gtk_main_context)
{
g_main_context_invoke(g_gtk_main_context, [](gpointer data) -> gboolean
{
auto* cb_data = static_cast<std::pair<TodoCallback, std::string>*>(data);
cb_data->first(cb_data->second);
delete cb_data;
return G_SOURCE_REMOVE; }, new std::pair<TodoCallback, std::string>(callback, json));
}
}

static void update_todo_row_label(GtkListBoxRow *row, const TodoItem &todo)
{
auto *label = gtk_label_new((todo.text + " - " + TodoItem::formatDate(todo.date)).c_str());
auto *old_label = GTK_WIDGET(gtk_container_get_children(GTK_CONTAINER(row))->data);
gtk_container_remove(GTK_CONTAINER(row), old_label);
gtk_container_add(GTK_CONTAINER(row), label);
gtk_widget_show_all(GTK_WIDGET(row));
}

static GtkWidget *create_todo_dialog(GtkWindow *parent, const TodoItem *existing_todo = nullptr)
{
auto *dialog = gtk_dialog_new_with_buttons(
existing_todo ? "Edit Todo" : "Add Todo",
parent,
GTK_DIALOG_MODAL,
"_Cancel", GTK_RESPONSE_CANCEL,
"_Save", GTK_RESPONSE_ACCEPT,
nullptr);

auto *content_area = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
gtk_container_set_border_width(GTK_CONTAINER(content_area), 10);

auto *entry = gtk_entry_new();
if (existing_todo)
{
gtk_entry_set_text(GTK_ENTRY(entry), existing_todo->text.c_str());
}
gtk_container_add(GTK_CONTAINER(content_area), entry);

auto *calendar = gtk_calendar_new();
if (existing_todo)
{
time_t unix_time = existing_todo->date / 1000;
struct tm *timeinfo = localtime(&unix_time);
gtk_calendar_select_month(GTK_CALENDAR(calendar), timeinfo->tm_mon, timeinfo->tm_year + 1900);
gtk_calendar_select_day(GTK_CALENDAR(calendar), timeinfo->tm_mday);
}
gtk_container_add(GTK_CONTAINER(content_area), calendar);

gtk_widget_show_all(dialog);
return dialog;
}

这些辅助函数对我们的应用程序至关重要

  • notify_callback:使用 g_main_context_invoke 安全地从 GTK 线程调用 JavaScript 回调,该函数调度函数在 GTK 主上下文中执行。谨记,GTK 主上下文是执行 GTK 操作以确保线程安全的环境,因为 GTK 不是线程安全的,所有 UI 操作都必须在主线程上进行。
  • update_todo_row_label:使用新文本和格式化日期更新待办事项列表中的一行。
  • create_todo_dialog:创建一个用于添加或编辑待办事项的对话框,包含
    • 用于待办事项文本的文本输入字段
    • 一个用于选择日期的日历控件
    • 用于保存或取消的相应按钮

事件处理程序

我们的原生用户界面有事件——而这些事件必须被处理。这段代码中唯一特定于 Electron 的部分是我们正在通知我们的 JS 回调。

src/cpp_code.cc
  static void edit_action(GSimpleAction *action, GVariant *parameter, gpointer user_data)
{
auto *builder = static_cast<GtkBuilder *>(user_data);
auto *list = GTK_LIST_BOX(gtk_builder_get_object(builder, "todo_list"));
auto *row = gtk_list_box_get_selected_row(list);
if (!row)
return;

gint index = gtk_list_box_row_get_index(row);
auto size = static_cast<gint>(g_todos.size());
if (index < 0 || index >= size)
return;

auto *dialog = create_todo_dialog(
GTK_WINDOW(gtk_builder_get_object(builder, "window")),
&g_todos[index]);

if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT)
{
auto *entry = GTK_ENTRY(gtk_container_get_children(
GTK_CONTAINER(gtk_dialog_get_content_area(GTK_DIALOG(dialog))))
->data);
auto *calendar = GTK_CALENDAR(gtk_container_get_children(
GTK_CONTAINER(gtk_dialog_get_content_area(GTK_DIALOG(dialog))))
->next->data);

const char *new_text = gtk_entry_get_text(entry);

guint year, month, day;
gtk_calendar_get_date(calendar, &year, &month, &day);
GDateTime *datetime = g_date_time_new_local(year, month + 1, day, 0, 0, 0);
gint64 new_date = g_date_time_to_unix(datetime) * 1000;
g_date_time_unref(datetime);

g_todos[index].text = new_text;
g_todos[index].date = new_date;

update_todo_row_label(row, g_todos[index]);
notify_callback(g_todoUpdatedCallback, g_todos[index].toJson());
}

gtk_widget_destroy(dialog);
}

static void delete_action(GSimpleAction *action, GVariant *parameter, gpointer user_data)
{
auto *builder = static_cast<GtkBuilder *>(user_data);
auto *list = GTK_LIST_BOX(gtk_builder_get_object(builder, "todo_list"));
auto *row = gtk_list_box_get_selected_row(list);
if (!row)
return;

gint index = gtk_list_box_row_get_index(row);
auto size = static_cast<gint>(g_todos.size());
if (index < 0 || index >= size)
return;

std::string json = g_todos[index].toJson();
gtk_container_remove(GTK_CONTAINER(list), GTK_WIDGET(row));
g_todos.erase(g_todos.begin() + index);
notify_callback(g_todoDeletedCallback, json);
}

static void on_add_clicked(GtkButton *button, gpointer user_data)
{
auto *builder = static_cast<GtkBuilder *>(user_data);
auto *entry = GTK_ENTRY(gtk_builder_get_object(builder, "todo_entry"));
auto *calendar = GTK_CALENDAR(gtk_builder_get_object(builder, "todo_calendar"));
auto *list = GTK_LIST_BOX(gtk_builder_get_object(builder, "todo_list"));

const char *text = gtk_entry_get_text(entry);
if (strlen(text) > 0)
{
TodoItem todo;
uuid_generate(todo.id);
todo.text = text;

guint year, month, day;
gtk_calendar_get_date(calendar, &year, &month, &day);
GDateTime *datetime = g_date_time_new_local(year, month + 1, day, 0, 0, 0);
todo.date = g_date_time_to_unix(datetime) * 1000;
g_date_time_unref(datetime);

g_todos.push_back(todo);

auto *row = gtk_list_box_row_new();
auto *label = gtk_label_new((todo.text + " - " + TodoItem::formatDate(todo.date)).c_str());
gtk_container_add(GTK_CONTAINER(row), label);
gtk_container_add(GTK_CONTAINER(list), row);
gtk_widget_show_all(row);

gtk_entry_set_text(entry, "");

notify_callback(g_todoAddedCallback, todo.toJson());
}
}

static void on_row_activated(GtkListBox *list_box, GtkListBoxRow *row, gpointer user_data)
{
GMenu *menu = g_menu_new();
g_menu_append(menu, "Edit", "app.edit");
g_menu_append(menu, "Delete", "app.delete");

auto *popover = gtk_popover_new_from_model(GTK_WIDGET(row), G_MENU_MODEL(menu));
gtk_popover_set_position(GTK_POPOVER(popover), GTK_POS_RIGHT);
gtk_popover_popup(GTK_POPOVER(popover));

g_object_unref(menu);
}

这些事件处理程序管理用户交互

edit_action:通过以下方式处理编辑待办事项

  • 获取选定的行
  • 创建包含当前待办事项数据的对话框
  • 如果用户确认,则更新待办事项
  • 通过回调通知 JavaScript

delete_action:删除待办事项并通知 JavaScript。

on_add_clicked:当用户点击“添加”按钮时添加新的待办事项

  • 从输入字段获取文本和日期
  • 创建一个具有唯一 ID 的新 TodoItem
  • 将其添加到列表和底层数据存储中
  • 通知 JavaScript

on_row_activated:当点击待办事项时显示弹出菜单,提供编辑或删除选项。

GTK 应用设置

现在,我们需要设置我们的 GTK 应用程序。考虑到我们已经有一个正在运行的 GTK 应用程序,这可能有些反直觉。这里的激活代码是必需的,因为这是与 Electron 并行运行的原生 C++ 代码,而不是在 Electron 内部运行。虽然 Electron 确实有自己的主进程和渲染器进程,但这个 GTK 应用程序作为一个原生的操作系统窗口运行,它从 Electron 应用程序启动,但运行在自己的进程或线程中。hello_gui() 函数专门启动 GTK 应用程序,并拥有自己的线程(g_gtk_thread)、应用程序循环和 UI 上下文。

src/cpp_code.cc
  static gboolean init_gtk_app(gpointer user_data)
{
auto *app = static_cast<GtkApplication *>(user_data);
g_application_run(G_APPLICATION(app), 0, nullptr);
g_object_unref(app);
if (g_main_loop)
{
g_main_loop_quit(g_main_loop);
}
return G_SOURCE_REMOVE;
}

static void activate_handler(GtkApplication *app, gpointer user_data)
{
auto *builder = gtk_builder_new();

const GActionEntry app_actions[] = {
{"edit", edit_action, nullptr, nullptr, nullptr, {0, 0, 0}},
{"delete", delete_action, nullptr, nullptr, nullptr, {0, 0, 0}}};
g_action_map_add_action_entries(G_ACTION_MAP(app), app_actions,
G_N_ELEMENTS(app_actions), builder);

gtk_builder_add_from_string(builder,
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
"<interface>"
" <object class=\"GtkWindow\" id=\"window\">"
" <property name=\"title\">Todo List</property>"
" <property name=\"default-width\">400</property>"
" <property name=\"default-height\">500</property>"
" <child>"
" <object class=\"GtkBox\">"
" <property name=\"visible\">true</property>"
" <property name=\"orientation\">vertical</property>"
" <property name=\"spacing\">6</property>"
" <property name=\"margin\">12</property>"
" <child>"
" <object class=\"GtkBox\">"
" <property name=\"visible\">true</property>"
" <property name=\"spacing\">6</property>"
" <child>"
" <object class=\"GtkEntry\" id=\"todo_entry\">"
" <property name=\"visible\">true</property>"
" <property name=\"hexpand\">true</property>"
" <property name=\"placeholder-text\">Enter todo item...</property>"
" </object>"
" </child>"
" <child>"
" <object class=\"GtkCalendar\" id=\"todo_calendar\">"
" <property name=\"visible\">true</property>"
" </object>"
" </child>"
" <child>"
" <object class=\"GtkButton\" id=\"add_button\">"
" <property name=\"visible\">true</property>"
" <property name=\"label\">Add</property>"
" </object>"
" </child>"
" </object>"
" </child>"
" <child>"
" <object class=\"GtkScrolledWindow\">"
" <property name=\"visible\">true</property>"
" <property name=\"vexpand\">true</property>"
" <child>"
" <object class=\"GtkListBox\" id=\"todo_list\">"
" <property name=\"visible\">true</property>"
" <property name=\"selection-mode\">single</property>"
" </object>"
" </child>"
" </object>"
" </child>"
" </object>"
" </child>"
" </object>"
"</interface>",
-1, nullptr);

auto *window = GTK_WINDOW(gtk_builder_get_object(builder, "window"));
auto *button = GTK_BUTTON(gtk_builder_get_object(builder, "add_button"));
auto *list = GTK_LIST_BOX(gtk_builder_get_object(builder, "todo_list"));

gtk_window_set_application(window, app);

g_signal_connect(button, "clicked", G_CALLBACK(on_add_clicked), builder);
g_signal_connect(list, "row-activated", G_CALLBACK(on_row_activated), nullptr);

gtk_widget_show_all(GTK_WIDGET(window));
}

让我们仔细看看上面的代码

  • init_gtk_app:运行 GTK 应用程序的主循环。
  • activate_handler:在激活时设置应用程序 UI
    • 创建一个 GtkBuilder 用于加载 UI
    • 注册编辑和删除操作
    • 使用 GTK 的 XML 标记语言定义 UI 布局
    • 将信号连接到我们的事件处理程序

UI 布局使用 XML 内联定义,这是 GTK 应用程序中的一种常见模式。它创建了一个主窗口、输入控件(文本输入、日历和添加按钮)、一个用于显示待办事项的列表框,以及适当的布局容器和滚动条。

主 GUI 函数和线程管理

现在我们已经将所有内容连接起来,我们可以添加我们的两个核心 GUI 函数:hello_gui()(我们将从 JavaScript 调用它)和 cleanup_gui() 来清理一切。您可能会欣喜地发现,我们对 GTK 应用、上下文和线程的精心设置使这一切变得简单明了。

src/cpp_code.cc
  void hello_gui()
{
if (g_gtk_thread != nullptr)
{
g_print("GTK application is already running.\n");
return;
}

if (!gtk_init_check(0, nullptr))
{
g_print("Failed to initialize GTK.\n");
return;
}

g_gtk_main_context = g_main_context_new();
g_main_loop = g_main_loop_new(g_gtk_main_context, FALSE);

g_gtk_thread = new std::thread([]()
{
GtkApplication* app = gtk_application_new("com.example.todo", G_APPLICATION_NON_UNIQUE);
g_signal_connect(app, "activate", G_CALLBACK(activate_handler), nullptr);

g_idle_add_full(G_PRIORITY_DEFAULT, init_gtk_app, app, nullptr);

if (g_main_loop) {
g_main_loop_run(g_main_loop);
} });

g_gtk_thread->detach();
}

void cleanup_gui()
{
if (g_main_loop && g_main_loop_is_running(g_main_loop))
{
g_main_loop_quit(g_main_loop);
}

if (g_main_loop)
{
g_main_loop_unref(g_main_loop);
g_main_loop = nullptr;
}

if (g_gtk_main_context)
{
g_main_context_unref(g_gtk_main_context);
g_gtk_main_context = nullptr;
}

g_gtk_thread = nullptr;
}

这些函数管理 GTK 应用程序的生命周期

  • hello_gui:暴露给 JavaScript 的入口点,它检查 GTK 是否已在运行,初始化 GTK,创建一个新的主上下文和主循环,启动一个线程来运行 GTK 应用程序,并分离线程使其独立运行。
  • cleanup_gui:在应用程序关闭时正确清理 GTK 资源。

在单独的线程中运行 GTK 对于 Electron 集成至关重要,因为它防止 GTK 主循环阻塞 Node.js 的事件循环。

回调管理

之前,我们设置了全局变量来存储我们的回调。现在,我们将添加赋值这些回调的函数。这些回调构成了我们原生 GTK 代码和 JavaScript 之间的桥梁,实现了双向通信。

src/cpp_code.cc
  void setTodoAddedCallback(TodoCallback callback)
{
g_todoAddedCallback = callback;
}

void setTodoUpdatedCallback(TodoCallback callback)
{
g_todoUpdatedCallback = callback;
}

void setTodoDeletedCallback(TodoCallback callback)
{
g_todoDeletedCallback = callback;
}

整合 cpp_code.cc

我们现在已经完成了插件的 GTK 和原生部分——也就是说,最关心与操作系统交互的代码(相比之下,与连接原生 C++ 和 JavaScript 世界的关联较少)。添加完以上所有部分后,您的 src/cpp_code.cc 应该如下所示

src/cpp_code.cc
#include <gtk/gtk.h>
#include <string>
#include <functional>
#include <chrono>
#include <vector>
#include <uuid/uuid.h>
#include <ctime>
#include <thread>
#include <memory>

using TodoCallback = std::function<void(const std::string &)>;

namespace cpp_code
{

// Basic functions
std::string hello_world(const std::string &input)
{
return "Hello from C++! You said: " + input;
}

// Data structures
struct TodoItem
{
uuid_t id;
std::string text;
int64_t date;

std::string toJson() const
{
char uuid_str[37];
uuid_unparse(id, uuid_str);
return "{"
"\"id\":\"" +
std::string(uuid_str) + "\","
"\"text\":\"" +
text + "\","
"\"date\":" +
std::to_string(date) +
"}";
}

static std::string formatDate(int64_t timestamp)
{
char date_str[64];
time_t unix_time = timestamp / 1000;
strftime(date_str, sizeof(date_str), "%Y-%m-%d", localtime(&unix_time));
return date_str;
}
};

// Forward declarations
static void update_todo_row_label(GtkListBoxRow *row, const TodoItem &todo);
static GtkWidget *create_todo_dialog(GtkWindow *parent, const TodoItem *existing_todo);

// Global state
namespace
{
TodoCallback g_todoAddedCallback;
TodoCallback g_todoUpdatedCallback;
TodoCallback g_todoDeletedCallback;
GMainContext *g_gtk_main_context = nullptr;
GMainLoop *g_main_loop = nullptr;
std::thread *g_gtk_thread = nullptr;
std::vector<TodoItem> g_todos;
}

// Helper functions
static void notify_callback(const TodoCallback &callback, const std::string &json)
{
if (callback && g_gtk_main_context)
{
g_main_context_invoke(g_gtk_main_context, [](gpointer data) -> gboolean
{
auto* cb_data = static_cast<std::pair<TodoCallback, std::string>*>(data);
cb_data->first(cb_data->second);
delete cb_data;
return G_SOURCE_REMOVE; }, new std::pair<TodoCallback, std::string>(callback, json));
}
}

static void update_todo_row_label(GtkListBoxRow *row, const TodoItem &todo)
{
auto *label = gtk_label_new((todo.text + " - " + TodoItem::formatDate(todo.date)).c_str());
auto *old_label = GTK_WIDGET(gtk_container_get_children(GTK_CONTAINER(row))->data);
gtk_container_remove(GTK_CONTAINER(row), old_label);
gtk_container_add(GTK_CONTAINER(row), label);
gtk_widget_show_all(GTK_WIDGET(row));
}

static GtkWidget *create_todo_dialog(GtkWindow *parent, const TodoItem *existing_todo = nullptr)
{
auto *dialog = gtk_dialog_new_with_buttons(
existing_todo ? "Edit Todo" : "Add Todo",
parent,
GTK_DIALOG_MODAL,
"_Cancel", GTK_RESPONSE_CANCEL,
"_Save", GTK_RESPONSE_ACCEPT,
nullptr);

auto *content_area = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
gtk_container_set_border_width(GTK_CONTAINER(content_area), 10);

auto *entry = gtk_entry_new();
if (existing_todo)
{
gtk_entry_set_text(GTK_ENTRY(entry), existing_todo->text.c_str());
}
gtk_container_add(GTK_CONTAINER(content_area), entry);

auto *calendar = gtk_calendar_new();
if (existing_todo)
{
time_t unix_time = existing_todo->date / 1000;
struct tm *timeinfo = localtime(&unix_time);
gtk_calendar_select_month(GTK_CALENDAR(calendar), timeinfo->tm_mon, timeinfo->tm_year + 1900);
gtk_calendar_select_day(GTK_CALENDAR(calendar), timeinfo->tm_mday);
}
gtk_container_add(GTK_CONTAINER(content_area), calendar);

gtk_widget_show_all(dialog);
return dialog;
}

static void edit_action(GSimpleAction *action, GVariant *parameter, gpointer user_data)
{
auto *builder = static_cast<GtkBuilder *>(user_data);
auto *list = GTK_LIST_BOX(gtk_builder_get_object(builder, "todo_list"));
auto *row = gtk_list_box_get_selected_row(list);
if (!row)
return;

gint index = gtk_list_box_row_get_index(row);
auto size = static_cast<gint>(g_todos.size());
if (index < 0 || index >= size)
return;

auto *dialog = create_todo_dialog(
GTK_WINDOW(gtk_builder_get_object(builder, "window")),
&g_todos[index]);

if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT)
{
auto *entry = GTK_ENTRY(gtk_container_get_children(
GTK_CONTAINER(gtk_dialog_get_content_area(GTK_DIALOG(dialog))))
->data);
auto *calendar = GTK_CALENDAR(gtk_container_get_children(
GTK_CONTAINER(gtk_dialog_get_content_area(GTK_DIALOG(dialog))))
->next->data);

const char *new_text = gtk_entry_get_text(entry);

guint year, month, day;
gtk_calendar_get_date(calendar, &year, &month, &day);
GDateTime *datetime = g_date_time_new_local(year, month + 1, day, 0, 0, 0);
gint64 new_date = g_date_time_to_unix(datetime) * 1000;
g_date_time_unref(datetime);

g_todos[index].text = new_text;
g_todos[index].date = new_date;

update_todo_row_label(row, g_todos[index]);
notify_callback(g_todoUpdatedCallback, g_todos[index].toJson());
}

gtk_widget_destroy(dialog);
}

static void delete_action(GSimpleAction *action, GVariant *parameter, gpointer user_data)
{
auto *builder = static_cast<GtkBuilder *>(user_data);
auto *list = GTK_LIST_BOX(gtk_builder_get_object(builder, "todo_list"));
auto *row = gtk_list_box_get_selected_row(list);
if (!row)
return;

gint index = gtk_list_box_row_get_index(row);
auto size = static_cast<gint>(g_todos.size());
if (index < 0 || index >= size)
return;

std::string json = g_todos[index].toJson();
gtk_container_remove(GTK_CONTAINER(list), GTK_WIDGET(row));
g_todos.erase(g_todos.begin() + index);
notify_callback(g_todoDeletedCallback, json);
}

static void on_add_clicked(GtkButton *button, gpointer user_data)
{
auto *builder = static_cast<GtkBuilder *>(user_data);
auto *entry = GTK_ENTRY(gtk_builder_get_object(builder, "todo_entry"));
auto *calendar = GTK_CALENDAR(gtk_builder_get_object(builder, "todo_calendar"));
auto *list = GTK_LIST_BOX(gtk_builder_get_object(builder, "todo_list"));

const char *text = gtk_entry_get_text(entry);
if (strlen(text) > 0)
{
TodoItem todo;
uuid_generate(todo.id);
todo.text = text;

guint year, month, day;
gtk_calendar_get_date(calendar, &year, &month, &day);
GDateTime *datetime = g_date_time_new_local(year, month + 1, day, 0, 0, 0);
todo.date = g_date_time_to_unix(datetime) * 1000;
g_date_time_unref(datetime);

g_todos.push_back(todo);

auto *row = gtk_list_box_row_new();
auto *label = gtk_label_new((todo.text + " - " + TodoItem::formatDate(todo.date)).c_str());
gtk_container_add(GTK_CONTAINER(row), label);
gtk_container_add(GTK_CONTAINER(list), row);
gtk_widget_show_all(row);

gtk_entry_set_text(entry, "");

notify_callback(g_todoAddedCallback, todo.toJson());
}
}

static void on_row_activated(GtkListBox *list_box, GtkListBoxRow *row, gpointer user_data)
{
GMenu *menu = g_menu_new();
g_menu_append(menu, "Edit", "app.edit");
g_menu_append(menu, "Delete", "app.delete");

auto *popover = gtk_popover_new_from_model(GTK_WIDGET(row), G_MENU_MODEL(menu));
gtk_popover_set_position(GTK_POPOVER(popover), GTK_POS_RIGHT);
gtk_popover_popup(GTK_POPOVER(popover));

g_object_unref(menu);
}

static gboolean init_gtk_app(gpointer user_data)
{
auto *app = static_cast<GtkApplication *>(user_data);
g_application_run(G_APPLICATION(app), 0, nullptr);
g_object_unref(app);
if (g_main_loop)
{
g_main_loop_quit(g_main_loop);
}
return G_SOURCE_REMOVE;
}

static void activate_handler(GtkApplication *app, gpointer user_data)
{
auto *builder = gtk_builder_new();

const GActionEntry app_actions[] = {
{"edit", edit_action, nullptr, nullptr, nullptr, {0, 0, 0}},
{"delete", delete_action, nullptr, nullptr, nullptr, {0, 0, 0}}};
g_action_map_add_action_entries(G_ACTION_MAP(app), app_actions,
G_N_ELEMENTS(app_actions), builder);

gtk_builder_add_from_string(builder,
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
"<interface>"
" <object class=\"GtkWindow\" id=\"window\">"
" <property name=\"title\">Todo List</property>"
" <property name=\"default-width\">400</property>"
" <property name=\"default-height\">500</property>"
" <child>"
" <object class=\"GtkBox\">"
" <property name=\"visible\">true</property>"
" <property name=\"orientation\">vertical</property>"
" <property name=\"spacing\">6</property>"
" <property name=\"margin\">12</property>"
" <child>"
" <object class=\"GtkBox\">"
" <property name=\"visible\">true</property>"
" <property name=\"spacing\">6</property>"
" <child>"
" <object class=\"GtkEntry\" id=\"todo_entry\">"
" <property name=\"visible\">true</property>"
" <property name=\"hexpand\">true</property>"
" <property name=\"placeholder-text\">Enter todo item...</property>"
" </object>"
" </child>"
" <child>"
" <object class=\"GtkCalendar\" id=\"todo_calendar\">"
" <property name=\"visible\">true</property>"
" </object>"
" </child>"
" <child>"
" <object class=\"GtkButton\" id=\"add_button\">"
" <property name=\"visible\">true</property>"
" <property name=\"label\">Add</property>"
" </object>"
" </child>"
" </object>"
" </child>"
" <child>"
" <object class=\"GtkScrolledWindow\">"
" <property name=\"visible\">true</property>"
" <property name=\"vexpand\">true</property>"
" <child>"
" <object class=\"GtkListBox\" id=\"todo_list\">"
" <property name=\"visible\">true</property>"
" <property name=\"selection-mode\">single</property>"
" </object>"
" </child>"
" </object>"
" </child>"
" </object>"
" </child>"
" </object>"
"</interface>",
-1, nullptr);

auto *window = GTK_WINDOW(gtk_builder_get_object(builder, "window"));
auto *button = GTK_BUTTON(gtk_builder_get_object(builder, "add_button"));
auto *list = GTK_LIST_BOX(gtk_builder_get_object(builder, "todo_list"));

gtk_window_set_application(window, app);

g_signal_connect(button, "clicked", G_CALLBACK(on_add_clicked), builder);
g_signal_connect(list, "row-activated", G_CALLBACK(on_row_activated), nullptr);

gtk_widget_show_all(GTK_WIDGET(window));
}

void hello_gui()
{
if (g_gtk_thread != nullptr)
{
g_print("GTK application is already running.\n");
return;
}

if (!gtk_init_check(0, nullptr))
{
g_print("Failed to initialize GTK.\n");
return;
}

g_gtk_main_context = g_main_context_new();
g_main_loop = g_main_loop_new(g_gtk_main_context, FALSE);

g_gtk_thread = new std::thread([]()
{
GtkApplication* app = gtk_application_new("com.example.todo", G_APPLICATION_NON_UNIQUE);
g_signal_connect(app, "activate", G_CALLBACK(activate_handler), nullptr);

g_idle_add_full(G_PRIORITY_DEFAULT, init_gtk_app, app, nullptr);

if (g_main_loop) {
g_main_loop_run(g_main_loop);
} });

g_gtk_thread->detach();
}

void cleanup_gui()
{
if (g_main_loop && g_main_loop_is_running(g_main_loop))
{
g_main_loop_quit(g_main_loop);
}

if (g_main_loop)
{
g_main_loop_unref(g_main_loop);
g_main_loop = nullptr;
}

if (g_gtk_main_context)
{
g_main_context_unref(g_gtk_main_context);
g_gtk_main_context = nullptr;
}

g_gtk_thread = nullptr;
}

void setTodoAddedCallback(TodoCallback callback)
{
g_todoAddedCallback = callback;
}

void setTodoUpdatedCallback(TodoCallback callback)
{
g_todoUpdatedCallback = callback;
}

void setTodoDeletedCallback(TodoCallback callback)
{
g_todoDeletedCallback = callback;
}

} // namespace cpp_code

5) 创建 Node.js 插件桥接

现在让我们在 src/cpp_addon.cc 中实现我们的 C++ 代码和 Node.js 之间的桥接。让我们先创建一个基本的插件骨架

src/cpp_addon.cc
#include <napi.h>
#include <string>
#include "cpp_code.h"

// Class to wrap our C++ code will go here

Napi::Object Init(Napi::Env env, Napi::Object exports) {
// We'll add code here later
return exports;
}

NODE_API_MODULE(cpp_addon, Init)

这是使用 node-addon-api 的 Node.js 插件所需的最小结构。Init 函数在插件加载时被调用,NODE_API_MODULE 宏注册我们的初始化器。这个基本骨架目前还没有做什么,但它为 Node.js 加载我们的原生代码提供了入口点。

创建一个类来包装我们的 C++ 代码

让我们创建一个类来包装我们的 C++ 代码并将其暴露给 JavaScript。在之前的步骤中,我们添加了一个注释“Class to wrap our C++ code will go here”——用下面的代码替换它。

src/cpp_addon.cc
class CppAddon : public Napi::ObjectWrap<CppAddon>
{
public:
static Napi::Object Init(Napi::Env env, Napi::Object exports)
{
Napi::Function func = DefineClass(env, "CppLinuxAddon", {
InstanceMethod("helloWorld", &CppAddon::HelloWorld),
InstanceMethod("helloGui", &CppAddon::HelloGui),
InstanceMethod("on", &CppAddon::On)
});

Napi::FunctionReference *constructor = new Napi::FunctionReference();
*constructor = Napi::Persistent(func);
env.SetInstanceData(constructor);

exports.Set("CppLinuxAddon", func);
return exports;
}

CppAddon(const Napi::CallbackInfo &info)
: Napi::ObjectWrap<CppAddon>(info),
env_(info.Env()),
emitter(Napi::Persistent(Napi::Object::New(info.Env()))),
callbacks(Napi::Persistent(Napi::Object::New(info.Env()))),
tsfn_(nullptr)
{
// We'll implement the constructor together with a callback struct later
}

~CppAddon()
{
if (tsfn_ != nullptr)
{
napi_release_threadsafe_function(tsfn_, napi_tsfn_release);
tsfn_ = nullptr;
}
}

private:
Napi::Env env_;
Napi::ObjectReference emitter;
Napi::ObjectReference callbacks;
napi_threadsafe_function tsfn_;

// Method implementations will go here
};

在这里,我们创建了一个继承自 Napi::ObjectWrap<CppAddon> 的 C++ 类

static Napi::Object Init 定义了我们的 JavaScript 接口,包含三个方法

  • helloWorld:一个简单的函数,用于测试桥接
  • helloGui:启动我们的 GTK3 UI 的函数
  • on:一个用于注册事件回调的方法

构造函数初始化

  • emitter:一个将向 JavaScript 发射事件的对象
  • callbacks:一个已注册 JavaScript 回调函数的 map
  • tsfn_:一个线程安全函数句柄(对于 GTK3 线程通信至关重要)

析构函数在对象被垃圾回收时正确清理线程安全函数。

实现基本功能 - HelloWorld

接下来,我们将添加我们的两个主要方法 HelloWorld()HelloGui()。我们将将它们添加到我们的 private 作用域,就在我们有注释“Method implementations will go here”的地方。

src/cpp_addon.cc
Napi::Value HelloWorld(const Napi::CallbackInfo &info)
{
Napi::Env env = info.Env();

if (info.Length() < 1 || !info[0].IsString())
{
Napi::TypeError::New(env, "Expected string argument").ThrowAsJavaScriptException();
return env.Null();
}

std::string input = info[0].As<Napi::String>();
std::string result = cpp_code::hello_world(input);

return Napi::String::New(env, result);
}

void HelloGui(const Napi::CallbackInfo &info)
{
cpp_code::hello_gui();
}

// On() method implementation will go here

HelloWorld():

  • 验证输入参数(必须是字符串)
  • 调用我们的 C++ hello_world 函数
  • 将结果作为 JavaScript 字符串返回

HelloGui():

  • 仅调用我们的 C++ hello_gui 函数,不带参数
  • 不返回任何内容(void),因为该函数仅启动 UI
  • 这些方法构成了 JavaScript 调用和我们的原生 C++ 函数之间的直接桥梁。

您可能会想 Napi::CallbackInfo 是什么,或者它从哪里来。这是 Node-API (N-API) C++ 包装器提供的一个类,特别是来自 node-addon-api 包。它封装了关于 JavaScript 函数调用的所有信息,包括

  • 从 JavaScript 传递的参数
  • JavaScript 执行环境(通过 info.Env()
  • 函数调用的 this
  • 参数的数量(通过 info.Length()

这个类是 Node.js 原生插件开发的基础,因为它充当了 JavaScript 函数调用和 C++ 方法实现之间的桥梁。每个可以从 JavaScript 调用的原生方法都会收到一个 CallbackInfo 对象作为其参数,允许 C++ 代码在处理之前访问和验证 JavaScript 参数。您可以看到我们在 HelloWorld() 中使用它来获取函数参数以及关于函数调用的其他信息。我们的 HelloGui() 函数不使用它,但如果使用,它也将遵循相同的模式。

设置事件系统

现在我们将处理原生开发中最棘手的部分:设置事件系统。之前,我们在 cpp_code.cc 代码中添加了原生回调——在 cpp_addon.cc 的桥接代码中,我们需要找到一种方法,让这些回调最终触发一个 JavaScript 方法。

让我们从 On() 方法开始,它将从 JavaScript 调用。在我们之前编写的代码中,您会找到一个注释,写着 On() method implementation will go here。用以下方法替换它

src/cpp_addon.cc
Napi::Value On(const Napi::CallbackInfo &info)
{
Napi::Env env = info.Env();

if (info.Length() < 2 || !info[0].IsString() || !info[1].IsFunction())
{
Napi::TypeError::New(env, "Expected (string, function) arguments").ThrowAsJavaScriptException();
return env.Undefined();
}

callbacks.Value().Set(info[0].As<Napi::String>(), info[1].As<Napi::Function>());
return env.Undefined();
}

这个方法允许 JavaScript 为不同的事件类型注册回调,并将 JavaScript 函数存储在我们的 callbacks map 中以备将来使用。到目前为止,一切顺利——但现在我们需要让 cpp_code.cc 知道这些回调。我们还需要找到一种协调线程的方法,因为实际的 cpp_code.cc 将在自己的线程上执行大部分工作。

在我们的代码中,找到声明构造函数 CppAddon(const Napi::CallbackInfo &info) 的部分,您会在 public 部分找到它。它应该有一个注释,写着 We'll implement the constructor together with a callback struct later。然后,用以下代码替换该部分

src/cpp_addon.cc
  struct CallbackData
{
std::string eventType;
std::string payload;
CppAddon *addon;
};

CppAddon(const Napi::CallbackInfo &info)
: Napi::ObjectWrap<CppAddon>(info),
env_(info.Env()),
emitter(Napi::Persistent(Napi::Object::New(info.Env()))),
callbacks(Napi::Persistent(Napi::Object::New(info.Env()))),
tsfn_(nullptr)
{
napi_status status = napi_create_threadsafe_function(
env_,
nullptr,
nullptr,
Napi::String::New(env_, "CppCallback"),
0,
1,
nullptr,
nullptr,
this,
[](napi_env env, napi_value js_callback, void *context, void *data)
{
auto *callbackData = static_cast<CallbackData *>(data);
if (!callbackData)
return;

Napi::Env napi_env(env);
Napi::HandleScope scope(napi_env);

auto addon = static_cast<CppAddon *>(context);
if (!addon)
{
delete callbackData;
return;
}

try
{
auto callback = addon->callbacks.Value().Get(callbackData->eventType).As<Napi::Function>();
if (callback.IsFunction())
{
callback.Call(addon->emitter.Value(), {Napi::String::New(napi_env, callbackData->payload)});
}
}
catch (...)
{
}

delete callbackData;
},
&tsfn_);

if (status != napi_ok)
{
Napi::Error::New(env_, "Failed to create threadsafe function").ThrowAsJavaScriptException();
return;
}

// Set up the callbacks here
auto makeCallback = [this](const std::string &eventType)
{
return [this, eventType](const std::string &payload)
{
if (tsfn_ != nullptr)
{
auto *data = new CallbackData{
eventType,
payload,
this};
napi_call_threadsafe_function(tsfn_, data, napi_tsfn_blocking);
}
};
};

cpp_code::setTodoAddedCallback(makeCallback("todoAdded"));
cpp_code::setTodoUpdatedCallback(makeCallback("todoUpdated"));
cpp_code::setTodoDeletedCallback(makeCallback("todoDeleted"));
}

这是我们桥接中最复杂的部分:实现双向通信。这里有几点值得注意,让我们一步一步来

CallbackData 结构

  • 持有事件类型、JSON 载荷和我们插件的引用。

在构造函数中

  • 我们创建一个线程安全函数(napi_create_threadsafe_function),这对于从 GTK3 线程调用 JavaScript 至关重要
  • 线程安全函数回调解包数据并调用相应的 JavaScript 回调
  • 我们创建一个 lambda makeCallback,它为不同的事件类型生成回调函数
  • 我们使用设置器函数将这些回调注册到我们的 C++ 代码中

我们来谈谈 napi_create_threadsafe_function。不同线程的协调可能是原生插件开发中最困难的部分——在我们看来,也是开发者最容易放弃的地方。napi_create_threadsafe_function 由 N-API 提供,允许您从任何线程安全地调用 JavaScript 函数。这在使用像 GTK3 这样的 GUI 框架时至关重要,因为它们运行在自己的线程上。以下是它为何重要

  1. 线程安全:Electron 中的 JavaScript 运行在单个线程上(存在例外情况,但这通常是一个有用的规则)。没有线程安全函数,从另一个线程调用 JavaScript 会导致崩溃或竞态条件。
  2. 队列管理:它会自动排队函数调用,并在 JavaScript 线程上执行它们。
  3. 资源管理:它处理正确的引用计数,以确保对象在仍需要时不会被垃圾回收。

在我们的代码中,我们使用它来弥合 GTK3 事件循环和 Node.js 事件循环之间的差距,允许来自 GUI 的事件安全地触发 JavaScript 回调。

对于想要了解更多的开发者,您可以参考官方 N-API 文档以获取关于线程安全函数的详细信息,node-addon-api 包装器文档以了解 C++ 包装器实现,以及Node.js 线程模型文章以理解 Node.js 如何处理并发以及为什么需要线程安全函数。

整合 cpp_addon.cc

我们现在已经完成了插件的桥接部分——也就是说,最关心作为 JavaScript 和 C++ 代码之间桥梁的代码(相比之下,与实际与操作系统或 GTK 交互的关联较少)。添加完以上所有部分后,您的 src/cpp_addon.cc 应该如下所示

src/cpp_addon.cc
#include <napi.h>
#include <string>
#include "cpp_code.h"

class CppAddon : public Napi::ObjectWrap<CppAddon>
{
public:
static Napi::Object Init(Napi::Env env, Napi::Object exports)
{
Napi::Function func = DefineClass(env, "CppLinuxAddon", {
InstanceMethod("helloWorld", &CppAddon::HelloWorld),
InstanceMethod("helloGui", &CppAddon::HelloGui),
InstanceMethod("on", &CppAddon::On)
});

Napi::FunctionReference *constructor = new Napi::FunctionReference();
*constructor = Napi::Persistent(func);
env.SetInstanceData(constructor);

exports.Set("CppLinuxAddon", func);
return exports;
}

struct CallbackData
{
std::string eventType;
std::string payload;
CppAddon *addon;
};

CppAddon(const Napi::CallbackInfo &info)
: Napi::ObjectWrap<CppAddon>(info),
env_(info.Env()),
emitter(Napi::Persistent(Napi::Object::New(info.Env()))),
callbacks(Napi::Persistent(Napi::Object::New(info.Env()))),
tsfn_(nullptr)
{
napi_status status = napi_create_threadsafe_function(
env_,
nullptr,
nullptr,
Napi::String::New(env_, "CppCallback"),
0,
1,
nullptr,
nullptr,
this,
[](napi_env env, napi_value js_callback, void *context, void *data)
{
auto *callbackData = static_cast<CallbackData *>(data);
if (!callbackData)
return;

Napi::Env napi_env(env);
Napi::HandleScope scope(napi_env);

auto addon = static_cast<CppAddon *>(context);
if (!addon)
{
delete callbackData;
return;
}

try
{
auto callback = addon->callbacks.Value().Get(callbackData->eventType).As<Napi::Function>();
if (callback.IsFunction())
{
callback.Call(addon->emitter.Value(), {Napi::String::New(napi_env, callbackData->payload)});
}
}
catch (...)
{
}

delete callbackData;
},
&tsfn_);

if (status != napi_ok)
{
Napi::Error::New(env_, "Failed to create threadsafe function").ThrowAsJavaScriptException();
return;
}

// Set up the callbacks here
auto makeCallback = [this](const std::string &eventType)
{
return [this, eventType](const std::string &payload)
{
if (tsfn_ != nullptr)
{
auto *data = new CallbackData{
eventType,
payload,
this};
napi_call_threadsafe_function(tsfn_, data, napi_tsfn_blocking);
}
};
};

cpp_code::setTodoAddedCallback(makeCallback("todoAdded"));
cpp_code::setTodoUpdatedCallback(makeCallback("todoUpdated"));
cpp_code::setTodoDeletedCallback(makeCallback("todoDeleted"));
}

~CppAddon()
{
if (tsfn_ != nullptr)
{
napi_release_threadsafe_function(tsfn_, napi_tsfn_release);
tsfn_ = nullptr;
}
}

private:
Napi::Env env_;
Napi::ObjectReference emitter;
Napi::ObjectReference callbacks;
napi_threadsafe_function tsfn_;

Napi::Value HelloWorld(const Napi::CallbackInfo &info)
{
Napi::Env env = info.Env();

if (info.Length() < 1 || !info[0].IsString())
{
Napi::TypeError::New(env, "Expected string argument").ThrowAsJavaScriptException();
return env.Null();
}

std::string input = info[0].As<Napi::String>();
std::string result = cpp_code::hello_world(input);

return Napi::String::New(env, result);
}

void HelloGui(const Napi::CallbackInfo &info)
{
cpp_code::hello_gui();
}

Napi::Value On(const Napi::CallbackInfo &info)
{
Napi::Env env = info.Env();

if (info.Length() < 2 || !info[0].IsString() || !info[1].IsFunction())
{
Napi::TypeError::New(env, "Expected (string, function) arguments").ThrowAsJavaScriptException();
return env.Undefined();
}

callbacks.Value().Set(info[0].As<Napi::String>(), info[1].As<Napi::Function>());
return env.Undefined();
}
};

Napi::Object Init(Napi::Env env, Napi::Object exports)
{
return CppAddon::Init(env, exports);
}

NODE_API_MODULE(cpp_addon, Init)

6) 创建 JavaScript 包装器

最后,让我们在 js/index.js 中添加一个 JavaScript 包装器。我们都可以看到,C++ 需要大量的样板代码,而这些代码可能更容易或更快地用 JavaScript 编写——您会发现许多生产应用程序最终会在调用原生代码之前在 JavaScript 中转换数据或请求。例如,我们将时间戳转换为正确的 JavaScript 日期。

js/index.js
const EventEmitter = require('events');

class CppLinuxAddon extends EventEmitter {
constructor() {
super()

if (process.platform !== 'linux') {
throw new Error('This module is only available on Linux');
}

const native = require('bindings')('cpp_addon')
this.addon = new native.CppLinuxAddon()

// Set up event forwarding
this.addon.on('todoAdded', (payload) => {
this.emit('todoAdded', this.parse(payload))
});

this.addon.on('todoUpdated', (payload) => {
this.emit('todoUpdated', this.parse(payload))
})

this.addon.on('todoDeleted', (payload) => {
this.emit('todoDeleted', this.parse(payload))
})
}

helloWorld(input = "") {
return this.addon.helloWorld(input)
}

helloGui() {
return this.addon.helloGui()
}

// Parse JSON and convert date to JavaScript Date object
parse(payload) {
const parsed = JSON.parse(payload)

return { ...parsed, date: new Date(parsed.date) }
}
}

if (process.platform === 'linux') {
module.exports = new CppLinuxAddon()
} else {
// Return empty object on non-Linux platforms
module.exports = {}
}

这个包装器

  • 继承 EventEmitter 以实现原生事件处理
  • 仅在 Linux 平台加载
  • 将事件从 C++ 转发到 JavaScript
  • 提供干净的方法来调用 C++
  • 将 JSON 数据转换为正确的 JavaScript 对象

7) 构建和测试插件

在所有文件都就位后,你可以构建插件。

npm run build

如果构建成功,您现在可以将插件添加到您的 Electron 应用中,并在其中 importrequire 它。

使用示例

构建完插件后,您就可以在您的 Electron 应用程序中使用它了。这是一个完整的示例

// In your Electron main process or renderer process
import cppLinux from 'cpp-linux'

// Test the basic functionality
console.log(cppLinux.helloWorld('Hi!'))
// Output: "Hello from C++! You said: Hi!"

// Set up event listeners for GTK GUI interactions
cppLinux.on('todoAdded', (todo) => {
console.log('New todo added:', todo)
// todo: { id: "uuid-string", text: "Todo text", date: Date object }
})

cppLinux.on('todoUpdated', (todo) => {
console.log('Todo updated:', todo)
})

cppLinux.on('todoDeleted', (todo) => {
console.log('Todo deleted:', todo)
})

// Launch the native GTK GUI
cppLinux.helloGui()

当您运行此代码时

  1. helloWorld() 调用将返回来自 C++ 的问候
  2. 当用户与 GTK3 GUI 交互时,将触发事件监听器
  3. helloGui() 调用将打开一个原生的 GTK3 窗口,包含
    • 用于待办事项的文本输入字段
    • 用于选择日期的日历控件
    • 一个“添加”按钮,用于创建新的待办事项
    • 一个可滚动的列表,显示所有待办事项
    • 用于编辑和删除待办事项的右键上下文菜单

与原生 GTK3 界面的所有交互都将触发相应的 JavaScript 事件,使您的 Electron 应用程序能够实时响应原生 GUI 操作。

结论

您现在已经使用 C++ 和 GTK3 为 Linux 构建了一个完整的原生 Node.js 插件。这个插件

  1. 在 JavaScript 和 C++ 之间提供了双向桥梁
  2. 创建了一个在自己线程中运行的原生 GTK3 GUI
  3. 实现了一个简单的待办事项应用程序,具有添加功能
  4. 使用 GTK3,它与 Electron 的 Chromium 运行时兼容
  5. 安全地处理从 C++ 到 JavaScript 的回调

这个基础可以扩展,以在您的 Electron 应用程序中实现更复杂的 Linux 特定功能。您可以访问系统功能,与 Linux 特定的库集成,或者在保持 Electron 提供的灵活性和易用性的同时创建高性能的原生 UI。有关 GTK3 开发的更多信息,请参阅GTK3 文档GLib/GObject 文档。您还可以找到Node.js N-API 文档node-addon-api 来帮助扩展您的原生插件。