Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Async implementation of MDN native messaging. Provides the ability to install host manifest.

License

Notifications You must be signed in to change notification settings

IberAI/native-messaging

Repository files navigation

native_messaging

A batteries-included Rust crate for browser Native Messaging:

  • Build a Native Messaging host that talks to your extension over stdin/stdout
  • Install, verify, and remove the native host manifest for multiple browsers
  • Safe framing + size caps + structured errors (NmError)
  • Cross-platform: Windows / macOS / Linux

This crate aims to be the “it just works” choice for native messaging.


What is Native Messaging?

Native Messaging is how a browser extension talks to a local native process (your “host”). The protocol is:

  1. A 4-byte length prefix (u32, native endianness)
  2. Followed by that many bytes of UTF-8 JSON

Your host reads from stdin and writes to stdout.

Big gotchas (read this first)

  • Disconnect is normal: when the extension disconnects or browser exits, stdin usually closes. Treat NmError::Disconnected as a normal shutdown.
  • Never log to stdout: stdout is reserved for framed messages. Logging to stdout corrupts the stream. Log to stderr or a file.
  • Size limits are real:
    • host → browser: 1 MiB (enforced)
    • browser → host: 64 MiB (enforced)

Install

cargo add native_messaging

Tokio is required for the async host helpers (recommended). Use these features:

tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread", "sync"] }

Quickstart: robust async host loop (recommended)

This is the easiest correct way to run a host continuously and reply to messages.

use native_messaging::host::{event_loop, NmError, Sender};
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct In {
    ping: String,
}

#[derive(Serialize)]
struct Out {
    pong: String,
}

#[tokio::main]
async fn main() -> Result<(), NmError> {
    event_loop(|raw: String, send: Sender| async move {
        let incoming: In = serde_json::from_str(&raw).map_err(NmError::DeserializeJson)?;
        send.send(&Out { pong: incoming.ping }).await?;
        Ok(())
    })
    .await
}

Logging (important)

Do not use println!() in a host. It writes to stdout and breaks the protocol. Use stderr:

eprintln!("host starting"); // ✅ safe
// println!("host starting"); // ❌ unsafe

JS extension example (Chrome/Chromium)

This is what the extension side typically looks like:

const port = chrome.runtime.connectNative("com.example.host");

port.onMessage.addListener((msg) => {
  console.log("native reply:", msg);
});

port.onDisconnect.addListener(() => {
  console.log("native disconnected:", chrome.runtime.lastError);
});

port.postMessage({ ping: "hello" });

Pure framing (unit-test friendly)

You can test framing without stdin/stdout using an in-memory buffer:

use native_messaging::host::{encode_message, decode_message, MAX_FROM_BROWSER};
use serde_json::json;
use std::io::Cursor;

let msg = json!({"hello": "world"});
let frame = encode_message(&msg).unwrap();

let mut cur = Cursor::new(frame);
let raw = decode_message(&mut cur, MAX_FROM_BROWSER).unwrap();

let back: serde_json::Value = serde_json::from_str(&raw).unwrap();
assert_eq!(back, msg);

One-shot read/write helpers (convenience)

These helpers read one message from stdin and write one reply to stdout. For production hosts, prefer event_loop.

use native_messaging::{get_message, send_message};
use native_messaging::host::NmError;
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct In { ping: String }

#[derive(Serialize)]
struct Out { pong: String }

#[tokio::main]
async fn main() -> Result<(), NmError> {
    let raw = get_message().await?;
    let incoming: In = serde_json::from_str(&raw).map_err(NmError::DeserializeJson)?;
    send_message(&Out { pong: incoming.ping }).await?;
    Ok(())
}

Installing a manifest (config-driven browsers)

This crate includes an installer for writing/verifying/removing manifests for supported browsers.

Browser allowlists differ by family:

  • Chromium-family uses allowed_origins like: chrome-extension://<EXT_ID>/
  • Firefox-family uses allowed_extensions like: [email protected]
use std::path::Path;
use native_messaging::{install, Scope};

let host_name = "com.example.host";
let description = "Example native messaging host";

// On macOS/Linux, this must be an absolute path.
let exe_path = Path::new("/absolute/path/to/host-binary");

// Chromium-family allow-list:
let allowed_origins = vec![
    "chrome-extension://your_extension_id/".to_string(),
];

// Firefox-family allow-list:
let allowed_extensions = vec![
    "[email protected]".to_string(),
];

// Install for selected browsers by key:
let browsers = &["chrome", "firefox", "edge"];

install(
    host_name,
    description,
    exe_path,
    &allowed_origins,
    &allowed_extensions,
    browsers,
    Scope::User,
).unwrap();

Verify installation

use native_messaging::{verify_installed, Scope};

let ok = verify_installed("com.example.host", None, Scope::User).unwrap();
assert!(ok);

Remove a manifest

use native_messaging::{remove, Scope};

remove("com.example.host", &["chrome", "firefox", "edge"], Scope::User).unwrap();

Troubleshooting

Native messaging failures are usually manifest issues, not code.

“Specified native messaging host not found”

Check:

  • The extension calls the exact same host_name you installed (case-sensitive).
  • The manifest exists at the expected location (User vs System scope).
  • The manifest JSON is valid.

“Access to the specified native messaging host is forbidden”

Check:

  • Chromium-family: allowed_origins contains exact chrome-extension://<id>/
  • Firefox-family: allowed_extensions contains your addon ID

“Native host has exited” / “Failed to start native messaging host”

Check:

  • The manifest path points to a real executable.
  • On macOS/Linux, manifest path is absolute.
  • Your host does not log to stdout.
  • Your host handles disconnect cleanly (EOF → NmError::Disconnected).

Host prints weird JSON / extension can’t parse

This almost always means stdout was corrupted by logs. Switch logging to stderr/file.


API overview

Most users only need:

  • Host:

    • native_messaging::host::event_loop
    • native_messaging::host::Sender
    • native_messaging::host::NmError
  • Installer:

    • native_messaging::install
    • native_messaging::verify_installed
    • native_messaging::remove
    • native_messaging::Scope

Notes for crate maintainers / contributors

Run tests (including docs)

cargo test
cargo test --doc

Clippy (strict)

cargo clippy -- -D warnings

License

MIT

About

Async implementation of MDN native messaging. Provides the ability to install host manifest.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages