跳转到主要内容

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

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

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

  • 一套全面的 UI 小部件,如按钮、输入字段和列表
  • 跨桌面兼容性,支持各种 Linux 发行版
  • 与 Linux 桌面的原生主题和辅助功能集成
注意

我们特意使用 GTK3,因为它就是 Chromium(以及 Electron)内部使用的。使用 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 文件中的 <!@ 语法是命令展开运算符。它执行括号内的命令,并使用命令的输出作为该位置的值。因此,无论何时看到带有 pkg-config<!@,都知道我们正在调用一个 pkg-config 命令,并将输出用作我们的值。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
  • 用于待办事项操作(添加、更新、删除)的回调类型
  • 回调的 setter 函数

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 线程本身的指针
    • 一个用于存储我们的待办事项的向量

这些全局变量用于跟踪应用程序状态,并允许我们代码的不同部分相互交互。线程管理变量(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() 函数专门使用自己的线程(g_gtk_thread)、应用程序循环和 UI 上下文启动 GTK 应用程序。

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。在我们上一步中,我们添加了一个注释“包装 C++ 代码的类将在此处”,请用下面的代码替换它。

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 回调函数映射
  • tsfn_:一个线程安全函数句柄(对 GTK3 线程通信至关重要)

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

实现基本功能 - HelloWorld

接下来,我们将添加两个主要方法,HelloWorld()HelloGui()。我们将它们添加到我们的 private 作用域,就在我们有注释“方法实现将在此处”的地方。

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() 方法实现将在此处。请用以下方法替换它

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 映射中以供将来使用。到目前为止一切顺利——但现在我们需要让 cpp_code.cc 知道这些回调。我们还需要找到一种方法来协调我们的线程,因为实际的 cpp_code.cc 将在自己的线程上进行大部分工作。

在我们的代码中,找到声明构造函数 CppAddon(const Napi::CallbackInfo &info) 的部分,你会在 public 部分找到它。它应该有一个注释写着“我们将一起实现构造函数和一个回调结构”。然后,用以下代码替换该部分

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,它为不同的事件类型生成回调函数
  • 我们使用 setter 函数将这些回调注册到我们的 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 操作。

结论

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

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

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