Skip to main content

本地代码与 Electron:C++(Linux)

🌐 Native Code and Electron: C++ (Linux)

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

🌐 This tutorial builds on the general introduction to Native Code and Electron and focuses on creating a native addon for Linux using C++ and GTK3. To illustrate how you can embed native Linux code in your Electron app, we'll be building a basic native GTK3 GUI that communicates with Electron's JavaScript.

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

🌐 Specifically, we'll be using GTK3 for our GUI interface, which provides:

  • 一套全面的 UI 小部件,包括按钮、输入字段和列表
  • 跨不同 Linux 发行版的跨桌面兼容性
  • 与 Linux 桌面原生主题和辅助功能集成
note

我们特意使用 GTK3,因为 Chromium(以及由此延伸的 Electron)内部就是使用它的。使用 GTK4 会导致运行时冲突,因为同一个进程中会同时加载 GTK3 和 GTK4。如果 Chromium 升级到 GTK4,你也很可能能轻松将本地代码升级到 GTK4。

本教程最适合那些已经对 Linux 上的 GTK 开发有一定了解的人。你应该对 GTK 的基本概念有所经验,比如控件(widgets)、信号(signals)以及主事件循环(main event loop)。为了简明起见,我们不会花太多时间解释我们使用的各个 GTK 元素或为它们编写的代码。这使得本教程对已经掌握 GTK 开发并希望将技能应用到 Electron 的人非常有帮助,而无需再去完整阅读 GTK 文档。

🌐 This tutorial will be most useful to those who already have some familiarity with GTK development on Linux. You should have experience with basic GTK concepts like widgets, signals, and the main event loop. In the interest of brevity, we're not spending too much time explaining the individual GTK elements we're using or the code we're writing for them. This allows this tutorial to be really helpful for those who already know GTK development and want to use their skills with Electron - without having to also be an entire GTK documentation.

note

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

要求

🌐 Requirements

就像我们对本地代码和 Electron 的一般介绍一样,本教程假设你已经安装了 Node.js 和 npm,以及编译本地代码所需的基本工具。由于本教程讨论的是与 GTK3 交互的本地代码编写,你将需要:

🌐 Just like our general introduction to Native Code and Electron, this tutorial assumes you have Node.js and npm installed, as well as the basic tools necessary for compiling native code. Since this tutorial discusses writing native code that interacts with GTK3, you'll need:

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

在 Ubuntu/Debian 上,你可以使用以下命令安装:

🌐 On Ubuntu/Debian, you can install these with:

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

在 Fedora/RHEL/CentOS 上:

🌐 On Fedora/RHEL/CentOS:

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

1) 创建一个软件包

🌐 1) Creating a package

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

🌐 You can re-use the package we created in our Native Code and Electron tutorial. This tutorial will not be repeating the steps described there. Let's first setup our basic addon folder structure:

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 应该如下所示:

🌐 Our package.json should look like this:

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) 设置构建配置

🌐 2) Setting up the build configuration

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

🌐 For a Linux-specific addon using GTK3, we need to configure our binding.gyp file correctly to ensure our addon is only compiled on Linux systems - doing ideally nothing on other platforms. This involves using conditional compilation flags, leveraging pkg-config to automatically locate and include the GTK3 libraries and header paths on the user's system, and setting appropriate compiler flags to enable features like exception handling and threading support. The configuration will ensure that our native code can properly interface with both the Node.js/Electron runtime and the GTK3 libraries that provide the native GUI capabilities.

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 的格式兼容。

🌐 Let's examine the key parts of this configuration, starting with the pkg-config integration. The <!@ syntax in a binding.gyp file is a command expansion operator. It executes the command inside the parentheses and uses the command's output as the value at that position. So, wherever you see <!@ with pkg-config inside, know that we're calling a pkg-config command and using the output as our value. The sed command strips the -I prefix from the include paths to make them compatible with GYP's format.

3) 定义 C++ 接口

🌐 3) Defining the C++ interface

让我们在 include/cpp_code.h 中定义我们的标题:

🌐 Let's define our header in 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

此标头定义:

🌐 This header defines:

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

4) 实现 GTK3 图形用户界面代码

🌐 4) Implementing GTK3 GUI Code

现在,让我们在 src/cpp_code.cc 中实现我们的 GTK3 图形界面。我们将把它分成可管理的部分。我们将从一些头文件包含以及基本设置开始。

🌐 Now, let's implement our GTK3 GUI in src/cpp_code.cc. We'll break this into manageable sections. We'll start with a number of includes as well as the basic setup.

基本设置和数据结构

🌐 Basic Setup and Data Structures

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

本节内容:

🌐 In this section:

  • 我们包含了 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 获取列表。

🌐 The toJson() method is particularly important as it's what allows our C++ objects to be serialized for transmission to JavaScript. There are probably better ways to do that, but this tutorial is about combining C++ for native Linux UI development with Electron, so we'll give ourselves a pass for not writing better JSON serialization code here. There are many libraries to work with JSON in C++ with different trade-offs. See https://www.json.org/json-en.html for a list.

值得注意的是,我们实际上还没有添加任何用户界面——这将在下一步中进行。GTK 代码通常比较冗长,所以请耐心——尽管内容比较多。

🌐 Notably, we haven't actually added any user interface yet - which we'll do in the next step. GTK code tends to be verbose, so bear with us - despite the length.

全局状态和转发声明

🌐 Global state and forward declarations

在你 src/cpp_code.cc 中已有的代码下面,添加以下内容:

🌐 Below the code already in your src/cpp_code.cc, add the following:

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

这里我们:

🌐 Here we:

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

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

🌐 These global variables keep track of application state and allow different parts of our code to interact with each other. The thread management variables (g_gtk_main_context, g_main_loop, and g_gtk_thread) are particularly important because GTK requires running in its own event loop. Since our code will be called from Node.js/Electron's main thread, we need to run GTK in a separate thread to avoid blocking the JavaScript event loop. This separation ensures that our native UI remains responsive while still allowing bidirectional communication with the Electron application. The callbacks enable us to send events back to JavaScript when the user interacts with our native GTK interface.

辅助函数

🌐 Helper Functions

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

🌐 Moving on, we're adding more code below the code we've already written. In this section, we're adding three static helper methods - and also start setting up some actual native user interface. We'll add a helper function that'll notify a callback in a thread-safe way, a function to update a row label, and a function to create the whole "Add Todo" dialog.

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

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

🌐 These helper functions are crucial for our application:

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

事件处理程序

🌐 Event handlers

我们的本地用户界面有事件——这些事件必须被处理。这个代码中唯一特定于 Electron 的地方是我们在通知我们的 JS 回调。

🌐 Our native user interface has events - and those events must be handled. The only Electron-specific thing in this code is that we're notifying our JS callbacks.

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

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

🌐 These event handlers manage user interactions:

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

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

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

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

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

on_row_activated:点击待办事项时显示弹出菜单,其中包含编辑或删除选项。

GTK 应用设置

🌐 GTK application setup

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

🌐 Now, we'll need to setup our GTK application. This might be counter-intuitive, given that we already have a GTK application running. The activation code here is necessary because this is native C++ code running alongside Electron, not within it. While Electron does have its own main process and renderer processes, this GTK application operates as a native OS window that's launched from the Electron application but runs in its own process or thread. The hello_gui() function specifically starts the GTK application with its own thread (g_gtk_thread), application loop, and UI context.

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

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

🌐 Let's take a closer look at the code above:

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

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

🌐 The UI layout is defined inline using XML, which is a common pattern in GTK applications. It creates a main window, input controls (text entry, calendar, and add button), a list box for displaying todos, and proper layout containers and scrolling.

主要 GUI 函数和线程管理

🌐 Main GUI function and thread management

既然我们已经把一切连接好了,我们可以添加两个核心的 GUI 功能:hello_gui()(我们会从 JavaScript 调用它)和 cleanup_gui() 来清除所有内容。你们可能会很高兴地听到,我们对 GTK 应用、上下文和线程的仔细设置使这一切变得非常简单:

🌐 Now that we have everything wired, up, we can add our two core GUI functions: hello_gui() (which we'll call from JavaScript) and cleanup_gui() to get rid of everything. You'll be hopefully delighted to hear that our careful setup of GTK app, context, and threads makes this straightforward:

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 应用的生命周期:

🌐 These functions manage the GTK application lifecycle:

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

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

🌐 Running GTK in a separate thread is crucial for Electron integration, as it prevents the GTK main loop from blocking Node.js's event loop.

回调管理

🌐 Callback management

之前,我们设置了全局变量来保存回调函数。现在,我们将添加函数来分配这些回调。这些回调函数构成了我们的本地 GTK 代码与 JavaScript 之间的桥梁,实现双向通信。

🌐 Previously, we setup global variables to hold our callbacks. Now, we'll add functions that assign those callbacks. These callbacks form the bridge between our native GTK code and JavaScript, allowing bidirectional communication.

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 放在一起

🌐 Putting cpp_code.cc together

我们现在已经完成了插件的 GTK 和本地部分——也就是最关注与操作系统交互的代码(相对而言,与连接本地 C++ 和 JavaScript 世界的部分关系较小)。在添加了以上所有部分后,你的 src/cpp_code.cc 应该看起来像这样:

🌐 We've now finished the GTK and native part of our addon - that is, the code that's most concerned with interacting with the operating system (and by contrast, less so with bridging the native C++ and JavaScript worlds). After adding all the sections above, your src/cpp_code.cc should look like this:

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 插件桥

🌐 5) Creating the Node.js addon bridge

现在让我们在 src/cpp_addon.cc 中实现我们的 C++ 代码和 Node.js 之间的桥梁。让我们从为我们的插件创建一个基本框架开始:

🌐 Now let's implement the bridge between our C++ code and Node.js in src/cpp_addon.cc. Let's start by creating a basic skeleton for our addon:

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 加载我们的本地代码提供了入口点。

🌐 This is the minimal structure required for a Node.js addon using node-addon-api. The Init function is called when the addon is loaded, and the NODE_API_MODULE macro registers our initializer. This basic skeleton doesn't do anything yet, but it provides the entry point for Node.js to load our native code.

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

🌐 Create a class to wrap our C++ code

让我们创建一个类来封装我们的 C++ 代码并将其暴露给 JavaScript。在上一步中,我们添加了一条注释“Class to wrap our C++ code will go here”——请将其替换为以下代码。

🌐 Let's create a class that will wrap our C++ code and expose it to JavaScript. In our previous step, we've added a comment reading "Class to wrap our C++ code will go here" - replace it with the code below.

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++ 类:

🌐 Here, we create a C++ class that inherits from Napi::ObjectWrap<CppAddon>:

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

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

构造函数初始化:

🌐 The constructor initializes:

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

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

🌐 The destructor properly cleans up the thread-safe function when the object is garbage collected.

实现基本功能 - 你好世界

🌐 Implement basic functionality - HelloWorld

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

🌐 Next, we'll add our two main methods, HelloWorld() and HelloGui(). We'll add these to our private scope, right where we have a comment reading "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 函数,无需参数
  • 由于该函数仅启动 UI,因此不返回任何内容(void)。
  • 这些方法构成了 JavaScript 调用和原生 C++ 函数之间的直接桥梁。

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

🌐 You might be wondering what Napi::CallbackInfo is or where it comes from. This is a class provided by the Node-API (N-API) C++ wrapper, specifically from the node-addon-api package. It encapsulates all the information about a JavaScript function call, including:

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

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

🌐 This class is fundamental to the Node.js native addon development as it serves as the bridge between JavaScript function calls and C++ method implementations. Every native method that can be called from JavaScript receives a CallbackInfo object as its parameter, allowing the C++ code to access and validate the JavaScript arguments before processing them. You can see us using it in HelloWorld() to get function parameters and other information about the function call. Our HelloGui() function doesn't use it, but if it did, it'd follow the same pattern.

设置事件系统

🌐 Setting up the event system

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

🌐 Now we'll tackle the tricky part of native development: setting up the event system. Previously, we added native callbacks to our cpp_code.cc code - and in our bridge code in cpp_addon.cc, we'll need to find a way to have those callbacks ultimately trigger a JavaScript method.

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

🌐 Let's start with the On() method, which we'll call from JavaScript. In our previously written code, you'll find a comment reading On() method implementation will go here. Replace it with the following method:

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 大部分工作将在它自己的线程上进行。

🌐 This method allows JavaScript to register callbacks for different event types and stores the JavaScript function in our callbacks map for later use. So far, so good - but now we need to let cpp_code.cc know about these callbacks. We also need to figure out a way to coordinate our threads, because the actual cpp_code.cc will be doing most of its work on its own thread.

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

🌐 In our code, find the section where we're declaring the constructor CppAddon(const Napi::CallbackInfo &info), which you'll find in the public section. It should have a comment reading We'll implement the constructor together with a callback struct later. Then, replace that part with the following code:

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

这是我们桥梁中最复杂的部分:实现双向通信。这里有一些值得注意的事项,让我们一步步来看看:

🌐 This is the most complex part of our bridge: implementing bidirectional communication. There are a few things worth noting going on here, so let's take them step by step:

CallbackData 结构体:

  • 保存事件类型、JSON 负载以及对我们插件的引用。

在构造函数中:

🌐 In the constructor:

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

让我们谈谈 napi_create_threadsafe_function。不同线程的协调或许是本地插件开发中最困难的部分——根据我们的经验,这也是开发者最容易放弃的地方。napi_create_threadsafe_function 由 N-API 提供,允许你从任何线程安全地调用 JavaScript 函数。当你使用像 GTK3 这样在自己线程上运行的 GUI 框架时,这一点至关重要。原因如下:

🌐 Let's talk about napi_create_threadsafe_function. The orchestration of different threads is maybe the most difficult part about native addon development - and in our experience, the place where developers are most likely to give up. napi_create_threadsafe_function is provided by the N-API and allows you to safely call JavaScript functions from any thread. This is essential when working with GUI frameworks like GTK3 that run on their own thread. Here's why it's important:

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

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

🌐 In our code, we're using it to bridge the gap between GTK3's event loop and Node.js's event loop, allowing events from our GUI to safely trigger JavaScript callbacks.

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

🌐 For developers wanting to learn more, you can refer to the official N-API documentation for detailed information about thread-safe functions, the node-addon-api wrapper documentation for the C++ wrapper implementation, and the Node.js Threading Model article to understand how Node.js handles concurrency and why thread-safe functions are necessary.

cpp_addon.cc 放在一起

🌐 Putting cpp_addon.cc together

我们现在已经完成了插件的桥接部分——也就是说,这部分代码主要负责在你的 JavaScript 和 C++ 代码之间搭建桥梁(相比之下,实际上与操作系统或 GTK 的交互较少)。在添加了上面所有部分之后,你的 src/cpp_addon.cc 应该如下所示:

🌐 We've now finished the bridge part of our addon - that is, the code that's most concerned with being the bridge between your JavaScript and C++ code (and by contrast, less so actually interacting with the operating system or GTK). After adding all the sections above, your src/cpp_addon.cc should look like this:

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 封装器

🌐 6) Creating a JavaScript wrapper

让我们在 js/index.js 中通过添加一个 JavaScript 封装器来完成最后的工作。正如我们所看到的,C++ 需要大量样板代码,而这些代码在 JavaScript 中可能更容易或更快编写——你会发现许多生产应用最终会在调用本地代码之前在 JavaScript 中转换数据或请求。例如,我们会将时间戳转换为合适的 JavaScript 日期。

🌐 Let's finish things off by adding a JavaScript wrapper in js/index.js. As we could all see, C++ requires a lot of boilerplate code that might be easier or faster to write in JavaScript - and you will find that many production applications end up transforming data or requests in JavaScript before invoking native code. We, for instance, turn our timestamp into a proper JavaScript date.

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 = {}
}

此封装器:

🌐 This wrapper:

  • 扩展 EventEmitter 以处理原生事件
  • 仅在 Linux 平台上加载
  • 将事件从 C++ 转发到 JavaScript
  • 提供简洁的方法来调用 C++。
  • 将 JSON 数据转换为正确的 JavaScript 对象

7) 构建和测试插件

🌐 7) Building and testing the addon

所有文件就绪后,即可构建插件:

🌐 With all files in place, you can build the addon:

npm run build

如果构建完成,现在你可以将插件添加到你的 Electron 应用中,并在那里 importrequire 它。

🌐 If the build completes, you can now add the addon to your Electron app and import or require it there.

使用示例

🌐 Usage Example

一旦你构建了这个插件,就可以在你的 Electron 应用中使用它。下面是一个完整的示例:

🌐 Once you've built the addon, you can use it in your Electron application. Here's a complete example:

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

运行此代码时:

🌐 When you run this code:

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

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

🌐 All interactions with the native GTK3 interface will trigger the corresponding JavaScript events, allowing your Electron application to respond to native GUI actions in real-time.

结论

🌐 Conclusion

你现在已经使用 C++ 和 GTK3 为 Linux 构建了一个完整的本地 Node.js 插件。该插件:

🌐 You've now built a complete native Node.js addon for Linux using C++ and GTK3. This addon:

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

这个基础可以扩展,以在你的 Electron 应用中实现更复杂的 Linux 特定功能。你可以访问系统功能、集成 Linux 特定的库,或创建高性能的原生 UI,同时保持 Electron 提供的开发灵活性和简便性。

有关 GTK3 开发的更多信息,请参阅 GTK3 文档GLib/GObject 文档。你还可以查阅 Node.js N-API 文档node-addon-api,以帮助扩展你的原生插件。

🌐 This foundation can be extended to implement more complex Linux-specific features in your Electron applications. You can access system features, integrate with Linux-specific libraries, or create performant native UIs while maintaining the flexibility and ease of development that Electron provides. For more information on GTK3 development, refer to the GTK3 Documentation and the GLib/GObject documentation. You may also find the Node.js N-API documentation and node-addon-api helpful for extending your native addons.