NOTE: The codebase is Windows-users oriented!
- In order to set up the native messaging host (hereinafter - "NMH") on your local machine referring to specification that describes how to implement it;
- Load the extension on Chromium-based browser of choice (e.g. Chrome), the proof-of-case extension can be found under
./nmh-extension--unpacked/path (directory).
Before running the command, it assumes your current working directory is
native_messaging_hoston your active terminal (I use Git Bash for Windows, you may use complete WSL 2.0 or later), then simply run
dart compile exe ./bin/main.dart -o ./bin/main.exe; NOTE: you do not need to run it from the terminal, the extension itself will !
- Load the unpacked extension, and click on your extension icon pinned to your browser's toolbar
- Open the extension service worker console and expect the following output as shown in Figure 1:

- Build something incredible with Dart and JavaScript (Chrome Extensions), respecting the stdio limitations as described in the specification.
I am not gonna lie, I got stuck at least once, credits to GitHub Copilot for the guidance ❤️
The native messaging protocol frames every message with a 4-byte little-endian length prefix followed by a UTF-8 encoded JSON payload. bin/main.dart listens on stdin, and lib/src/native_messaging_host_base.dart implements the framing logic described below.
Minimal robust algorithm (steps):
- Maintain a buffer of unconsumed bytes.
- Append each incoming chunk to that buffer.
- While buffer length >= 4:
- Read length = getInt32(buffer[0..3], Endian.little).
- If buffer length >= 4 + length:
- Extract payload = buffer[4 .. 4+length-1].
- Process payload (utf8.decode -> jsonDecode).
- Remove consumed bytes (0 .. 4+length-1) from buffer.
- Continue loop (there may be another full frame).
- Else:
- Break and wait for more bytes to arrive (partial payload).
- Repeat on next chunk.
NOTE:
decodeMessagecurrently assumes a single, already-complete frame is passed in (it does not itself maintain a rolling buffer across multiplestdinchunks); the buffering behavior above describes the general algorithm a robust caller (such asmain()) should apply on top of it if a message can arrive split across chunks.
The table below maps the Dart Types/classes used throughout the implementation to their official dart.dev API documentation, for a one-to-one, easy-to-follow reference while reading the source.
| Type / class | Used in | Purpose | dart.dev API reference |
|---|---|---|---|
List<int> |
bin/main.dart, captureStdin |
Raw bytes received from stdin/passed to helpers before typed conversion |
List |
Uint8List |
encodeMessage, decodeMessage, bin/main.dart |
Fixed-length, byte-level view over binary data (the wire format for encode/decode) | Uint8List |
ByteData |
encodeMessage, decodeMessage (buffer.asByteData()) |
Reads/writes fixed-width, endian-aware numeric values within a byte buffer | ByteData |
Endian |
encodeMessage, decodeMessage (Endian.little) |
Specifies byte order (little-endian) when reading/writing the 32-bit length prefix | Endian |
String |
encodeMessage, decodeMessage, captureStdin |
Holds the UTF-8 decoded JSON text before/after (de)serialization | String |
Map<String, dynamic> |
encodeMessage, decodeMessage, bin/main.dart |
Structured representation of the decoded/encoded JSON message | Map |
ArgumentError |
decodeMessage |
Thrown when the buffer is too short or the payload is incomplete | ArgumentError |
IOSink |
captureStdin |
Buffered output sink returned by File.openWrite, used to append log lines |
IOSink |
File |
captureStdin |
Represents log.jsonl on disk, opened in append mode |
File |
FileMode |
captureStdin (FileMode.append) |
Controls how the log file is opened (append vs. write/overwrite) | FileMode |
Stdin |
bin/main.dart (io.stdin) |
Standard input stream the NMH listens on for incoming framed messages | Stdin |
Stdout |
bin/main.dart (io.stdout) |
Standard output stream used to write the framed, encoded response | Stdout |
ProcessSignal |
bin/main.dart (ProcessSignal.sigint) |
Represents OS signals (e.g. SIGINT) that the process listens/reacts to |
ProcessSignal |
- Write a test that temporarily would generate
./add_this_registry.regpopulating the '@=' with concrete value of the path, rather than something likeDISKVOLUME:\\PATH_TO_DART_PACKAGE_BIN_DIR\\manifest.jsonas given in the initial project template; optionally, leverage package:puppeteer package dependency by extracting currently tested NMH's "ID" value that would in turn replace./bin/manifest.jsonfile'sYOUR_EXTENSION_GENERATED_IDplaceholder as given in the initial project template - this two-step testing process would reduce the significant amount of manual intervention by consumer or tester oneself, however for this proof-of-case scenario, it does a job as is...
Made with ♥ by projektorius96 a.k.a. Lukas Gaučas