ネットを彷徨っていて「へー」といたく感心した記事。
『Native Messaging を使い、ブラウザ拡張でプロセス間通信を行う方法』
Native Messaging を用いると、Web ブラウザとは別に、ユーザーのPCにインストールされたネイティブアプリケーションとブラウザ拡張の間でメッセージ交換を行うことができます。つまり、Web ブラウザに用意された API ではアクセスできないハードウェアなどのリソースに対して、ブラウザ拡張からアクセスできるようになります。
こういう技術があるのは知っていたが、詳しい知識はゼロ。

拡張機能を持っている各種ブラウザなら適用可能な技術なようだが、chorome ユーザーが多いと思うので今回は chrome 拡張で考えてみる。
ところで・・
なぜ、Native Messaging が必要か
と思う人もいるかもしれない。
ワイもブラウザ関連の技術はそれほど詳しくないので上手い説明はできないが、簡単にいうなら「サイトページなどと連動して動く JavaScript は、基本的に PC 本体のリソースを使うことが制限されているから」ということになる。
というか、なんの制限もなく操作できたら困る。
昨今では、ネットを介して家電の操作を行う、なんてことが当たり前にできるようになったが、これがブラウザ経由で簡単にできてしまったら、帰宅時に家中めちゃくちゃなんて事態が生じてしまう。
家電ぐらいなら可愛いもので、USB を介して産業システムを制御しているような PC が悪意あるページから操作されてしまったら、社会レベルでかなり大変なことになる。
だが、その一方で、これだけネットが普及してしまうと、各種ネットサービスを提供するにあたってブラウザから PC のリソースを操作したいという要求は強く存在する。
実際、WebUSB のような規格も制定されつつあるのだが、各種機能が消えたり復活したりと仕様がなかなか一定しない。
それで Native Messaging のような技術が必要とされている、ということなんだろう。
最近だとマイナ関係のサイトで、この技術は結構使われているようだ。
今は過渡期なんでしょう。
話を元に戻そう。
まずは環境の構築。
準備
ネイティブアプリを呼び出す chrome 拡張とネイティブアプリの開発環境、それとサーバー機能のあるテスト環境が必要そうなので、以下のようなフォルダをまず作成。

opj-c フォルダで呼び出されるネイティブアプリの開発、
browser フォルダで chrome 拡張の開発、
testserver にテスト環境を整備、
という意図。
こうすれば、初心者でも混乱しないだろう。
testserver に関しては、今回は『イマドキのフロントエンド開発』で紹介されている node – webpack のものを使った。
なお、このときのエントリポイント index.js は空で構わない。
環境はバッチリなんだけどさ
環境を整えた後、ネット上のサンプルなどを試した・・・
のだが、見事なまでに動かないね(笑)。
これには理由があって
・現在の manifest.json のバージョンアップ(2 → 3)が 2024/11 と間近に迫っていて、現行の chrome は 3 推奨
・3 にすると書き方はけっこう変わるらしく従来の書き方ではエラーが出る
という背景がある。
とりあえず、「動く」chrome 拡張の manifest.json の書き方を提示しておくと
{
"name": "Sample",
"version": "1.0.0",
"manifest_version": 3,
"description": "Sample Chrome Extension",
"background": {
"service_worker": "background.js"
},
"permissions": [ "nativeMessaging"],
"content_scripts": [{
"matches": ["http://localhost:3000/*"],
"js": [
"content.js"
]
}]
}
これを、browser フォルダに入れておく。
chrome 拡張に登録する際には、browser フォルダを指定し、デバッグなどは testserver 内で立ち上げた node 由来のサーバの指定のページで行うことができる。
ちなみに、2 → 3 で大きく変わったのは、 service_worker という書き方。
これに関しては google 公式が『拡張機能 Service Worker について』という記事を公開しているので、目を通しておきましょう。
また、Service Worker を使う API も公式がドキュメントを公開してくれている。
host 側アプリ
操作するホスト PC 側のアプリだが、何でもいいというものではなく、入出力は標準入出力に限定されている。(ここら辺も上述の「ブラウザから PC の操作は制限されている」という影響でてますね)
だから、メッセージのやり取りは stdio(C の場合)で行う必要がある。
Java であれば System.in あたりを使う。
適当に作成してみる。
もっとも、アプリは C や Java で作成した「いかにも」なものでなくてもよく、シェルスクリプトでもいいらしいのだが。
参照:『Google ChromeでトレイからCDを排出できるブラウザ拡張をつくりました(Native messaging版)』
ホスト側アプリは、細かいことを気にしなければ、こんなものでも動く。
#include <stdio.h>
#include <limits.h>
int main(){
uint8_t *input;
uint8_t *buffer;
if((input = (uint8_t *)fgets((char *)buffer,INT_MAX,stdin)) != NULL){
//確認用
//for(int i=0;i<sizeof(input);i++){
// printf("input data is %x", input[i]);
//}
}
fwrite(input, sizeof(input), INT_MAX, stdout);
}
stdin で入ってきたバイト列をそのまま stdout で返しているだけ。
これでも chrome 拡張側(以下のコード)は反応してくれた。

ただ、仕様には全くあっていない。JavaScript は見事なまでに response を undefined と認識している(笑)。

ホストアプリからのメッセージは、正しくは
32byte ヘッダ(以下のデータ長) + base64 でエンコードされた JSON データ
のバイト列で返さないといけないらしい。
→と書いていたが、以下のコードのネイティブアプリからのレスポンス {“message”:”sign”} を Service Worker は正しく読んでいたので、必ずしも base64 でエンコードする必要はなさそうだ。
どうやら、ブラウザとホストアプリは Native Messaging を介してお話できているようですね。
もうちょっとちゃんと書くかと思っていたら、それ用のテストコードを書いている人がいたので、ちょっぴり改変して以下に掲げておく。(しかし、このコードはなかなか教育的ですね)
#include <stdio.h>
#include <limits.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
uint8_t* read_input()
{
uint32_t length;
size_t count;
int err;
count = fread(&length, sizeof(uint32_t), 1, stdin);
if (count != 1)
{
if (feof(stdin))
{
fprintf(stderr, "Unexpectedly encountered EOF while reading file\n");
}
else if ((err = ferror(stdin)))
{
fprintf(stderr, "An error occured while reading file, err code: %d\n", err);
clearerr(stdin);
}
return NULL;
}
uint8_t* value = malloc((length + 1) * sizeof(*value));
if (value == NULL)
{
fprintf(stderr, "An error occured while allocating memory for value");
return NULL;
}
count = fread(value, sizeof(*value), length, stdin);
if (count != length)
{
if (feof(stdin))
{
fprintf(stderr, "Unexpectedly encountered EOF while reading file\n");
}
else if ((err = ferror(stdin)))
{
fprintf(stderr, "An error occured while reading file, err code: %d\n", err);
clearerr(stdin);
}
free(value);
return NULL;
}
return value;
}
size_t write_output(const uint8_t* const value, uint32_t length)
{
size_t count;
int err;
if (length > (1024 * 1024)) {
fprintf(stderr, "Message too large");
return 0;
}
count = fwrite(&length, sizeof(uint32_t), 1, stdout);
if (count != 1)
{
if (feof(stdout))
{
fprintf(stderr, "Unexpectedly encountered EOF while reading file\n");
}
else if ((err = ferror(stdout)))
{
fprintf(stderr, "An error occured while reading file, err code: %d\n", err);
clearerr(stdout);
}
return 0;
}
count = fwrite(value, sizeof(char), length, stdout);
if (count != length)
{
if (feof(stdout))
{
fprintf(stderr, "Unexpectedly encountered EOF while writing file\n");
}
else if ((err = ferror(stdout)))
{
fprintf(stderr, "An error occured while writing file, err code: %d\n", err);
clearerr(stdout);
}
return 0;
}
fflush(stdout);
return length + 4;
}
int main()
{
size_t count;
uint8_t* val = read_input();
if (val == NULL)
{
exit(EXIT_FAILURE);
}
sleep(3);//add akiba-chan
count = write_output((uint8_t*)"{\"msg\":\"sign\"}", 14);
if (count != 18)
{
free(val);
exit(EXIT_FAILURE);
}
free(val);
}
「ちょっぴり改変」というのは、 main 関数内の sleep(3) の追加。
PC ハードのリソースを使う場合、大抵の場合、ソフト的な処理に比べ時間がかかる。
そのためリクエストを送ってからレスポンスが返ってくるまで秒単位の時間を設定できるようにした。
(x 秒待ちたいときは sleep(x) とする)
実際、マイナ(PKI)カードの署名処理では数秒単位の時間がかかる。
(続く)