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 小部件,包括按钮、输入字段和列表

    ¥A comprehensive set of UI widgets like buttons, entry fields, and lists

  • 跨不同 Linux 发行版的跨桌面兼容性

    ¥Cross-desktop compatibility across various Linux distributions

  • 与 Linux 桌面原生主题和辅助功能集成

    ¥Integration with the native theming and accessibility features of Linux desktops

[!NOTE] 我们特别使用 GTK3,因为 Chromium(以及 Electron)内部使用的是 GTK3。使用 GTK4 会导致运行时冲突,因为 GTK3 和 GTK4 会加载到同一个进程中。如果 Chromium 升级到 GTK4,你很可能也可以轻松地将原生代码升级到 GTK4。

¥[!NOTE] We specifically use GTK3 because that's what Chromium (and by extension, Electron) uses internally. Using GTK4 would cause runtime conflicts since both GTK3 and GTK4 would be loaded in the same process. If and when Chromium upgrades to GTK4, you will likely be able to easily upgrade your native code to GTK4, too.

本教程对那些已经熟悉 Linux 上 GTK 开发的读者最为有用。你应该熟悉 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 开发指南。

¥[!NOTE] If you're not already familiar with these concepts, the GTK3 documentation and GTK3 tutorials are excellent resources to get started. The GNOME Developer Documentation also provides comprehensive guides for GTK development.

要求

¥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 发行版

    ¥A Linux distribution with GTK3 development files installed

  • pkg-config 工具

    ¥The pkg-config tool

  • G++ 编译器和构建工具

    ¥G++ compiler and build tools

在 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

*

你可以重复使用我们在 原生代码和 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"
}
}

*

对于使用 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.

*

让我们在 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 函数

    ¥A basic hello_world function

  • 一个用于创建 GTK3 GUI 的 hello_gui 函数

    ¥A hello_gui function to create a GTK3 GUI

  • 待办事项操作的回调类型(添加、更新、删除)

    ¥Callback types for Todo operations (add, update, delete)

  • 回调函数的设置函数

    ¥Setter functions for the callback

*

现在,让我们在 src/cpp_code.cc 中实现 GTK3 GUI。我们将把它分成几个易于管理的部分。我们将从一些包含文件以及基本设置开始。

¥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 生成所需的头文件。

    ¥We include necessary headers for GTK3, standard library components, and UUID generation.

  • 定义一个 TodoCallback 类型来处理与 JavaScript 的通信。

    ¥Define a TodoCallback type to handle communication back to JavaScript.

  • 创建一个 TodoItem 结构体来存储我们的待办事项数据:

    ¥Create a TodoItem struct to store our todo data with:

    • 一个用于唯一标识的 UUID

      ¥A UUID for unique identification

    • 文本内容和时间戳

      ¥Text content and a timestamp

    • 一种转换为 JSON 并发送给 JavaScript 的方法

      ¥A method to convert to JSON for sending to JavaScript

    • 一个用于格式化日期以便显示的静态辅助函数

      ¥A static helper to format dates for display

toJson() 方法非常重要,因为它允许我们的 C++ 对象序列化并传输到 JavaScript。可能还有更好的方法,但本教程是关于如何将 C++ 与 Electron 结合用于原生 Linux UI 开发,所以我们在这里就不写更好的 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:

  • 我们稍后会用到的转发声明辅助函数

    ¥Forward-declare helper functions we'll use later

  • 在匿名命名空间中设置全局状态,包括:

    ¥Set up global state in an anonymous namespace, including:

    • addupdatedelete 待办事项操作的回调

      ¥Callbacks for the add, update, and delete todo operations

    • 用于线程管理的 GTK 主上下文和循环指针

      ¥GTK main context and loop pointers for thread management

    • 指向 GTK 线程本身的指针

      ¥A pointer to the GTK thread itself

    • 一个用于存储待办事项的向量

      ¥A vector to store our todos

这些全局变量跟踪应用状态,并允许代码的不同部分相互交互。线程管理变量(g_gtk_main_contextg_main_loopg_gtk_thread)尤为重要,因为 GTK 需要在自己的事件循环中运行。由于我们的代码将从 Node.js/Electron 的主线程调用,因此我们需要在单独的线程中运行 GTK,以避免阻塞 JavaScript 事件循环。这种分离确保了原生 UI 保持响应速度,同时仍允许与 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 回调,g_main_context_invoke 会在 GTK 主上下文中调度函数执行。提醒一下,GTK 主上下文是执行 GTK 操作的环境,以确保线程安全,因为 GTK 并非线程安全的,所有 UI 操作都必须在主线程上进行。

    ¥notify_callback: Safely invokes JavaScript callbacks from the GTK thread using g_main_context_invoke, which schedules function execution in the GTK main context. As a reminder, the GTK main context is the environment where GTK operations must be performed to ensure thread safety, as GTK is not thread-safe and all UI operations must happen on the main thread.

  • update_todo_row_label:使用新文本和格式化日期更新待办事项列表中的一行。

    ¥update_todo_row_label: Updates a row in the todo list with new text and formatted date.

  • create_todo_dialog:创建一个用于添加或编辑待办事项的对话框,其中包含:

    ¥create_todo_dialog: Creates a dialog for adding or editing todos with:

    • 一个用于输入待办事项文本的文本输入字段

      ¥A text entry field for the todo text

    • 用于选择日期的日历小部件

      ¥A calendar widget for selecting the date

    • 用于保存或取消的相应按钮

      ¥Appropriate buttons for saving or canceling

事件处理程序

¥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:通过以下方式处理待办事项的编辑:

¥edit_action: Handles editing a todo by:

  • 获取选定的行

    ¥Getting the selected row

  • 创建一个包含当前待办事项数据的对话框

    ¥Creating a dialog with the current todo data

  • 如果用户确认,则更新待办事项

    ¥Updating the todo if the user confirms

  • 通过回调通知 JavaScript

    ¥Notifying JavaScript via callback

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

¥delete_action: Removes a todo and notifies JavaScript.

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

¥on_add_clicked: Adds a new todo when the user clicks the Add button:

  • 从输入字段获取文本和日期

    ¥Gets text and date from input fields

  • 创建一个具有唯一 ID 的新 TodoItem

    ¥Creates a new TodoItem with a unique ID

  • 将其添加到列表和底层数据存储中

    ¥Adds it to the list and the underlying data store

  • 通知 JavaScript

    ¥Notifies JavaScript

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

¥on_row_activated: Shows a popup menu when a todo is clicked, with options to edit or delete.

GTK 应用设置

¥GTK application setup

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

¥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 应用主循环。

    ¥init_gtk_app: Runs the GTK application main loop.

  • activate_handler:设置激活时的应用 UI:

    ¥activate_handler: Sets up the application UI when activated:

    • 创建一个 GtkBuilder 用于加载 UI

      ¥Creates a GtkBuilder for loading the UI

    • 注册编辑和删除操作。

      ¥Registers edit and delete actions

    • 使用 GTK 的 XML 标记语言定义 UI 布局

      ¥Defines the UI layout using GTK's XML markup language

    • 将信号连接到我们的事件处理程序

      ¥Connects signals to our event handlers

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 应用,并分离该线程使其独立运行。

    ¥hello_gui: The entry point exposed to JavaScript that checks if GTK is already running, initializes GTK, creates a new main context and loop, launches a thread to run the GTK application, and detaches the thread so it runs independently.

  • cleanup_gui:在应用关闭时正确清理 GTK 资源。

    ¥cleanup_gui: Properly cleans up GTK resources when the application closes.

在单独的线程中运行 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

*

现在让我们在 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。在我们之前的步骤中,我们添加了一条注释 "用于封装我们 C++代码的类将放在这里"。 - 用下面的代码替换它。

¥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 接口:

¥static Napi::Object Init defines our JavaScript interface with three methods:

  • helloWorld:一个用于测试桥接的简单函数

    ¥helloWorld: A simple function to test the bridge

  • helloGui:启动 GTK3 UI 的函数

    ¥helloGui: The function to launch our GTK3 UI

  • on:一种注册事件回调的方法

    ¥on: A method to register event callbacks

构造函数初始化:

¥The constructor initializes:

  • emitter:一个将向 JavaScript 发送事件的对象

    ¥emitter: An object that will emit events to JavaScript

  • callbacks:已注册的 JavaScript 回调函数映射

    ¥callbacks: A map of registered JavaScript callback functions

  • tsfn_:一个线程安全函数句柄(对于 GTK3 线程通信至关重要)

    ¥tsfn_: A thread-safe function handle (crucial for GTK3 thread communication)

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

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

实现基本功能 - HelloWorld

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

  • 验证输入参数(必须是字符串)

    ¥Validates the input argument (must be a string)

  • 调用我们的 C++ hello_world 函数

    ¥Calls our C++ hello_world function

  • 将结果返回为 JavaScript 字符串。

    ¥Returns the result as a JavaScript string

HelloGui()

  • 直接调用 C++ hello_gui 函数,无需参数

    ¥Simply calls our C++ hello_gui function without arguments

  • 由于该函数仅启动 UI,因此不返回任何内容(void)。

    ¥Returns nothing (void) as the function just launches the UI

  • 这些方法构成了 JavaScript 调用和原生 C++ 函数之间的直接桥梁。

    ¥These methods form the direct bridge between JavaScript calls and our native C++ functions.

你可能想知道 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 传递的参数

    ¥The arguments passed from JavaScript

  • JavaScript 执行环境(通过 info.Env()

    ¥The JavaScript execution environment (via info.Env())

  • 函数调用的 this

    ¥The this value of the function call

  • 参数数量(通过 info.Length()

    ¥The number of arguments (via 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 结构体:

¥CallbackData struct:

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

    ¥Holds the event type, JSON payload, and a reference to our addon.

在构造函数中:

¥In the constructor:

  • 我们创建一个线程安全函数 (napi_create_threadsafe_function),它对于从 GTK3 线程调用 JavaScript 至关重要。

    ¥We create a thread-safe function (napi_create_threadsafe_function) which is crucial for calling into JavaScript from the GTK3 thread

  • 线程安全函数回调会解压数据并调用相应的 JavaScript 回调。

    ¥The thread-safe function callback unpacks the data and calls the appropriate JavaScript callback

  • 我们创建一个 lambda makeCallback,用于为不同的事件类型生成回调函数。

    ¥We create a lambda makeCallback that produces callback functions for different event types

  • 我们使用 setter 函数将这些回调注册到我们的 C++ 代码中。

    ¥We register these callbacks with our C++ code using the setter functions

我们来谈谈 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 会导致崩溃或竞争条件。

    ¥Thread Safety: JavaScript in Electron runs on a single thread (exceptions apply, but this is a generally useful rule). Without thread-safe functions, calling JavaScript from another thread would cause crashes or race conditions.

  2. 队列管理:它会自动将函数调用排队并在 JavaScript 线程上执行。

    ¥Queue Management: It automatically queues function calls and executes them on the JavaScript thread.

  3. 资源管理:它会处理合适的引用计数,以确保对象在仍然需要时不会被垃圾回收。

    ¥Resource Management: It handles proper reference counting to ensure objects aren't garbage collected while still needed.

在我们的代码中,我们使用它来弥合 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 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)

*

让我们在 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 以处理原生事件

    ¥Extends EventEmitter for native event handling

  • 仅在 Linux 平台上加载

    ¥Only loads on Linux platforms

  • 将事件从 C++ 转发到 JavaScript

    ¥Forwards events from C++ to JavaScript

  • 提供简洁的方法来调用 C++。

    ¥Provides clean methods to call into C++

  • 将 JSON 数据转换为正确的 JavaScript 对象

    ¥Converts JSON data into proper JavaScript objects

*

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

¥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++ 的问候语

    ¥The helloWorld() call will return a greeting from C++

  2. 当用户与 GTK3 GUI 交互时,事件监听器将被触发。

    ¥The event listeners will be triggered when users interact with the GTK3 GUI

  3. helloGui() 调用将打开一个原生 GTK3 窗口,其中包含:

    ¥The helloGui() call will open a native GTK3 window with:

    • 一个用于输入待办事项的文本输入字段

      ¥A text entry field for todo items

    • 用于选择日期的日历小部件

      ¥A calendar widget for selecting dates

    • 用于创建新待办事项的 "添加" 按钮

      ¥An "Add" button to create new todos

    • 一个显示所有待办事项的可滚动列表

      ¥A scrollable list showing all todos

    • 右键单击上下文菜单,用于编辑和删除待办事项。

      ¥Right-click context menus for editing and deleting todos

所有与原生 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++ 之间提供双向桥梁。

    ¥Provides a bidirectional bridge between JavaScript and C++

  2. 创建一个在其自身线程中运行的原生 GTK3 GUI

    ¥Creates a native GTK3 GUI that runs in its own thread

  3. 实现一个具有添加功能的简单 Todo 应用

    ¥Implements a simple Todo application with add functionality

  4. 使用 GTK3,它与 Electron 的 Chromium 运行时兼容

    ¥Uses GTK3, which is compatible with Electron's Chromium runtime

  5. 安全地处理从 C++ 到 JavaScript 的回调

    ¥Handles callbacks from C++ to JavaScript safely

此基础可以扩展,以便在你的 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.