原生代码与 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 应该如下所示
{
"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 库进行接口。
{
"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 中定义我们的头文件
#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。我们将把它分解成可管理的部分。我们将从一些包含项和基本设置开始。
基本设置和数据结构
#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 中已有代码的下方,添加以下内容
// 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;
}
在这里,我们
- 前向声明我们稍后将使用的辅助函数
- 在匿名命名空间中设置全局状态,包括
- 用于
add、update和delete待办事项操作的回调 - 用于线程管理的 GTK 主上下文和主循环指针
- 指向 GTK 线程本身的指针
- 一个用于存储我们待办事项的 vector
- 用于
这些全局变量用于跟踪应用程序状态,并允许我们代码的不同部分相互交互。线程管理变量(g_gtk_main_context、g_main_loop 和 g_gtk_thread)尤其重要,因为 GTK 需要在其自己的事件循环中运行。由于我们的代码将从 Node.js/Electron 的主线程调用,我们需要在一个单独的线程中运行 GTK,以避免阻塞 JavaScript 事件循环。这种分离确保了我们的原生 UI 保持响应,同时允许与 Electron 应用程序进行双向通信。回调使我们能够在用户与我们的原生 GTK 界面交互时将事件发送回 JavaScript。
辅助函数
接下来,我们将更多代码添加到我们已写代码的下方。在本节中,我们将添加三个静态辅助方法——并开始设置一些实际的原生用户界面。我们将添加一个用于通知回调的线程安全函数,一个用于更新行标签的函数,以及一个用于创建整个“添加待办事项”对话框的函数。
// 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 回调。
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 上下文。
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 应用、上下文和线程的精心设置使这一切变得简单明了。
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 之间的桥梁,实现了双向通信。
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 应该如下所示
#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 之间的桥接。让我们先创建一个基本的插件骨架
#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”——用下面的代码替换它。
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 回调函数的 maptsfn_:一个线程安全函数句柄(对于 GTK3 线程通信至关重要)
析构函数在对象被垃圾回收时正确清理线程安全函数。
实现基本功能 - HelloWorld
接下来,我们将添加我们的两个主要方法 HelloWorld() 和 HelloGui()。我们将将它们添加到我们的 private 作用域,就在我们有注释“Method implementations will go here”的地方。
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。用以下方法替换它
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。然后,用以下代码替换该部分
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 框架时至关重要,因为它们运行在自己的线程上。以下是它为何重要
- 线程安全:Electron 中的 JavaScript 运行在单个线程上(存在例外情况,但这通常是一个有用的规则)。没有线程安全函数,从另一个线程调用 JavaScript 会导致崩溃或竞态条件。
- 队列管理:它会自动排队函数调用,并在 JavaScript 线程上执行它们。
- 资源管理:它处理正确的引用计数,以确保对象在仍需要时不会被垃圾回收。
在我们的代码中,我们使用它来弥合 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 应该如下所示
#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 日期。
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 应用中,并在其中 import 或 require 它。
使用示例
构建完插件后,您就可以在您的 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()
当您运行此代码时
helloWorld()调用将返回来自 C++ 的问候- 当用户与 GTK3 GUI 交互时,将触发事件监听器
helloGui()调用将打开一个原生的 GTK3 窗口,包含- 用于待办事项的文本输入字段
- 用于选择日期的日历控件
- 一个“添加”按钮,用于创建新的待办事项
- 一个可滚动的列表,显示所有待办事项
- 用于编辑和删除待办事项的右键上下文菜单
与原生 GTK3 界面的所有交互都将触发相应的 JavaScript 事件,使您的 Electron 应用程序能够实时响应原生 GUI 操作。
结论
您现在已经使用 C++ 和 GTK3 为 Linux 构建了一个完整的原生 Node.js 插件。这个插件
- 在 JavaScript 和 C++ 之间提供了双向桥梁
- 创建了一个在自己线程中运行的原生 GTK3 GUI
- 实现了一个简单的待办事项应用程序,具有添加功能
- 使用 GTK3,它与 Electron 的 Chromium 运行时兼容
- 安全地处理从 C++ 到 JavaScript 的回调
这个基础可以扩展,以在您的 Electron 应用程序中实现更复杂的 Linux 特定功能。您可以访问系统功能,与 Linux 特定的库集成,或者在保持 Electron 提供的灵活性和易用性的同时创建高性能的原生 UI。有关 GTK3 开发的更多信息,请参阅GTK3 文档和GLib/GObject 文档。您还可以找到Node.js N-API 文档和node-addon-api 来帮助扩展您的原生插件。