javascript

От клика до железа: хроника одного запроса. Часть 1

  • пятница, 25 апреля 2025 г. в 00:00:05
https://habr.com/ru/companies/nspk/articles/897598/

Введение

Увлекались ли вы когда-нибудь задачей так сильно, что полностью выпадали из жизни? Я — да. Писал код, разбирался с нюансами, тестировал, переделывал, снова тестировал… В какой-то момент мой друг, давно не слышавший обо мне, решил узнать, куда я пропал. Мы созвонились, и я рассказал, чем занимаюсь. Он послушал, усмехнулся: «Как же хорошо, что я выбрал бэкенд-разработку».

На самом деле ничего сверхъестественного в этой задаче не было. Но и простой её тоже не назовёшь — архитектура сложилась под влиянием множества ограничений: браузер не может напрямую запускать exe-файл, бэкенд не имеет доступа к локальному оборудованию, а взаимодействие между всеми этими частями нужно было выстроить чётко и последовательно.

Всё это заставляет внимательнее смотреть на возможности, которые предоставляет среда браузера. Chrome-расширения работают в строго изолированном контексте, JavaScript не имеет доступа к файловой системе или системным вызовам, и для связи с нативным приложением приходится учитывать ряд особенностей: протокол обмена, формат сообщений, правила безопасности и другие детали, которые легко упустить, если не сталкивался с этим раньше.

В этой статье я расскажу, как построить такую связку с помощью механизма Native Messaging: от интерфейса в браузере до запуска локального exe. Разберём архитектуру, покажу, какие задачи решает этот подход, и напишем рабочий пример — расширение, которое сможет общаться с программой на C.

Так что устраивайтесь поудобнее и давайте разбираться.

Ниже — структура статьи, чтобы вы могли сразу перейти к интересующим вас разделам.

Мотивация

(Если вам не интересен контекст и вы хотите сразу перейти к практике, можете пропустить этот раздел.)

Прежде чем перейти к техническим деталям, давайте разберёмся в мотивации. Почему вообще возникла необходимость использовать Chrome-расширение и механизм Native Messaging?

Во-первых, это не первый случай, когда в проекте, над которым я работаю, применяют эту технологию. Ещё до моего прихода подобное решение уже использовалось для другого бизнес-кейса. Тогда потребовалось связать между собой несколько компонентов:

  • Бэкенд, который отвечает за управление процессом,

  • WebSocket, обеспечивающий коммуникацию,

  • Фронтенд, обрабатывающий сообщения от WebSocket,

  • Chrome-расширение, которое передаёт команды локальному приложению,

  • Нативное приложение, взаимодействующее с железом,

  • И само оборудование, с которым, собственно, всё и работает.

рис. 1 - Диаграмма последовательности реализованного решения (Альфа).
рис. 1 - Диаграмма последовательности реализованного решения (Альфа).

Этот механизм доказал свою надёжность: весь процесс мог длиться до 48 часов, при этом все его звенья работали чётко и синхронно.

Теперь о текущей задаче. В этот раз речь идёт об интеграции с программно-аппаратным комплексом, предназначенным для выполнения определённых операций с физическими носителями данных. Это не просто железка, а серьёзное оборудование, которое стоит недёшево.

Почему именно этот подход?

Казалось бы, логично было бы организовать всё через бэкенд: просто отправлять команды с сервера напрямую на оборудование. Однако это невозможно.

Проблема в том, что бэкенд и конечное устройство находятся в разных сегментах сети.

  • Изменить сетевую архитектуру нельзя – корпоративная политика безопасности запрещает любые изменения, в том числе открытие дополнительных портов или обходные решения через VPN.

  • Бэкенд не может напрямую взаимодействовать с оборудованием, потому что оно физически недоступно из его сегмента сети.

Поэтому пришлось искать альтернативный путь. Решение? Использовать Chrome-расширение как посредника.

Как это работает?

  • Пользователь в веб-интерфейсе нажимает кнопку.

  • Происходит HTTP-запрос на бэкенд, который создаёт сессию и отправляет её ID в ответе.

  • Фронтенд получает этот ID и передаёт его в Chrome-расширение.

  • Chrome-расширение запускает нативное приложение и передаёт ему ID сессии.

  • Нативное приложение взаимодействует с оборудованием через DLL (для Windows) или .so (Shared Object) для Unix-подобных систем, подключаемые с помощью JNA.

  • Результаты по цепочке передаются обратно на бэкенд.

рис. 2 - Диаграмма последовательности нового бизнес-кейса (Бета).
рис. 2 - Диаграмма последовательности нового бизнес-кейса (Бета).

Выбор технологий

Так как для работы с оборудованием предоставляется набор динамических библиотек (DLL для Windows, .so для Linux/macOS), взаимодействовать с ним можно через C, C++ или C#. В комплекте есть заголовочные файлы и описание API, так что технически всё выглядит достаточно просто: вызываешь нужные методы – получаешь результат.

Для основной реализации нативного приложения был выбран Java. Основная причина — кроссплатформенность. Java позволяет запускать приложение на разных операционных системах без необходимости перекомпилировать код для каждой платформы.

Однако, поскольку в первой части статьи основная цель — объяснить базовые принципы Native Messaging, начнём с более простого примера и напишем простое приложение на C.

Выбор архитектуры был обусловлен не желанием усложнить процесс, а объективными ограничениями инфраструктуры. Chrome-расширение в связке с Native Messaging – это не костыль, а проверенное решение, которое уже однажды показало свою надёжность. Теперь пора разбираться, как это работает.

Что такое Native Messaging и как оно работает?

Native Messaging — это механизм взаимодействия Chrome-расширения с локальным нативным приложением. Его ключевая особенность — использование стандартного ввода/вывода (stdin/stdout) для передачи данных, без открытия сетевых соединений или работы с файловой системой.

Этот механизм предоставляет гибкость, но накладывает ряд ограничений, о которых стоит знать.

Ограничения протокола Native Messaging

1. Передача данных только через JSON

Native Messaging поддерживает только текстовые сообщения в формате JSON. Двоичные данные передавать нельзя, их нужно либо кодировать (например, в Base64), либо сохранять в файл и передавать путь.

2. Фиксированный формат сообщений

Каждое сообщение должно начинаться с 4-байтового заголовка, указывающего его длину, а затем следовать JSON-данные. Это требование жёсткое, и несоблюдение формата приведёт к отказу в обработке сообщения.

Рис. 3 - Структура сообщения Native Messaging.
Рис. 3 - Структура сообщения Native Messaging.

3. Нет прямого управления процессами

Chrome не управляет жизненным циклом нативного приложения. Оно должно самостоятельно завершаться после обработки запроса, если не используется долговременное соединение (сохранение потока открытым).

4. Ограничения по безопасности

  • Нативное приложение не может быть частью расширения — его нужно устанавливать отдельно.

  • Расширение может взаимодействовать только с зарегистрированными нативными приложениями, указанными в манифесте.

  • Нативное приложение не может запустить Chrome-расширение — только наоборот.

5. Ограничение на размер сообщения

  • Сообщение от расширения к нативному приложению не должно превышать 1 МБ.

  • Сообщение от нативного приложения обратно ограничено 4 МБ.

  • Если необходимо передавать большие объёмы данных, можно использовать:

    • Файлы — данные записываются на диск, а расширение получает ссылку на них.

    • Чанки — большие сообщения разбиваются на части и отправляются последовательно, а затем собираются на принимающей стороне.

Рис.4 - Передача данных чанками.
Рис.4 - Передача данных чанками.

Разработка Chrome-расширения

Прежде чем перейти к коду, разберёмся в условиях, в которых будет работать наше расширение.

Обычно взаимодействие с Chrome-расширением происходит через popup.html — всплывающее окно, которое открывается при нажатии на иконку расширения. Однако в нашем случае взаимодействие будет осуществляться из внешней веб-страницы.

Это накладывает несколько особенностей:

  • Веб-страница не может напрямую взаимодействовать с расширением, поэтому потребуется использовать content script или механизм chrome.runtime.connect.

  • Chrome-расширение должно получить разрешение на доступ к странице в манифесте.

  • Для передачи данных между веб-страницей, content script и background script потребуется Message Passing API.

Рис. 5 - Взаимодействие внешней веб-страницы с Chrome-расширением
Рис. 5 - Взаимодействие внешней веб-страницы с Chrome-расширением

Что такое Message Passing API?

Рис.6 - Иллюстрация работы компонентов Chrome-расширения
Рис.6 - Иллюстрация работы компонентов Chrome-расширения

"Представьте три комнаты, в каждой из которых происходит что-то своё: в одной работает веб-страница, в другой — content script, а в третьей — background script. Дверей между ними нет, и они не могут просто взять и передать друг другу файлы или команды. Но есть телефонная связь — это и есть Message Passing API."

Чтобы эти комнаты могли взаимодействовать, используется телефонная система:

  • Content Script может звонить в другие комнаты и передавать информацию, но не может сам инициировать важные процессы.

  • Background Script получает звонки и может выполнить команду, но сам не знает, когда его вызовут.

  • Injected Script (myExtensionApi.js) — это как секретарь, который набирает номер нужной комнаты и передаёт сообщения.

Без Message Passing API эти комнаты существовали бы отдельно, не имея возможности координировать свою работу.

Файловая структура расширения

Теперь, когда понятно, как части расширения общаются друг с другом, разберёмся с его файловой структурой:

Рис. 7- Файловая структура расширения
Рис. 7- Файловая структура расширения
  • background.js – фоновый скрипт расширения.

    • Запускается в фоне и работает как Service Worker.

    • Обрабатывает сообщения от content script и native messaging host.

    • Может выполнять долгосрочные задачи, но не работает постоянно – Chrome выгружает его из памяти, когда он не используется.

  • content.js – контентный скрипт.

    • Встраивается в веб-страницу и взаимодействует с её DOM.

    • Передаёт команды и данные между веб-страницей и background.js.

    • Не может напрямую использовать Chrome API, но может отправлять сообщения в background.js.

  • myExtensionApi.js – API-файл, доступный для веб-страницы.

    • Подключается к странице как обычный JS-скрипт.

    • Создаёт объект window.myExtension, через который веб-страница может вызывать функции расширения.

    • Передаёт запросы в content.js, который затем пересылает их в background.js.

  • manifest.json – основной конфигурационный файл расширения.

    • Определяет права и доступ к API Chrome.

    • Указывает, какие файлы являются background script и content script.

    • Описывает web_accessible_resources, чтобы myExtensionApi.js мог быть загружен на веб-странице.

  • icons/ – папка с иконками расширения.

    • Содержит icon.png и, возможно, другие размеры (icon16.png, icon48.png, icon128.png).

    • Используется для отображения в панели расширений Chrome и в настройках.

Практика

manifest.json

Начнём с manifest.json. Это файл, без которого Chrome-расширение просто не запустится. Он описывает всё:

  • Какое поведение у расширения

  • Какие файлы в него входят

  • Какие разрешения ему нужны

Можно сказать, что это паспорт нашего расширения. Давай взглянем на его содержимое:

{
  "name": "My Chrome Extension",
  "version": "1.0",
  "manifest_version": 3,
  "description": "Расширение для взаимодействия с нативным приложением через Native Messaging.",
  "permissions": [
    "nativeMessaging"
  ],
  "background": {
    "service_worker": "background.js"
  },
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content.js"],
      "run_at": "document_start"
    }
  ],
  "web_accessible_resources": [
    {
      "resources": ["myExtensionApi.js"],
      "matches": ["<all_urls>"]
    }
  ],
  "host_permissions": [
    "<all_urls>"
  ],
  "icons": {
    "16": "icons/icon.png",
    "48": "icons/icon.png",
    "128": "icons/icon.png"
  }
}

Что здесь важно?

1. "permissions": ["nativeMessaging"]

Это ключевой момент. Chrome по умолчанию запрещает расширениям общаться с нативными приложениями. Чтобы это разрешить, в манифесте надо прописать "nativeMessaging".

2. "background": { "service_worker": "background.js" }

В Manifest V3 фоновый скрипт больше не работает постоянно. Вместо него используется Service Worker, который просыпается только по событию. Это сделано ради производительности, но накладывает ограничения:

  • Он засыпает, если не активен.

  • В нём нельзя использовать setTimeout и setInterval (нужно alarms).

3. "content_scripts"

Вот тут интересно:

  • "matches": ["<all_urls>"] означает, что content.js будет загружаться на всех сайтах.

  • "run_at": "document_start" позволяет загружать скрипт до того, как страница полностью загрузится.

4. "web_accessible_resources"

Chrome не разрешает веб-страницам просто так загружать файлы расширения.
Эта строка позволяет использовать myExtensionApi.js, чтобы веб-страница могла взаимодействовать с расширением.

5. "icons"

Не так важно для работы, но полезно для интерфейса. Без иконки расширение выглядит… как кусок кода без лица.

background.js

Фоновый скрипт отвечает за обработку запросов от content script и передачу данных нативному приложению через Native Messaging API.

В данном случае используется разовое соединение через chrome.runtime.sendNativeMessage(), когда расширению нужно выполнить один запрос и получить ответ без поддержания постоянного соединения.

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
    if (request.action === "sendToNative") {
        chrome.runtime.sendNativeMessage(
            "my.native.app",
            request.data,
            (response) => {
                if (chrome.runtime.lastError) {
                    console.error("Ошибка при общении с нативным приложением:", chrome.runtime.lastError.message);
                    sendResponse({ error: chrome.runtime.lastError.message });
                } else {
                    console.log("Ответ от нативного приложения:", response);
                    sendResponse(response);
                }
            }
        );
        return true; // Указываем, что sendResponse будет вызван асинхронно
    }
});

Как это работает?

1. chrome.runtime.onMessage.addListener()

Слушает сообщения от content.js и передаёт их в нативное приложение.

2. chrome.runtime.sendNativeMessage()

Отправляет разовое сообщение без установления постоянного соединения. Получает ответ и передаёт его обратно в content.js.

3. Обработка ошибок

Если что-то пошло не так, Chrome передаст chrome.runtime.lastError, который мы логируем.

4. return true;

Это необходимо, чтобы sendResponse() работал асинхронно.

Теперь background.js готов. Следующий шаг — content.js, который будет передавать команды в background script.

content.js

Контентный скрипт загружается в контексте веб-страницы, но работает изолированно. Он не может напрямую обращаться к API Chrome, но может:

  • Слушать команды от веб-страницы

  • Передавать их в background.js

  • Получать ответ и отправлять его обратно на страницу

window.addEventListener("message", (event) => {
    if (event.source !== window  !event.data  event.data.type !== "FROM_PAGE") {
        return;
    }

    console.log("Получен запрос от веб-страницы:", event.data);

    // Отправляем сообщение в background.js
    chrome.runtime.sendMessage(
        { action: "sendToNative", data: event.data.payload },
        (response) => {
            console.log("Ответ от background.js:", response);
            // Отправляем ответ обратно на веб-страницу
            window.postMessage({ type: "FROM_EXTENSION", payload: response }, "*");
        }
    );
});

Как это работает?

1. Слушаем события от веб-страницы

window.addEventListener("message", ...) перехватывает сообщения, отправленные через window.postMessage(). Проверяем, что сообщение пришло от той же страницы и содержит type: "FROM_PAGE".

2. Передаём сообщение в background.js

Используем chrome.runtime.sendMessage(), отправляя объект { action: "sendToNative", data: event.data.payload }.

3. Ждём ответ и отправляем обратно на страницу

Когда background.js получает ответ от нативного приложения, мы его логируем и передаём обратно на веб-страницу через window.postMessage().

Почему используется window.postMessage()?

Контентный скрипт не может напрямую взаимодействовать с объектами страницы (например, window.myExtension). Единственный способ передавать данные между ними — это window.postMessage(), который создаёт своего рода "туннель" между разными контекстами JavaScript.

Теперь content.js готов. Следующий шаг — myExtensionApi.js, который упростит взаимодействие веб-страницы с расширением.

myExtensionApi.js

Этот файл загружается на веб-странице и создаёт объект window.myExtension, предоставляющий удобные методы для общения с расширением. Вместо того чтобы веб-страница напрямую отправляла сообщения через window.postMessage(), мы инкапсулируем логику в отдельный API.

(() => {
    if (window.myExtension) {
        console.warn("myExtension уже определён.");
        return;
    }

    window.myExtension = {
        sendMessageToNative: (message) => {
            return new Promise((resolve, reject) => {
                const responseHandler = (event) => {
                    if (event.source !== window  !event.data  event.data.type !== "FROM_EXTENSION") {
                        return;
                    }
                    window.removeEventListener("message", responseHandler);

                    if (event.data.payload && event.data.payload.error) {
                        reject(new Error(event.data.payload.error));
                    } else {
                        resolve(event.data.payload);
                    }
                };

                window.addEventListener("message", responseHandler);

                window.postMessage({ type: "FROM_PAGE", payload: message }, "*");

                // Таймаут на случай, если ответа нет
                setTimeout(() => {
                    window.removeEventListener("message", responseHandler);
                    reject(new Error("Время ожидания ответа от расширения истекло"));
                }, 5000);
            });
        }
    };

    console.log("myExtension API загружен.");
})();

Как это работает?

1. Проверяем, не загружен ли API дважды

Если window.myExtension уже существует, ничего не делаем, чтобы избежать конфликтов.

2. Определяем метод sendMessageToNative(message)

При вызове он отправляет сообщение через window.postMessage() в content.js.

Ожидает ответа, используя Promise.

3. Добавляем обработчик ответа

window.addEventListener("message", ...) ловит сообщения от content.js.

Когда приходит type: "FROM_EXTENSION", мы передаём данные в resolve().

4. Добавляем защиту от зависания

Если за 5 секунд не приходит ответа, reject() завершает Promise с ошибкой.

Как веб-страница будет использовать API?

После подключения myExtensionApi.js, на веб-странице можно отправлять команды так:

window.myExtension.sendMessageToNative({ command: "ping" })
    .then(response => console.log("Ответ от нативного приложения:", response))
    .catch(error => console.error("Ошибка:", error));

Этот API делает взаимодействие прозрачным и удобным:

  • Веб-страница ничего не знает о window.postMessage() и chrome.runtime.sendMessage().

  • Работа с расширением происходит через window.myExtension, без прямого взаимодействия с Chrome API.

Теперь расширение готово и мы можем его установить в браузер, а также написать тестовую веб-страницу index.html.

Установка расширения

Chrome позволяет загружать расширения без публикации в интернет-магазине. Для этого:

1. Открываем chrome://extensions/

2. Включаем режим разработчика (Developer mode) в правом верхнем углу.

3. Нажимаем Load unpacked (Загрузить распакованное).

4. Выбираем папку с нашим расширением (extension).

После загрузки расширение появится в списке. Теперь его можно включить/выключить, просмотреть его ID и открыть консоль background.js.

Закрепляем ID расширения с помощью key

По умолчанию Chrome генерирует случайный ID расширения при каждом его установке. Если мы не хотим, чтобы он менялся, можно зафиксировать ID, добавив поле "key" в manifest.json.

В консоли background.js выполните команду:

chrome.management.getSelf((info) => console.log(info.id));

Скопируйте ID и вставьте его в manifest.json в поле "key":

{
    "name": "My Chrome Extension",
    "version": "1.0",
    "manifest_version": 3,
    "description": "Расширение для взаимодействия с нативным приложением через Native Messaging.",
    "permissions": [
      "nativeMessaging"
    ],
    "background": {
      "service_worker": "background.js"
    },
    "content_scripts": [
      {
        "matches": ["<all_urls>"],
        "js": ["content.js"],
        "run_at": "document_start"
      }
    ],
    "web_accessible_resources": [
      {
        "resources": ["myExtensionApi.js"],
        "matches": ["<all_urls>"]
      }
    ],
    "host_permissions": [
      "<all_urls>"
    ],
    "icons": {
      "16": "icons/icon.png",
      "48": "icons/icon.png",
      "128": "icons/icon.png"
    },
    "key": "cfmbjaocnfillcmjbimhmmknfmnafafj" // Добавленное поле
  }

Зачем нужен key?

  • Фиксированный ID расширения – это важно, если расширение взаимодействует с другими сервисами, которые проверяют его ID.

  • Сохранение разрешений – Chrome не сбросит разрешения при каждом обновлении, если ID остаётся неизменным.

Теперь расширение можно загружать повторно, и его ID останется неизменным.

Разработка тестовой веб-страницы

Что будет на странице?

  • Подключим myExtensionApi.js, чтобы веб-страница могла взаимодействовать с расширением.

  • Добавим одну кнопку, которая отправляет тестовую команду в нативное приложение через window.myExtension.sendMessageToNative().

  • Выведем ответ от нативного приложения на страницу.

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Тест расширения</title>
    <script src="chrome-extension://nfjicgegikkgaalohojndbobkehjpbck/myExtensionApi.js"></script>
    <style>
        body {
            font-family: Arial, sans-serif;
            text-align: center;
            margin: 50px;
        }
        button {
            padding: 10px 20px;
            font-size: 16px;
            cursor: pointer;
        }
        #response {
            margin-top: 20px;
            font-weight: bold;
        }
    </style>
</head>
<body>

    <h1>Тест Chrome-расширения</h1>
    <button id="sendCommand">Отправить команду</button>
    <p id="response">Ожидание ответа...</p>

    <script>
        document.getElementById("sendCommand").addEventListener("click", () => {
            document.getElementById("response").textContent = "Отправка запроса...";

            window.myExtension.sendMessageToNative({ command: "test" })
                .then(response => {
                    document.getElementById("response").textContent = "Ответ: " + JSON.stringify(response);
                })
                .catch(error => {
                    document.getElementById("response").textContent = "Ошибка: " + error.message;
                });
        });
    </script>

</body>
</html>

Почему myExtensionApi.js подключается так?

<script src="chrome-extension://nfjicgegikkgaalohojndbobkehjpbck/myExtensionApi.js"></script>

Это важно, потому что:

  • Обычное подключение через <script src="myExtensionApi.js"> не сработает, так как myExtensionApi.js является частью Chrome-расширения, а не статического сайта.

  • Файл загружается из расширения по chrome-extension://[ID_расширения]/..., где ID_расширения — это зафиксированный ID расширения из manifest.json.

Если вы сделали, всё правильно, то по нажатию на кнопку “Отправить команду”, вы увидите:

Разработка нативного приложения на C

Теперь создадим нативное приложение, которое будет получать команды от Chrome-расширения и отправлять ответ обратно. Для простоты оно будет работать как эхо-сервер: принимать JSON, логировать его в консоль и отправлять обратно в Chrome-расширение.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <io.h>

// Функция для чтения данных из stdin
char* read_message() {
    unsigned int message_length = 0;
    if (fread(&message_length, sizeof(message_length), 1, stdin) != 1) {
        fprintf(stderr, "[Ошибка] Не удалось прочитать длину сообщения\n");
        return NULL;
    }

    fprintf(stderr, "[DEBUG] Длина сообщения: %u байт\n", message_length);

    char message = (char) malloc(message_length + 1);
    if (!message) {
        fprintf(stderr, "[Ошибка] Ошибка выделения памяти\n");
        exit(1);
    }

    size_t bytes_read = 0;
    while (bytes_read < message_length) {
        size_t result = fread(message + bytes_read, 1, message_length - bytes_read, stdin);
        if (result <= 0) {
            fprintf(stderr, "[Ошибка] Ошибка чтения данных сообщения\n");
            free(message);
            return NULL;
        }
        bytes_read += result;
    }

    message[message_length] = '\0';
    fprintf(stderr, "[DEBUG] Полученное сообщение: %s\n", message);
    return message;
}

// Функция для отправки ответа в stdout
void send_message(const char* message) {
    unsigned int message_length = strlen(message);

    fwrite(&message_length, sizeof(message_length), 1, stdout);
    fwrite(message, sizeof(char), message_length, stdout);
    fflush(stdout); // Обязательно сбрасываем буфер stdout

    fprintf(stderr, "[DEBUG] Отправлен ответ: %s\n", message);
}

int main() {
    // Переводим stdin и stdout в бинарный режим для Windows
    setmode(fileno(stdin), OBINARY);
    setmode(fileno(stdout), OBINARY);

    char *request = read_message();
    if (request) {
        fprintf(stderr, "[INFO] Получено сообщение: %s\n", request);

        // Формируем JSON-ответ
        char response[1024];
        snprintf(response, sizeof(response), "{\"echo\": %s}", request);

        send_message(response);
        free(request);
    }

    return 0; // Завершаем процесс после обработки одного сообщения
}

Как это работает?

1. Читаем данные из Chrome

  • read_message() сначала читает 4 байта длины JSON-сообщения.

  • Затем читает сам JSON из stdin.

2. Формируем ответ

  • Берём входное сообщение и оборачиваем его в JSON-ответ { "echo": <оригинальное сообщение> }.

3. Отправляем ответ в Chrome

  • send_message() записывает длину JSON и сам JSON в stdout, чтобы Chrome-расширение могло его прочитать.

Компиляция .exe с помощью GCC

Для компиляции я использую gcc из пакета MSYS2. В этой статье я не буду подробно останавливаться на его установке и настройке, так как она не связана с основной темой. Если у вас уже настроена среда, достаточно выполнить следующую команду:

gcc -o native.exe native.c

После компиляции у нас получится native.exe, который можно использовать для работы с Chrome-расширением:

Связываем расширение и приложение

Регистрация нативного приложения в системе

Чтобы Chrome-расширение могло взаимодействовать с нативным приложением, его необходимо зарегистрировать в системе. Это делается с помощью записи в реестре (Windows) и JSON-манифеста для Native Messaging Host.

В этом разделе создадим:

  • install.bat – скрипт для установки (добавления записи в реестр).

  • uninstall.bat – скрипт для удаления записи.

  • nmh-manifest.json – манифест нативного приложения, который Chrome использует для связи.

JSON-манифест (nmh-manifest.json)

Chrome ищет специальный JSON-файл, в котором описано, где находится исполняемый файл (native.exe) и какие расширения могут с ним взаимодействовать.

{
  "name": "my.native.app",
  "description": "Нативное приложение для Chrome Native Messaging",
  "path": "native.exe",
  "type": "stdio",
  "allowed_origins": [
    "chrome-extension://nfjicgegikkgaalohojndbobkehjpbck/"
  ]
}

Что означают эти поля?

  • "name" – уникальное имя приложения, которое Chrome использует для связи. Должно совпадать с chrome.runtime.sendNativeMessage("my.native.app", ...) в background.js.

  • "description" – описание приложения (необязательное поле).

  • "path" – путь к native.exe. Важно указывать абсолютный путь, иначе Chrome не найдёт файл.

  • "type" – Chrome поддерживает только stdio, что означает ввод/вывод через stdin и stdout.

  • "allowed_origins" – список Chrome-расширений, которым разрешено взаимодействовать с нативным приложением.

Важно: ID расширения (nfjicgegikkgaalohojndbobkehjpbck) должен совпадать с ID установленного расширения, иначе Chrome заблокирует соединение.

Добавляем записи в реестр (install.bat, uninstall.bat)

Chrome ищет манифест нативного приложения по пути в реестре, поэтому добавим install.bat, который будет добавлять запись в реестр:

REG ADD "HKCU\Software\Google\Chrome\NativeMessagingHosts\my.native.app" /ve /t REG_SZ /d "%~dp0nmh-manifest.json" /f

И для удобства добавим uninstall.bat, который делает ровно обратное:

REG DELETE "HKCU\Software\Google\Chrome\NativeMessagingHosts\my.native.app" /f

Теперь наш репозиторий должен выглядеть вот так:

Проверка работоспособности

После выполнения install.bat, браузер сможет вызывать native.exe, отправить ему команду и получить ответ. Давайте это проверим:

Что дальше?

Этот пример дал базовое понимание технологии. Но возможности Native Messaging на этом не заканчиваются.

Что можно улучшить и расширить?

  • Добавить асинхронную обработку сообщений вместо простого эхо.

  • Сделать долговременное соединение (chrome.runtime.connectNative).

  • Реализовать поддержку сложных команд, например, управление локальными сервисами.

  • Улучшить UI для отображения состояния работы нативного приложения.

Native Messaging API — мощный инструмент, который позволяет безопасно и эффективно интегрировать браузер с локальными процессами. Главное — понимать его ограничения и правильно строить архитектуру.

На этом всё! Если статья оказалась полезной — значит, всё не зря.