利用C++和napi以及gtk编写addons向js提供接口
在做electron的时候,发现有一些linux上的内容只凭借node的api无法实现,例如获取桌面文件的图标等等,而且目前情况下node-ffi已经很久不更新了,使用起来还需要降级node,但是降级的话很多新的模块也就不能用了(比如fs-extra还有什么promise的错误)。
因此,选择了napi来开发,这个用起来很方便。而且利用node-gyp编译后,结果就是.node形式的二进制文件,直接import就可以用了,但是为了方便使用最好是将你的addons使用bindings封装成模块,因为我实测不封装成模块,后面项目编译和打包的时候根本搜索不到你的.node文件,并且回报一些奇怪的错误。
在写小项目的时候,甚至不用管js,专注于写C++然后,最后的时候,利用napi头文件,根据示例提供的方式获取js层传来的参数,调用自己写的方法,在把c++类型封装成js类型就可以了。
也就是说,在编写的时候,和普通c++唯一的区别就是,你需要关注怎么把js传进来的参数转换C++类型,怎么把你写的c++类型转换成js类型,ok,bb完了,进入正题.
第一步:首先新建文件夹,新建package.json
package.json中填写一下内容:
{
"name": "native",
"version": "1.0.0",
"description": "",
"private": true,
"gypfile": true,
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"bindings": "^1.5.0",
"node-addon-api": "^4.1.0"
}
}
当然这里是我给出的版本,这些模块你可以使用自己喜欢的版本,然后npm install 安装相关模块
新建binding.gyp文件
这个文件用于配置和你的C++文件相关的东西,例如入口文件、你需要用到的库文件、头文件,后续都需要在这里配置模板是这样:
{
"targets": [
{
"target_name": "nativelib",
"sources": [
"main.cc"
],
"include_dirs": [
"<!@(node -p \"require('node-addon-api').include\")",
"/usr/include/at-spi2-atk/2.0",
"/usr/include/at-spi-2.0",
"/usr/include/dbus-1.0",
"/usr/lib/x86_64-linux-gnu/dbus-1.0/include",
"/usr/include/gtk-3.0",
"/usr/include/gio-unix-2.0",
"/usr/include/cairo",
"/usr/include/pango-1.0",
"/usr/include/fribidi",
"/usr/include/harfbuzz",
"/usr/include/atk-1.0",
"/usr/include/cairo",
"/usr/include/pixman-1",
"/usr/include/uuid",
"/usr/include/freetype2",
"/usr/include/libpng16",
"/usr/include/gdk-pixbuf-2.0",
"/usr/include/libmount",
"/usr/include/blkid",
"/usr/include/glib-2.0",
"/usr/lib/x86_64-linux-gnu/glib-2.0/include",
],
"libraries": [
"-lgtk-3",
"-lgdk-3",
"-lpangocairo-1.0",
"-lpango-1.0",
"-lharfbuzz -latk-1.0",
"-lcairo-gobject",
"-lcairo",
"-lgdk_pixbuf-2.0",
"-lgio-2.0",
"-lgobject-2.0",
"-lglib-2.0",
],
"dependencies": [
"<!(node -p \"require('node-addon-api').gyp\")"
],
"cflags!": ["-fno-exceptions"],
"cflags_cc!": ["-fno-exceptions"],
"defines": ["NAPI_CPP_EXCEPTIONS"],
"xcode_settings": {
"GCC_ENABLE_CPP_EXCEPTIONS": "YES"
}
}
]
}
在普通c++项目中,我们可以使用pkg-config去搜索相关的库和头文件,这个addons中我没有找到什么替代方案,这个里面我不能使用pkg-config,所以我是手动用pkg-config在命令行中查找后手动填写的,比如在这个项目中我需要用到gtk,像这样
现在命令行中,搜索依赖:
pkg-config --cflags --libs gtk+-3.0
搜索的结果是
-pthread -I/usr/include/gtk-3.0 -I/usr/include/at-spi2-atk/2.0 -I/usr/include/at-spi-2.0 -I/usr/include/dbus-1.0 -I/usr/lib/x86_64-linux-gnu/dbus-1.0/include -I/usr/include/gtk-3.0 -I/usr/include/gio-unix-2.0 -I/usr/include/cairo -I/usr/include/pango-1.0 -I/usr/include/fribidi -I/usr/include/harfbuzz -I/usr/include/atk-1.0 -I/usr/include/cairo -I/usr/include/pixman-1 -I/usr/include/uuid -I/usr/include/freetype2 -I/usr/include/libpng16 -I/usr/include/gdk-pixbuf-2.0 -I/usr/include/libmount -I/usr/include/blkid -I/usr/include/glib-2.0 -I/usr/lib/x86_64-linux-gnu/glib-2.0/include -lgtk-3 -lgdk-3 -lpangocairo-1.0 -lpango-1.0 -lharfbuzz -latk-1.0 -
然后能根据这个结果,我将每一个-I后面跟的参数填写进bingding.gyp文件的include_dirs,然后pkg运行结果后面部分l开头的那些lib写到
bingding.gyp文件的libraries,就大功告成了,然后去你在bingding.gyp设置的c++文件(设置我的main.cc)里面写相关的C++代码
编译c++部分代码
下面是c++部分代码的写法:
#include<gtk/gtk.h>
#include<gio/gio.h>
#include<string>
#include <napi.h>
#include<gio-unix-2.0/gio/gdesktopappinfo.h>
using namespace Napi;
using std::string;
static std::vector<string> search_path;
//你自己的函数随便用c++写
void push(const string& path){
search_path.push_back(path);
}
/*这个函数用来封装你写的c++函数,也就是把传来的CallbackInfo获取出参数并转换,然后调用你写的c++函数,再把你的函数的返回值封装?成js的内容
*/
void call_add_search_path(const CallbackInfo& info){
string path = info[0].As<String>().Utf8Value();
push(path);
}
//export method
Napi::Object Init(Env env, Object exports) {
//在这里设置,第一个参数是你导出的函数的名字
exports.Set("add_seach_path", Function::New(env, call_add_search_path));
return exports;
}
NODE_API_MODULE(addon, Init)
这是我原项目main.cc的代码
#include<gtk/gtk.h>
#include<gio/gio.h>
#include<string>
#include <napi.h>
#include<gio-unix-2.0/gio/gdesktopappinfo.h>
using namespace Napi;
using std::string;
static std::vector<string> search_path;
void push(const string& path){
search_path.push_back(path);
}
/*
native method get_icon
desc:get icon thrrough gtk,gio
param : filename ,icon size
dependency:gtk 3.0
if you only want to get .so library,extract this function and delete eveything relative to napi,
then use "gcc main.c -fPIC -shared -o libiconget.so `pkg-config --cflags --libs gtk+-3.0`" command to compile it.
*/
string get_icon(const string& filename,const int size){
string result= "unknown";
//get file from path
GFile* file = g_file_new_for_path(filename.c_str());
GError* gerror = 0;
//query "standard::icon" info from file
GFileInfo* gfileinfo = g_file_query_info(file,"standard::icon", GFileQueryInfoFlags::G_FILE_QUERY_INFO_NONE,g_cancellable_new(), &gerror);
if (gerror) {
g_error_free(gerror);
gerror = 0;
}
if (gfileinfo) {
//get icon name from gfileinfo
GIcon* icon = g_file_info_get_icon (gfileinfo);
if(icon){
GtkIconTheme* theme = gtk_icon_theme_new();
//add some ukui paths
gtk_icon_theme_append_search_path(theme,"/usr/share/icons/ukui/48x48/places");
gtk_icon_theme_append_search_path(theme,"/usr/share/icons/ukui/48x48/mimetypes");
gtk_icon_theme_append_search_path(theme,"/usr/share/icons/ukui/48x48/apps");
gtk_icon_theme_append_search_path(theme,"/usr/share/icons/ukui/48x48/devices");
GtkIconInfo* info = 0;
//lookup icon
info = gtk_icon_theme_lookup_by_gicon (theme,icon,size,GtkIconLookupFlags::GTK_ICON_LOOKUP_NO_SVG);
if(info){
//copy result
result = gtk_icon_info_get_filename(info);
//free info
g_object_unref(info);
}
g_object_unref(theme);
}
//free gfileinfo,file,theme
g_object_unref(gfileinfo);
g_object_unref(file);
}
return result;
}
string get_desktop_name(const string& dpath,const string& unknow){
// std::ifstream is(dpath,std::ios_base::in);
// string iconname;
// string line;
// while(getline(is,line)){
// if(!strncmp(line.c_str(),"Name=",4)) {
// iconname = line.substr(5);
// break;
// }
// }
// is.close();
// if (iconname!="")return iconname;
// return unkown;
GDesktopAppInfo* appinfo = g_desktop_app_info_new_from_filename (dpath.c_str());
if(!appinfo) return unknow;
const char* result = g_desktop_app_info_get_string(appinfo,"Name");
if(result==NULL) return unknow;
return result;
}
string get_type(const string& s){
GFile* file = g_file_new_for_path(s.c_str());
GError* gerror = 0;
//query "standard::icon" info from file
GFileInfo* gfileinfo = g_file_query_info(file,"standard::icon", GFileQueryInfoFlags::G_FILE_QUERY_INFO_NONE,g_cancellable_new(), &gerror);
if (gerror) {
g_error_free(gerror);
gerror = 0;
return "";
}
GIcon* icon = g_file_info_get_icon (gfileinfo);
string type = g_themed_icon_get_names ((GThemedIcon*)icon)[0];
g_object_unref(gfileinfo);
g_object_unref(file);
return type;
}
string get_desktop_exec(const string& dpath){
// std::ifstream is(dpath,std::ios_base::in);
// string iconname;
// string line;
// while(getline(is,line)){
// if(!strncmp(line.c_str(),"Exec=",4)) {
// iconname = line.substr(5);
// break;
// }
// }
// is.close();
GDesktopAppInfo* appinfo = g_desktop_app_info_new_from_filename (dpath.c_str());
const char* re = g_desktop_app_info_get_string(appinfo,"Exec");
if(re == NULL) return "";
return re;
}
string get_desktop_icon(const string& dpath,const string& unkpath){
GDesktopAppInfo* appinfo = g_desktop_app_info_new_from_filename (dpath.c_str());
const char* c = g_desktop_app_info_get_string(appinfo,"Icon");
if(c== NULL) return unkpath;
string iconname = c;
if(g_path_is_absolute(iconname.c_str())) return iconname;
GFile* file = 0;
string cpath;
for(size_t i=0; i<search_path.size(); i++){
cpath = search_path[i] + iconname + ".png";
file = g_file_new_for_path(cpath.c_str());
if(g_file_query_exists(file,NULL))
{
g_object_unref(file);
return cpath;
}
}
return unkpath;
}
Array get_recommended_open_way(const CallbackInfo& info){
Env env = info.Env();
string path = info[0].As<String>().Utf8Value();
Array obj_array = Array::New(env);
GFile* file = g_file_new_for_path (path.c_str());
GError* gerror = 0;
GFileInfo* gfileinfo = g_file_query_info(file,"standard::*", GFileQueryInfoFlags::G_FILE_QUERY_INFO_NONE,g_cancellable_new(), &gerror);
if(gerror){
g_error_free(gerror);
gerror = 0;
g_object_unref(file);
return obj_array;
}
const char* types = g_file_info_get_content_type (gfileinfo);
GList* list = g_app_info_get_recommended_for_type (types);
string exe,name;
GAppInfo* appinfo = nullptr;
int i = 0;
while(list){
appinfo = static_cast<GAppInfo*>(list->data);
name = g_app_info_get_name(appinfo);
exe = g_app_info_get_commandline (appinfo);
Object obj = Object::New(env);
obj.Set("app",name);
obj.Set("exec",exe);
obj_array[i] = obj;
g_object_unref(appinfo);
list = list->next;
i++;
}
//release object
g_object_unref(file);
return obj_array;
}
//transfer params from node and call c++ native method get_icon
String call_get_icon(const CallbackInfo& info){
Env env = info.Env();
string filename = info[0].As<String>().Utf8Value();
int size = info[1].ToNumber().Int32Value();
return String::New(env,get_icon(filename,size));
}
String call_get_desktop_icon(const CallbackInfo& info){
Env env = info.Env();
string dpath = info[0].As<String>().Utf8Value();
string unknow = info[1].As<String>().Utf8Value();
return String::New(env,get_desktop_icon(dpath,unknow));
}
String call_get_desktop_name(const CallbackInfo& info){
Env env = info.Env();
string dpath = info[0].As<String>().Utf8Value();
string unknow = info[1].As<String>().Utf8Value();
return String::New(env,get_desktop_name(dpath,unknow));
}
String call_get_desktop_exec(const CallbackInfo& info){
Env env = info.Env();
string dpath = info[0].As<String>().Utf8Value();
return String::New(env,get_desktop_exec(dpath));
}
String call_get_type(const CallbackInfo& info){
Env env = info.Env();
string dpath = info[0].As<String>().Utf8Value();
return String::New(env,get_type(dpath));
}
void call_add_search_path(const CallbackInfo& info){
string path = info[0].As<String>().Utf8Value();
push(path);
}
//export method
Napi::Object Init(Env env, Object exports) {
exports.Set("get_ndesktop_icon", Function::New(env, call_get_icon));
exports.Set("get_desktop_icon", Function::New(env, call_get_desktop_icon));
exports.Set("add_seach_path", Function::New(env, call_add_search_path));
exports.Set("get_desktop_name", Function::New(env, call_get_desktop_name));
exports.Set("get_desktop_exec", Function::New(env, call_get_desktop_exec));
exports.Set("get_type", Function::New(env, call_get_type));
exports.Set("get_recommended_open_way", Function::New(env, get_recommended_open_way));
return exports;
}
NODE_API_MODULE(addon, Init)
到这里就差不多,写完之后,使用npm install 或者node-gyp rebuild去编译就可以了,编译后的.node文件就是在当前目录下build文件夹中的Release文件夹下。
目前我的项目结构是这样:
要使用的话,最简单的方法就是新建项目,把编译后的.node文件复制出来,下面我新建项目调用测试
看,我在main.cc里面写的native函数都被导出来了,使用当然也可以
如果你的项目比较小,到这就可以结束了,笔者写了一个小型的桌面应用,当时这样使用编译的.node,在测试的时候是没有问题的,但是打包成可执行文件的时候就一直找不到这个文件,所以我利用bindings将其封装node-module,解决了这个问题
封装成node-module
新建index.js,然后在index.js中这样写:
const native = require("bindings")("nativelib");
const fs = require("fs");
native.add_seach_path("/usr/share/pixmaps/");
native.add_seach_path("/usr/share/icons/hicolor/48x48/apps/");
native.add_seach_path("/usr/share/icons/ukui/48x48/apps/");
exports.get_icon = function getIcon(filename, size, unkownicon) {
if (filename.endsWith(".desktop")) {
return native.get_desktop_icon(filename, unkownicon);
} else {
let iconpath = native.get_ndesktop_icon(filename, size);
if (iconpath == "unknown") return unkownicon;
if (!iconpath.includes("gnome")) return iconpath;
let ukuipath = iconpath.replace("/gnome/", "/ukui/");
return fs.existsSync(ukuipath) ? ukuipath : iconpath;
}
};
exports.get_desktop_name = function(filename, unkown) {
return native.get_desktop_name(filename, unkown);
};
exports.get_desktop_exec = function(filename) {
return native.get_desktop_exec(filename);
};
exports.get_type = function(filename) {
return native.get_type(filename);
};
exports.get_recommended_open_way = function(path) {
return native.get_recommended_open_way(path);
};
项目结构:
这样就成了,如果你的主项目要用到这个模块,只需要复制这个文件夹到项目路径里面,然后执行
npm install --save ./native
就可以将其安装到你的项目中
这样其实还有个好处,别人使用你的模块的时候,你甚至可以将node-modules文件夹和build文件夹删除,一方面发送的时候小,另一方面,然后别人安装的时候,会在他的系统环境下自动重新编译一下这个模块,提高了平台的适用性。
结束!
|