- 投稿日:2021-02-13T18:50:14+09:00
Node.js からC++関数への引数、返り値まとめ
はじめに
- node.jsからC++関数を利用する際に、引数・返り値の渡し方について困ったことはありませんか?
- この記事では、
node-addon-api
を使う場合の、引数・返り値の受け渡し方をまとめています。環境構築がまだの方は?
- この記事の内容をトライする前に、以下の記事で環境構築を完了させてください!
目次
1. 関数の基本形
- node-addon-apiを利用する場合、ラップされるC++の関数は以下のように定義します。
Napi::Value 関数名(const Napi::CallbackInfo& info);
- 引数の個数、型に関係なく、引数は
const Napi::CallbackInfo& info
と記述- 返り値は、型に関係なく
Napi::Value
と記述- 以下は、関数宣言と関数定義の例です。
- 引数、返り値ともに
void
です。void.h#include <napi.h> Napi::Value func(const Napi::CallbackInfo& info);void.cpp#include <napi.h> Napi::Value func(const Napi::CallbackInfo& info) { // Do nothing. return env.Null(); }2. 値の受け渡し
- js ↔︎ C++でやり取りするにあたって必要な引数、返り値の処理についてまとめました。
引数の型対応表
C++型 napi型 int .As<Napi::Number>().Int32Value()
double .As<Napi::Number>().DoubleValue()
std::string .As<Napi::String>().ToString()
返り値の型対応表
C++型 napi型 int, double return Napi::Number::New(env, C++変数名) std::string return Napi::String::New(env, C++変数名)
- jsは数値型が1種類しかないため、数値であればNapi::Number型で良い
関数例
- 例として、jsから2つの引数(a,b)をC++で受け取り、その和をjsに返却する関数add()を考えてみましょう。
example.cpp#include <napi.h> Napi::Value add(const Napi::CallbackInfo& info) { // お約束 Napi::Env env = info.Env(); // 引数は、配列infoから取り出す。 double a = info[0].As<Napi::Number>().DoubleValue(); double b = info[1].As<Napi::Number>().DoubleValue(); // C++で行いたい処理を行う double ans = a + b; // 返り値は、Napi::○○ 型にキャストして返却する return Napi::Number::New(env, ans); }
- jsファイルからは次のように見えます
example.js// (前処理は省略) let ans = add(1,2) console.log(ans); // >> expected: 33. 配列の受け渡し
引数に配列を渡す( js → C++ )
array.cppNapi::Value setArr(const Napi::CallbackInfo& info) { Napi::Env env = info.Env(); Napi::Array arr = info[0].As<Array>(); // C++の配列 std::vector<double> vec(arr.Length(), 0.0); // for文で要素を順に代入 for(size_t i = 0; i < arr.Length(); i++){ Napi::Value val = arr[i]; vec[i] = val.As<Napi::Number>().DoubleValue(); } return env.Null(); }返り値に配列を渡す( C++ → js )
array.cppNapi::Value getArr(const CallbackInfo& info){ Napi::Env env = info.Env(); // C++の配列 std::vector<double> vec = {1.0, 0.5, 0.25}; // for文で要素を順に代入 Napi::Array outArr = Napi::Array::New(env, vec.size()); for (size_t i = 0; i < vec.size(); i++) { outArr[i] = Napi::Number::New(env, vec[i]); } return outArr; }
- jsファイルからは次のように利用します。
array.js// (前処理は省略) setArr([1,2,3,4,5]); var arr = getArr(); console.log(arr); // >> expected: [1.0, 0.5, 0.25]4. (応用)異なるプリミティブ型を配列に入れて返却する
- C++で複数の返り値を返したい場合に有効です。
- リターンコードと計算結果の組み合わせなどを返却できます。
array2.cppNapi::Value getReturns(const CallbackInfo& info){ Napi::Env env = info.Env(); // do Something C++ // 返り値として、 1.0 と "aabbcc" を返却する例 const int returnArgNums = 2; int zero = 0; int one = 1; Napi::Array retArr = Napi::Array::New(env, returnArgNums); retArr[zero] = Napi::Number::New(env, 1.0); retArr[one] = Napi::String::New(env, "aabbcc"); return retArr; }array2.js// (前処理は省略) var arr = getReturns(); console.log("ret1 =", arr[0], "ret2 =", arr[1]); // >> expected: ret1 = 1 ret2 = aabbcc5. 試してみよう
- 前回の記事 で作成したプロジェクトの
wrapper.h
,wrapper.cpp
,index.js
を下記のコードで上書きすると、本記事の内容を試すことができます!├── node_modules
├── package-lock.json
├── package.json
├── index.js <-- 上書き
├── addon.cc
├── wrapper.cc <-- 上書き
├── wrapper.h <-- 上書き
└── binding.gypサンプルコードはこちら
wrapper.hを開く
wrapper.h#ifndef WRAPPER #define WRAPPER #include <napi.h> // 必要なヘッダ class Wrapper : public Napi::ObjectWrap<Wrapper> { public: static Napi::Object Init(Napi::Env env, Napi::Object exports); static Napi::Object NewInstance(Napi::Env env, const Napi::CallbackInfo& info); Wrapper(const Napi::CallbackInfo& info); ~Wrapper(); Napi::Value getNum(const Napi::CallbackInfo& info); Napi::Value add(const Napi::CallbackInfo& info); // <-- 追加 Napi::Value setArr(const Napi::CallbackInfo& info); // <-- 追加 Napi::Value getArr(const Napi::CallbackInfo& info); // <-- 追加 Napi::Value getReturns(const Napi::CallbackInfo& info); // <-- 追加 private: double m_value; }; #endif
wrapper.ccを開く
wrapper.cc#include "wrapper.h" #include <napi.h> using namespace Napi; // ---------------------------------------------------------- // // ---------------------のり付け部分--------------------------- // // ---------------------------------------------------------- // // new() の定義 Napi::Object Wrapper::NewInstance(Napi::Env env, const Napi::CallbackInfo &info) { Napi::EscapableHandleScope scope(env); // jsからコンストラクタに渡されるArgsは infoに配列として入っている const std::initializer_list<napi_value> initArgList = {info[0]}; // ここでWrapper:::Wrapper()が呼ばれる Napi::Object obj = env.GetInstanceData<Napi::FunctionReference>()->New(initArgList); // gcにメモリ解放されないようにスコープを除外する return scope.Escape(napi_value(obj)).ToObject(); } // メンバ関数のバインド Napi::Object Wrapper::Init(Napi::Env env, Napi::Object exports) { Napi::Function func = DefineClass( env, "Wrapper", { // ここにメソッドを登録する InstanceMethod("getNum", &Wrapper::getNum), InstanceMethod("add", &Wrapper::add), // <-- 追加 InstanceMethod("setArr", &Wrapper::setArr), // <-- 追加 InstanceMethod("getArr", &Wrapper::getArr), // <-- 追加 InstanceMethod("getReturns", &Wrapper::getReturns), // <-- 追加 }); Napi::FunctionReference *constructor = new Napi::FunctionReference(); *constructor = Napi::Persistent(func); env.SetInstanceData(constructor); exports.Set("Wrapper", func); return exports; } // ---------------------------------------------------------- // // --------------- Wrapperクラスの定義はこれより下 --------------- // // ---------------------------------------------------------- // // コンストラクタ Wrapper::Wrapper(const Napi::CallbackInfo &info) : Napi::ObjectWrap<Wrapper>(info) { m_value = 0.0; }; Wrapper::~Wrapper(){}; // メンバ関数 Napi::Value Wrapper::getNum(const Napi::CallbackInfo &info) { Napi::Env env = info.Env(); return Napi::Number::New(env, this->m_value); } // 引数・返り値の受け渡し Napi::Value Wrapper::add(const Napi::CallbackInfo& info) { // お約束 Napi::Env env = info.Env(); // 引数は、配列infoから取り出す。 double a = info[0].As<Napi::Number>().DoubleValue(); double b = info[1].As<Napi::Number>().DoubleValue(); // C++で行いたい処理を行う double ans = a + b; // 返り値は、Napi::○○ 型にキャストして返却する return Napi::Number::New(env, ans); } // 配列の受け取り Napi::Value Wrapper::setArr(const Napi::CallbackInfo& info) { Napi::Env env = info.Env(); Napi::Array arr = info[0].As<Array>(); // C++の配列 std::vector<double> vec(arr.Length(), 0.0); // for文で要素を順に代入 for(size_t i = 0; i < arr.Length(); i++){ Napi::Value val = arr[i]; vec[i] = val.As<Napi::Number>().DoubleValue(); } return env.Null(); } // 配列の返却 Napi::Value Wrapper::getArr(const CallbackInfo& info){ Napi::Env env = info.Env(); // C++の配列 std::vector<double> vec = {1.0, 0.5, 0.25}; // for文で要素を順に代入 Napi::Array outArr = Napi::Array::New(env, vec.size()); for (size_t i = 0; i < vec.size(); i++) { outArr[i] = Napi::Number::New(env, vec[i]); } return outArr; } // プリミティブ型が混在した配列の返却 Napi::Value Wrapper::getReturns(const CallbackInfo& info){ Napi::Env env = info.Env(); // do Something C++ // 返り値として、 1.0 と "aabbcc" を返却する例 const int returnArgNums = 2; int zero = 0; int one = 1; Napi::Array retArr = Napi::Array::New(env, returnArgNums); retArr[zero] = Napi::Number::New(env, 1.0); retArr[one] = Napi::String::New(env, "aabbcc"); return retArr; }
index.jsを開く
index.js// addon.cc内の NODE_API_MODULE(addon, InitAll) が呼ばれる var Wrapper = require('bindings')('addon'); // addon.cc内の CreateObject() が呼ばれる var obj = new Wrapper() // wrapper.cc内で登録した getNum()が呼ばれる console.log(obj.getNum()); // ---- 本記事で追加した関数 ---- // let ans = obj.add(1,2) console.log(ans); // >> expected: 3 obj.setArr([1,2,3,4,5]); var arr = obj.getArr(); console.log(arr); // >> expected: [1.0, 0.5, 0.25] var arr = obj.getReturns(); console.log("ret1 =", arr[0], "ret2 =", arr[1]); // >> expected: ret1 = 1 ret2 = aabbcc // ---- 本記事で追加した関数 ---- //その他
- 返り値にユーザー定義型やオブジェクトを返す場合は このあたり が参考になるかもしれません。
- 投稿日:2021-02-13T18:47:45+09:00
Google CalendarにAPI連携(スケジュール一覧取得 / スケジュール作成 / スケジュール削除)する(Node.js)
GoogleカレンダーにNode.jsからAPI連携する機会があったので、手順をメモしておきます。
GitHub
https://github.com/Thirosue/gcp-sample/tree/main/calendar
手順
- カレンダーAPIの有効化
- クレデンシャル作成
- トークン生成 / スケジュール一覧取得
- API連携(スケジュール作成 / スケジュール削除)
1. カレンダーAPIの有効化
GCPにログインし、検索フォームで「Calendar」と入力し、「Google Calendar API」をクリックします。
APIを有効にします。
2. クレデンシャル作成
次は、API連携に必要なクレデンシャルの作成です。
「Get Started with the Calendar API」をクリックして、公式のチュートリアルページに移動します。
チュートリアルページに移動後、「Node.js」を選択します。
Node.jsのクイックスタートページの「Enable the Google Calendar API」ボタンをクリックします。
任意のプロジェクト名を入力して、先に進みます。
Node.jsをコマンドライン実行する場合は、「Desktop app」を選択し、作成します。
クレデンシャルファイルをダウンロードします。
本手順に従う場合は、名前を「credentials.json」としてください。3. トークン生成 / スケジュール一覧取得
次に、API連携用のトークンを作成します。
git clone
& 作業フォルダに移動 &2.
で作成したファイルを移動 & ライブラリダウンロード(yarn install
)$ git clone git@github.com:Thirosue/gcp-sample.git $ cd gcp-sample/calendar # 作業フォルダに移動 $ mv ~/Desktop/credentials.json . # 2.で作成したファイル(credentials.json)を作業フォルダ直下に配置してください。 ※移動元は調整 $ yarn install
- 作成したアプリの認証用ページに移動
list.js(
node list.js
)を実行して、表示されたURLをブラウザで表示してください。% node list.js Authorize this app by visiting this url: https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcalendar&response_type=code&client_id=*********************************************************GCPユーザでログインします。
警告が表示されますが、カレンダーにアクセス権限を付与するため、先に進みます。
権限が適切であることを確認して、許可します。
→ここでは、カレンダーの追加 / 削除 / 閲覧を許可するため、編集権限も付与しています。検証コードが表示されるので、コピーしておきます。
念のため、次のステップが完了するまで、ブラウザは表示したままにしておきましょう。
- トークンの作成
再度、list.js(
node list.js
)を実行して、先ほど生成したコードを貼り付けて、実行(Enterキー入力)します。% node list.js Authorize this app by visiting this url: https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcalendar&response_type=code&client_id=********************************************************* Enter the code from that page here: <<ここに作成したコードを貼り付ける>>すると、スケジュール一覧APIが実行でき、直近の予定が取得できるでしょう。
% node list.js Authorize this app by visiting this url: https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcalendar&response_type=code&client_id=********************************************************* Enter the code from that page here: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX Token stored to token.json Upcoming 10 events: 2021-02-14T09:00:00+09:00 - 予定1 2021-02-16T09:30:00+09:00 - 予定2 ...(省略)...4. API連携
スケジュール作成
先ほど作成した、クレデンシャルとトークンを利用して、スケジュールを作成します。
create.jsを実行(node create.js
)してみましょう。スケジュール作成 実行(
node create.js
)% node create.js Create Event captured: // イベントの情報です。 { summary: 'サンプル', description: 'カレンダー説明', start: { dateTime: '2021-02-13T18:41:24+09:00', timeZone: 'Asia/Tokyo' }, end: { dateTime: '2021-02-13T19:41:24+09:00', timeZone: 'Asia/Tokyo' }, colorId: 2, reminders: { useDefault: false, overrides: [ [Object], [Object] ] } } Event created: { config: { url: 'https://www.googleapis.com/calendar/v3/calendars/primary/events', method: 'POST', paramsSerializer: [Function (anonymous)], data: { summary: 'サンプル', description: 'カレンダー説明', start: [Object], end: [Object], colorId: 2, reminders: [Object] }, headers: { ...(省略)... }, params: [Object: null prototype] {}, validateStatus: [Function (anonymous)], body: '{"summary":"サンプル","description":"カレンダー説明","start":{"dateTime":"2021-02-13T18:41:24+09:00","timeZone":"Asia/Tokyo"},"end":{"dateTime":"2021-02-13T19:41:24+09:00","timeZone":"Asia/Tokyo"},"colorId":2,"reminders":{"useDefault":false,"overrides":[{"method":"email","minutes":120},{"method":"popup","minutes":30}]}}', responseType: 'json' }, data: { kind: 'calendar#event', etag: '"3226411377019000"', id: '01d3i44j58jp6v609ag8aehtb0', //<----- このイベントIDを削除で使います status: 'confirmed', ...(省略)... }, headers: { ...(省略)... }, status: 200, statusText: 'OK' }
- 認証クライアント取得コードの内容(
./auth.js
) ※スケジュール作成 / スケジュール削除で利用./auth.jsconst fs = require('fs').promises; const { google } = require('googleapis'); const getOAuth2Client = async () => { const credentialsText = await fs.readFile('credentials.json', 'utf-8'); const credentials = JSON.parse(credentialsText); const tokenText = await fs.readFile('token.json', 'utf-8'); const token = JSON.parse(tokenText); const { client_secret, client_id, redirect_uris } = credentials.installed; const oAuth2Client = new google.auth.OAuth2( client_id, client_secret, redirect_uris[0]); oAuth2Client.setCredentials(token); return oAuth2Client; } module.exports = getOAuth2Client;
- スケジュール作成コードの内容(
./create.js
)./create.jsconst { google } = require('googleapis'); const moment = require('moment'); const getOAuth2Client = require('./auth'); // サンプルイベント const event = { 'summary': 'サンプル', 'description': 'カレンダー説明', 'start': { 'dateTime': moment().add(1, 'h').format(), 'timeZone': 'Asia/Tokyo', }, 'end': { 'dateTime': moment().add(2, 'h').format(), 'timeZone': 'Asia/Tokyo', }, 'colorId': 2, // @see https://lukeboyle.com/blog-posts/2016/04/google-calendar-api---color-id 'reminders': { 'useDefault': false, 'overrides': [ { 'method': 'email', 'minutes': 120 }, { 'method': 'popup', 'minutes': 30 }, ], }, }; (async () => { console.log('Create Event captured:'); console.log(event); const auth = await getOAuth2Client(); // 認証クライアント取得 const calendar = google.calendar({ version: 'v3', auth }); // カレンダーAPI連携用クライアント取得 const response = await calendar.events.insert({ auth, calendarId: 'primary', resource: event, }); console.log('Event created:', response); })()スケジュール削除
先ほど作成した、クレデンシャルとトークンとイベントIDを利用して、スケジュールを削除します。
delete.jsを編集して、実行(node delete.js
)してみましょう。イベントIDの編集
./delete.jsconst { google } = require('googleapis'); const getOAuth2Client = require('./auth'); const eventId = '01d3i44j58jp6v609ag8aehtb0'; //<----- イベントIDを先ほど作成したイベントのIDに変更します ...(省略)...スケジュール削除 実行(
node delete.js
)% node delete.js Delete Google Event id: 01d3i44j58jp6v609ag8aehtb0 Event deleted: { config: { url: 'https://www.googleapis.com/calendar/v3/calendars/primary/events/01d3i44j58jp6v609ag8aehtb0', method: 'DELETE', paramsSerializer: [Function (anonymous)], headers: { ...(省略)... }, params: [Object: null prototype] {}, validateStatus: [Function (anonymous)], responseType: 'json' }, data: '', headers: { ...(省略)... }, status: 204, statusText: 'No Content' }
- スケジュール削除コードの内容(
./delete.js
)./delete.jsconst { google } = require('googleapis'); const getOAuth2Client = require('./auth'); const eventId = '01d3i44j58jp6v609ag8aehtb0'; // イベントIDを指定 (async () => { console.log('Delete Google Event id: %s', eventId); const auth = await getOAuth2Client(); // 認証クライアント取得 const calendar = google.calendar({ version: 'v3', auth }); // カレンダーAPI連携用クライアント取得 const response = await calendar.events.delete({ auth, calendarId: 'primary', eventId, }); console.log('Event deleted:', response); })()終わりに
API連携することで、簡単にGoogleカレンダーが利用できました。
Googleカレンダーは、優れたUIやリマインダー機能なども搭載されているため、スケジュール機能が必要な場合、API連携も検討してみるのも良いかもしれません。参考
- 投稿日:2021-02-13T14:40:40+09:00
M1 MacでLINE botをVS Codeで開発してAzure Functionsへデプロイする環境構築メモ (Node.js版)
はじめに
M1 MacでLINE botをVS Codeで開発してAzure Functionsへデプロイするための環境構築メモです。
macOS Big Sur
バージョン: 11.2.11. VS Codeのインストール
バージョン: 1.53.2
2. VS CodeのAzure Functions extensionのインストール
3. Node.jsのインストール
Node.jsをインストールするためにnvmをインストールします。
ターミナルを開く(Rosettaは使用しない)
% curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.37.2/install.sh | bash
.zshrcファイルに以下を追加する。(.zshrcファイルが無ければ作成する)
export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm.zshrcファイルを読み込む。
% source .zshrc
Node.js v14.15.4のインストール
% nvm i v14.15.4(ビルドに時間が掛かります)
% npm -v 6.14.10 % node -v v14.15.4VS Codeでサンプルコードを動かそうとするとAzure Functions Core Toolsをインストールするようにエラーが表示される。
Azure Functions Core ToolsをインストールするためにはHomebrew (brewコマンド)を使用する必要がある。
4. Xcodeのインストール
5. Command Line Tools for Xcodeのインストール
6. Homebrewのインストール
% /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"brewコマンドが使用できるようにPATHを追加します。
% echo 'eval $(/opt/homebrew/bin/brew shellenv)' >> ~/.zprofile % eval $(/opt/homebrew/bin/brew shellenv)/opt以下はrootユーザーの管理下にあるので、/opt/homebrew以下にログインユーザーへ所有権設定を行います。
% sudo chown -R $USER $(brew --prefix)7. Azure Functions Core Toolsのインストール
% brew tap azure/functions % brew install azure-functions-core-tools@3
Start Debugging[F5]で実行するとgrpc関連でエラーが出力されます。
対処方法はこちら。
https://qiita.com/kitazaki/items/daf9f028e84415985ca7
- 投稿日:2021-02-13T12:45:38+09:00
【Step-By-Step】Node.jsからC++クラスを利用するための環境構築
はじめに
この記事は、javascriptからC++を呼び出す処理が必要になった時の備忘録です。
node-addon-apiというラッピングライブラリを活用します。
この記事の内容を「マネすれば動く」ように意識して書いています。
Electronなどのデスクトップアプリに応用できます。事前準備
- 下記のパッケージは事前にインストールしておいてください
- npm
- node.js
応用記事はこちら!
1. Node.js からC++関数への引数、返り値まとめ
2. Node.jsからC++クラス、dllを使う
目次
1. プロジェクトの新規作成
- 空のフォルダを新規作成します。今回は例として、
napi_sample
というフォルダ名にしました。- 作成したフォルダに移動し、下記のコマンドを実行し、必要モジュールをインストールします。
terminal$ npm init -y $ npm install node bindings node-addon-api
- コマンド実行後、下記のようなフォルダ構成となります。
カレントディレクトリ
├── node_modules
├── package-lock.json
└── package.json
- さらに、下記のようなpackage.jsonが自動的に作成されます。
package.json{ "name": "doit_myself", "version": "1.0.0", "description": "", "main": "index.js", //<-- 開始時にこの.jsファイルが読み込まれる "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "bindings": "^1.5.0", "node": "^15.8.0", "node-addon-api": "^3.1.0" } }index.jsを作成する
カレントディレクトリ
├── node_modules
├── package-lock.json
├── package.json
└── index.js <-- 新規作成
- 起動時に読み込まれるindex.jsを追加しましょう。
- 下記のようなサンプルとします。
index.jsconsole.log("Hello! node.js");ターミナルで動作確認する
- ここまでの環境構築がうまくいっているか確認します。
- index.jsがあるディレクトリで下記のコマンドを入力してください。
terminal$ node . >> Hello! node.js2. Cppファイルを追加する
- ここからは、jsで利用するためのCppラッパークラスを作成していきます。
- wrapper.h, wrapper.cc, addon.ccの順に説明します。
カレントディレクトリ
├── node_modules
├── package-lock.json
├── package.json
├── index.js
├── addon.cc <-- 新規作成
├── wrapper.cc <-- 新規作成
└── wrapper.h <-- 新規作成
※ 拡張子.cc はC++ファイルのことです。本質は .cppと変わりません。ラッパークラスの作成
- ネイティブC++をラッピングするクラスを作成します。
- このクラスの目的は、jsから渡された引数をC++で解釈できる形にし、C++の返り値をjsが利用できる形式に変換して渡すことです。
- 下記のwrapper.h, wrapper.ccをテンプレとしてお使いください。
wrapper.h#ifndef WRAPPER #define WRAPPER #include <napi.h> // 必要なヘッダ class Wrapper : public Napi::ObjectWrap<Wrapper> { public: static Napi::Object Init(Napi::Env env, Napi::Object exports); static Napi::Object NewInstance(Napi::Env env, const Napi::CallbackInfo& info); Wrapper(const Napi::CallbackInfo& info); ~Wrapper(); Napi::Value getNum(const Napi::CallbackInfo& info); private: double m_value; }; #endifwrapper.cc#include "wrapper.h" #include <napi.h> using namespace Napi; // ---------------------------------------------------------- // // ---------------------のり付け部分--------------------------- // // ---------------------------------------------------------- // // new() の定義 Napi::Object Wrapper::NewInstance(Napi::Env env, const Napi::CallbackInfo &info) { Napi::EscapableHandleScope scope(env); // jsからコンストラクタに渡されるArgsは infoに配列として入っている const std::initializer_list<napi_value> initArgList = {info[0]}; // ここでWrapper:::Wrapper()が呼ばれる Napi::Object obj = env.GetInstanceData<Napi::FunctionReference>()->New(initArgList); // gcにメモリ解放されないようにスコープを除外する return scope.Escape(napi_value(obj)).ToObject(); } // メンバ関数のバインド Napi::Object Wrapper::Init(Napi::Env env, Napi::Object exports) { Napi::Function func = DefineClass( env, "Wrapper", { // ここにメンバ関数を登録する InstanceMethod("getNum", &Wrapper::getNum), // InstanceMethod("jsから呼び出す際の関数名", "呼び出したいC++メンバ関数名"), }); Napi::FunctionReference *constructor = new Napi::FunctionReference(); *constructor = Napi::Persistent(func); env.SetInstanceData(constructor); exports.Set("Wrapper", func); return exports; } // ---------------------------------------------------------- // // --------------- Wrapperクラスの定義はこれより下 --------------- // // ---------------------------------------------------------- // // コンストラクタ Wrapper::Wrapper(const Napi::CallbackInfo &info) : Napi::ObjectWrap<Wrapper>(info) { m_value = 0.0; }; Wrapper::~Wrapper(){}; // メンバ関数 Napi::Value Wrapper::getNum(const Napi::CallbackInfo &info) { Napi::Env env = info.Env(); return Napi::Number::New(env, this->m_value); }
- 補足
- wrapper.cpp内の関数
Napi::Function func = DefineClass()
において、自作のC++メンバ関数を登録する必要があります。サンプルコードInstanceMethod("getNum", &Wrapper::getNum),
のように、InstanceMethod("jsから呼び出す際の関数名", "呼び出したいC++メンバ関数名")
として登録しなければなりません。jsとの結合用cppファイルを作る
- 次に、Wrapperクラスをjsモジュールとしてエクスポートするための処理をaddon.ccに記述します。
- こちらも詳細説明は省略します。テンプレとしてお使いください。
addon.cc#include <napi.h> #include "wrapper.h" #include <iostream> // jsオブジェクトが初期化された時 new()の呼び出し Napi::Object CreateObject(const Napi::CallbackInfo& info) { return Wrapper::NewInstance(info.Env(), info); } // js内でexport()が呼び出されたとき Napi::Object InitAll(Napi::Env env, Napi::Object exports) { // 関数定義 Napi::Object new_exports = Napi::Function::New(env, CreateObject); return Wrapper::Init(env, new_exports); } // jsへバインドするためのマクロ // jsで、 export('bindings')('addon')と記述したとき、上記のInitAll()が呼び出される NODE_API_MODULE(addon, InitAll)3. Cppファイルをビルドする
- 作成してたcppをビルドするための設定ファイルを作ります。
- binding.gyp というファイルです。
- VisualStudioのprojectのプロパティ設定、CMakeLists.txtと似たような設定をします。
binding.gypの追加
カレントディレクトリ
├── node_modules
├── package-lock.json
├── package.json
├── index.js
├── addon.cc
├── wrapper.cc
├── wrapper.h
└── binding.gyp <-- 新規作成binding.gyp{ "targets": [ { # ↓addon.cc内の NODE_API_MODULE(addon, InitAll) と同名にする "target_name": "addon", "cflags!": [ "-fno-exceptions" ], "cflags_cc!": [ "-fno-exceptions" ], # ↓必要な.ccファイルを全て記述する "sources": [ "addon.cc", "wrapper.cc"], "include_dirs": [ "<!@(node -p \"require('node-addon-api').include\")" ], "defines": [ 'NAPI_DISABLE_CPP_EXCEPTIONS' ], } ] }
- 上記ファイルをコピペいただければ問題ないです。
- 注意点として sources セクションには、使用する .cc (or .cpp) 拡張子のファイルを全て登録してください。
ビルド実行
- 下記コマンドを実行し、ビルドしてください。
terminal$ npm install . >> gyp info ok と表示されればビルド完了4. JavaScriptからビルドしたCppクラスを使う
- お待たせしました。最後に index.jsから wrapper.ccのクラスを使ってみましょう。
index.jsの書き換え
カレントディレクトリ
├── node_modules
├── package-lock.json
├── package.json
├── index.js <-- 書き換え
├── addon.cc
├── wrapper.cc
├── wrapper.h
└── binding.gyp
- index.jsを以下のように書き換えてください。
index.js// addon.cc内の NODE_API_MODULE(addon, InitAll) が呼ばれる var Wrapper = require('bindings')('addon'); // addon.cc内の CreateObject() が呼ばれる var obj = new Wrapper() // wrapper.cc内で登録した getNum()が呼ばれる console.log(obj.getNum());index.jsの実行
ターミナルで次のように実行します。
terminal$ node . >> 0 と表示されれば成功 >> Wrapper.m_valueの値が表示されています。お疲れ様でした。
node-addon-api は"お約束ごと"が多いので、
私はこの記事のようなテンプレを作り、使いまわしています。
参考になれば幸いです。参考リンク
- ドキュメント
- node-addon-examples
- 本格的に学びたい方は、このexampleを順にやっていくと良いでしょう
Others
- コンストラクタに複数の引数を渡す
- メンバ関数の引数、返り値について
- 別のC++クラスを利用する
- 投稿日:2021-02-13T12:45:38+09:00
【Step-By-Step】Node.jsからC++クラスを利用する
はじめに
この記事は、javascriptからC++を呼び出す処理が必要になった時の備忘録です。
node-addon-apiというラッピングライブラリを活用します。
この記事の内容を「マネすれば動く」ように意識して書いています。
Electronなどのデスクトップアプリに応用できます。事前準備
- 下記のパッケージは事前にインストールしておいてください
- npm
- node.js
応用記事はこちら!
1. Node.js からC++関数への引数、返り値まとめ
2. Coming soon...
目次
1. プロジェクトの新規作成
- 空のフォルダを新規作成します。今回は例として、
napi_sample
というフォルダ名にしました。- 作成したフォルダに移動し、下記のコマンドを実行し、必要モジュールをインストールします。
terminal$ npm init -y $ npm install node bindings node-addon-api
- コマンド実行後、下記のようなフォルダ構成となります。
カレントディレクトリ
├── node_modules
├── package-lock.json
└── package.json
- さらに、下記のようなpackage.jsonが自動的に作成されます。
package.json{ "name": "doit_myself", "version": "1.0.0", "description": "", "main": "index.js", //<-- 開始時にこの.jsファイルが読み込まれる "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "bindings": "^1.5.0", "node": "^15.8.0", "node-addon-api": "^3.1.0" } }index.jsを作成する
カレントディレクトリ
├── node_modules
├── package-lock.json
├── package.json
└── index.js <-- 新規作成
- 起動時に読み込まれるindex.jsを追加しましょう。
- 下記のようなサンプルとします。
index.jsconsole.log("Hello! node.js");ターミナルで動作確認する
- ここまでの環境構築がうまくいっているか確認します。
- index.jsがあるディレクトリで下記のコマンドを入力してください。
terminal$ node . >> Hello! node.js2. Cppファイルを追加する
- ここからは、jsで利用するためのCppラッパークラスを作成していきます。
- wrapper.h, wrapper.cc, addon.ccの順に説明します。
カレントディレクトリ
├── node_modules
├── package-lock.json
├── package.json
├── index.js
├── addon.cc <-- 新規作成
├── wrapper.cc <-- 新規作成
└── wrapper.h <-- 新規作成
※ 拡張子.cc はC++ファイルのことです。本質は .cppと変わりません。ラッパークラスの作成
- ネイティブC++をラッピングするクラスを作成します。
- このクラスの目的は、jsから渡された引数をC++で解釈できる形にし、C++の返り値をjsが利用できる形式に変換して渡すことです。
- 下記のwrapper.h, wrapper.ccをテンプレとしてお使いください。
wrapper.h#ifndef WRAPPER #define WRAPPER #include <napi.h> // 必要なヘッダ class Wrapper : public Napi::ObjectWrap<Wrapper> { public: static Napi::Object Init(Napi::Env env, Napi::Object exports); static Napi::Object NewInstance(Napi::Env env, const Napi::CallbackInfo& info); Wrapper(const Napi::CallbackInfo& info); ~Wrapper(); Napi::Value getNum(const Napi::CallbackInfo& info); private: double m_value; }; #endifwrapper.cc#include "wrapper.h" #include <napi.h> using namespace Napi; // ---------------------------------------------------------- // // ---------------------のり付け部分--------------------------- // // ---------------------------------------------------------- // // new() の定義 Napi::Object Wrapper::NewInstance(Napi::Env env, const Napi::CallbackInfo &info) { Napi::EscapableHandleScope scope(env); // jsからコンストラクタに渡されるArgsは infoに配列として入っている const std::initializer_list<napi_value> initArgList = {info[0]}; // ここでWrapper:::Wrapper()が呼ばれる Napi::Object obj = env.GetInstanceData<Napi::FunctionReference>()->New(initArgList); // gcにメモリ解放されないようにスコープを除外する return scope.Escape(napi_value(obj)).ToObject(); } // メンバ関数のバインド Napi::Object Wrapper::Init(Napi::Env env, Napi::Object exports) { Napi::Function func = DefineClass( env, "Wrapper", { // ここにメンバ関数を登録する InstanceMethod("getNum", &Wrapper::getNum), // InstanceMethod("jsから呼び出す際の関数名", "呼び出したいC++メンバ関数名"), }); Napi::FunctionReference *constructor = new Napi::FunctionReference(); *constructor = Napi::Persistent(func); env.SetInstanceData(constructor); exports.Set("Wrapper", func); return exports; } // ---------------------------------------------------------- // // --------------- Wrapperクラスの定義はこれより下 --------------- // // ---------------------------------------------------------- // // コンストラクタ Wrapper::Wrapper(const Napi::CallbackInfo &info) : Napi::ObjectWrap<Wrapper>(info) { m_value = 0.0; }; Wrapper::~Wrapper(){}; // メンバ関数 Napi::Value Wrapper::getNum(const Napi::CallbackInfo &info) { Napi::Env env = info.Env(); return Napi::Number::New(env, this->m_value); }
- 補足
- wrapper.cpp内の関数
Napi::Function func = DefineClass()
において、自作のC++メンバ関数を登録する必要があります。サンプルコードInstanceMethod("getNum", &Wrapper::getNum),
のように、InstanceMethod("jsから呼び出す際の関数名", "呼び出したいC++メンバ関数名")
として登録しなければなりません。jsとの結合用cppファイルを作る
- 次に、Wrapperクラスをjsモジュールとしてエクスポートするための処理をaddon.ccに記述します。
- こちらも詳細説明は省略します。テンプレとしてお使いください。
addon.cc#include <napi.h> #include "wrapper.h" #include <iostream> // jsオブジェクトが初期化された時 new()の呼び出し Napi::Object CreateObject(const Napi::CallbackInfo& info) { return Wrapper::NewInstance(info.Env(), info); } // js内でexport()が呼び出されたとき Napi::Object InitAll(Napi::Env env, Napi::Object exports) { // 関数定義 Napi::Object new_exports = Napi::Function::New(env, CreateObject); return Wrapper::Init(env, new_exports); } // jsへバインドするためのマクロ // jsで、 export('bindings')('addon')と記述したとき、上記のInitAll()が呼び出される NODE_API_MODULE(addon, InitAll)3. Cppファイルをビルドする
- 作成してたcppをビルドするための設定ファイルを作ります。
- binding.gyp というファイルです。
- VisualStudioのprojectのプロパティ設定、CMakeLists.txtと似たような設定をします。
binding.gypの追加
カレントディレクトリ
├── node_modules
├── package-lock.json
├── package.json
├── index.js
├── addon.cc
├── wrapper.cc
├── wrapper.h
└── binding.gyp <-- 新規作成binding.gyp{ "targets": [ { # ↓addon.cc内の NODE_API_MODULE(addon, InitAll) と同名にする "target_name": "addon", "cflags!": [ "-fno-exceptions" ], "cflags_cc!": [ "-fno-exceptions" ], # ↓必要な.ccファイルを全て記述する "sources": [ "addon.cc", "wrapper.cc"], "include_dirs": [ "<!@(node -p \"require('node-addon-api').include\")" ], "defines": [ 'NAPI_DISABLE_CPP_EXCEPTIONS' ], } ] }
- 上記ファイルをコピペいただければ問題ないです。
- 注意点として sources セクションには、使用する .cc (or .cpp) 拡張子のファイルを全て登録してください。
ビルド実行
- 下記コマンドを実行し、ビルドしてください。
terminal$ npm install . >> gyp info ok と表示されればビルド完了4. JavaScriptからビルドしたCppクラスを使う
- お待たせしました。最後に index.jsから wrapper.ccのクラスを使ってみましょう。
index.jsの書き換え
カレントディレクトリ
├── node_modules
├── package-lock.json
├── package.json
├── index.js <-- 書き換え
├── addon.cc
├── wrapper.cc
├── wrapper.h
└── binding.gyp
- index.jsを以下のように書き換えてください。
index.js// addon.cc内の NODE_API_MODULE(addon, InitAll) が呼ばれる var Wrapper = require('bindings')('addon'); // addon.cc内の CreateObject() が呼ばれる var obj = new Wrapper() // wrapper.cc内で登録した getNum()が呼ばれる console.log(obj.getNum());index.jsの実行
ターミナルで次のように実行します。
terminal$ node . >> 0 と表示されれば成功 >> Wrapper.m_valueの値が表示されています。お疲れ様でした。
node-addon-api は"お約束ごと"が多いので、
私はこの記事のようなテンプレを作り、使いまわしています。
参考になれば幸いです。参考リンク
- ドキュメント
- node-addon-examples
- 本格的に学びたい方は、このexampleを順にやっていくと良いでしょう
Others
- コンストラクタに複数の引数を渡す
- メンバ関数の引数、返り値について
- 別のC++クラスを利用する
- 投稿日:2021-02-13T12:08:35+09:00
M1 MacでAzure Functions Core Tools (Node.js版)が動かない件 (※ワークアラウンド)
はじめに
LINE botをVS Codeで開発してAzure Functionsへデプロイするメモ (Node.js版)
を書いたのですが、M1 Macで実行したところローカル環境で動作しませんでした。Start Debugging[F5]で実行するとgrpc関連でエラーが出力されます。
[2021-02-13T01:32:12.816Z] Debugger listening on ws://127.0.0.1:9229/f28a6986-09d1-4104-aae4-aa4ce42a3c82 [2021-02-13T01:32:12.824Z] For help, see: https://nodejs.org/en/docs/inspector [2021-02-13T01:32:12.824Z] internal/modules/cjs/loader.js:883 [2021-02-13T01:32:12.824Z] throw err; [2021-02-13T01:32:12.824Z] ^ [2021-02-13T01:32:12.825Z] Error: Cannot find module './Worker.js' [2021-02-13T01:32:12.827Z] Expected directory: node-v83-darwin-arm64-unknown [2021-02-13T01:32:12.827Z] Found: [node-v57-darwin-ia32-unknown, node-v57-darwin-x64-unknown, node-v57-linux-ia32-glibc, node-v57-linux-x64-glibc, node-v57-win32-ia32-unknown, node-v57-win32-x64-unknown, node-v64-darwin-ia32-unknown, node-v64-darwin-x64-unknown, node-v64-linux-ia32-glibc, node-v64-linux-x64-glibc, node-v64-win32-ia32-unknown, node-v64-win32-x64-unknown, node-v72-darwin-ia32-unknown, node-v72-darwin-x64-unknown, node-v72-linux-ia32-glibc, node-v72-linux-x64-glibc, node-v72-win32-ia32-unknown, node-v72-win32-x64-unknown, node-v83-darwin-ia32-unknown, node-v83-darwin-x64-unknown, node-v83-linux-ia32-glibc, node-v83-linux-x64-glibc, node-v83-win32-ia32-unknown, node-v83-win32-x64-unknown] [2021-02-13T01:32:12.827Z] Exceeded language worker restart retry count for runtime:node. Shutting down and proactively recycling the Functions Host to recover [2021-02-13T01:32:12.828Z] This problem can often be fixed by running "npm rebuild" on the current system [2021-02-13T01:32:12.828Z] Original error: Cannot find module '/opt/homebrew/Cellar/azure-functions-core-tools@3/3.0.3284/workers/node/grpc/src/node/extension_binary/node-v83-darwin-arm64-unknown/grpc_node.node' [2021-02-13T01:32:12.829Z] Require stack: [2021-02-13T01:32:12.829Z] - /opt/homebrew/Cellar/azure-functions-core-tools@3/3.0.3284/workers/node/worker-bundle.js [2021-02-13T01:32:12.829Z] - /opt/homebrew/Cellar/azure-functions-core-tools@3/3.0.3284/workers/node/dist/src/nodejsWorker.js [2021-02-13T01:32:12.831Z] Require stack: [2021-02-13T01:32:12.831Z] - /opt/homebrew/Cellar/azure-functions-core-tools@3/3.0.3284/workers/node/dist/src/nodejsWorker.js [2021-02-13T01:32:12.831Z] at Function.Module._resolveFilename (internal/modules/cjs/loader.js:880:15) [2021-02-13T01:32:12.831Z] at Function.Module._load (internal/modules/cjs/loader.js:725:27) [2021-02-13T01:32:12.831Z] at Module.require (internal/modules/cjs/loader.js:952:19) [2021-02-13T01:32:12.831Z] at require (internal/modules/cjs/helpers.js:88:18) [2021-02-13T01:32:12.869Z] at Object.<anonymous> (/opt/homebrew/Cellar/azure-functions-core-tools@3/3.0.3284/workers/node/dist/src/nodejsWorker.js:45:14) [2021-02-13T01:32:12.869Z] at Module._compile (internal/modules/cjs/loader.js:1063:30) [2021-02-13T01:32:12.869Z] at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10) [2021-02-13T01:32:12.869Z] at Module.load (internal/modules/cjs/loader.js:928:32) [2021-02-13T01:32:12.875Z] at Function.Module._load (internal/modules/cjs/loader.js:769:14) [2021-02-13T01:32:12.875Z] at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:72:12) { [2021-02-13T01:32:12.875Z] code: 'MODULE_NOT_FOUND', [2021-02-13T01:32:12.875Z] requireStack: [ [2021-02-13T01:32:12.875Z] '/opt/homebrew/Cellar/azure-functions-core-tools@3/3.0.3284/workers/node/dist/src/nodejsWorker.js' [2021-02-13T01:32:12.875Z] ] [2021-02-13T01:32:12.875Z] }調べてみたところ本家でもissueが上がっていました。
https://github.com/Azure/azure-functions-core-tools/issues/2431原因は出力内容からnode-v83-darwin-arm64-unknownが存在しないためです。
手動でgrpcモジュールをインストールしてリンクを貼れば動作しました。% npm init -y % npm install grpc % ln -s `pwd`/node_modules/grpc/src/node/extension_binary/node-v83-darwin-arm64-unknown /opt/homebrew/Cellar/azure-functions-core-tools@3/3.0.3284/workers/node/grpc/src/node/extension_binary
- 投稿日:2021-02-13T11:55:32+09:00
50行でちゃんと動くリマインダーアプリ「Notification-CLI」をリリースしました
50行でちゃんと動くリマインダーアプリ「Notification-CLI」をリリースしました
以下かんたんな日本語版クイックスタートになります。
Minimalistic Command Line Notification Application under 50 Lines
50行で実装のシンプルなコマンドラインベースのリマインダーアプリケーション。
クイックスタート Quick Start
This will nofity you when 2021 February 20, PM 6:00.
以下で2021年2月20日18時に通知をします。
./noc.js -d 2021022018 --desktopVery much minimalistic.
とてもミニマリスティック。
インストール Installation
git clone https://github.com/yuis-ice/notification-cli.git cd notification-cli chmod 755 ./noc.js npm i必要環境 Requirements
- node.js v13.10.1 or higher
# node.js [nvm-sh/nvm](https://github.com/nvm-sh/nvm) curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.37.2/install.sh | bash bash nvm install v13.10.1 node -v使用例 Examples
This will only notify you by command line console output:
コマンドライン出力で通知します。
./noc.js -d 2021022018The output be like:
出力はこんな感じです。
$ ./noc.js -d 202102120234 2021-02-12T02:33:16+09:00 Jobs started... 2021-02-12T02:33:16+09:00 You will be notified at: 2021-02-12T02:34:00+09:00 2021-02-12T02:34:00+09:00 Notified. $You can abbreviate your seconds, minutes, hours and so on:
秒、分などを省略しても動きます。
./noc.js -d 2022This will notify you when PM 11:00 in the day:
その日の23時になったら通知します。
./noc.js -d 23 --format HHAn alias makes your code much more minimalistic:
エイリアスを定義すると、よりシンプルになります。
alias notify="./noc.js --format MM,DD,HH" # Specifying absolute path recommended notify -d 2,20,18This will run your command:
シェルコマンド、外部コマンドも実行できます。
./noc.js -d 2021022018 -c "firefox.exe sound.mp3"No logs on background be like:
バックグラウンドでマルチインスタンスに使用するのもいいですね。
./noc.js -d 2021022018 --desktop --hide-log --log "" &My favorite format <3
僕のお気に入り。
./noc.js -d 2021022018 -c "firefox.exe sound.mp3" --desktop -t "your appointment soon"コマンドラインオプション Command line Options
$ ./noc.js Usage: notification-cli [options] Options: -d, --date <date> specify date to fire (e.g. "2022010100" for 2020/1/1 00:00) (default: null) -f, --format <format> specify date format (default: "YYYYMMDDHHmmss") -D, --desktop enables desktop notification -t, --title <text> specify title (default: "Notification-CLI") -m, --message <text> specify message (default: ":)") -c, --exec-command <command> specify command to run (e.g. firefox.exe ringtone.mp3) (default: null) -l, --log <text> specify console log message (default: "Notified.") -H, --hide-log hide infomation logs -h, --help display help for command一言
コードを書くよりreadmeを書く時間のほうが長くなってしまった例。
momentjsとnode-scheduleのおかげでimportやコマンドラインオプション部分を除くメインのコードはなんと10行程度で収まっています。
ソースコードを見られたら少し恥ずかしいかもしれない。対してソースコードのシンプリシティは初学者にとってそのコードを研究する敷居を低くしてくれると思うので、node.jsあるいはプログラミング初学者の人に積極的に参考にして頂ければ嬉しいと思う。