native_messaging

Crates.ionative_messaging
lib.rsnative_messaging
version0.2.0
created_at2024-11-01 14:13:22.784058+00
updated_at2025-12-21 06:52:00.195816+00
descriptionCross-platform Rust native messaging host for browser extensions (Chrome & Firefox), with async helpers and manifest installer.
homepagehttps://github.com/IberAI/native-messaging
repositoryhttps://github.com/IberAI/native-messaging
max_upload_size
id1431829
size94,471
(IberAI)

documentation

https://docs.rs/native-messaging

README

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: your-addon@example.org
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![
    "your-addon@example.org".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

Commit count: 13

cargo fmt