| Crates.io | native_messaging |
| lib.rs | native_messaging |
| version | 0.2.0 |
| created_at | 2024-11-01 14:13:22.784058+00 |
| updated_at | 2025-12-21 06:52:00.195816+00 |
| description | Cross-platform Rust native messaging host for browser extensions (Chrome & Firefox), with async helpers and manifest installer. |
| homepage | https://github.com/IberAI/native-messaging |
| repository | https://github.com/IberAI/native-messaging |
| max_upload_size | |
| id | 1431829 |
| size | 94,471 |
A batteries-included Rust crate for browser Native Messaging:
NmError)This crate aims to be the “it just works” choice for native messaging.
Native Messaging is how a browser extension talks to a local native process (your “host”). The protocol is:
u32, native endianness)Your host reads from stdin and writes to stdout.
NmError::Disconnected as a normal shutdown.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"] }
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
}
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
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" });
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);
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(())
}
This crate includes an installer for writing/verifying/removing manifests for supported browsers.
Browser allowlists differ by family:
allowed_origins like: chrome-extension://<EXT_ID>/allowed_extensions like: your-addon@example.orguse 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();
use native_messaging::{verify_installed, Scope};
let ok = verify_installed("com.example.host", None, Scope::User).unwrap();
assert!(ok);
use native_messaging::{remove, Scope};
remove("com.example.host", &["chrome", "firefox", "edge"], Scope::User).unwrap();
Native messaging failures are usually manifest issues, not code.
Check:
host_name you installed (case-sensitive).Check:
allowed_origins contains exact chrome-extension://<id>/allowed_extensions contains your addon IDCheck:
path points to a real executable.path is absolute.NmError::Disconnected).This almost always means stdout was corrupted by logs. Switch logging to stderr/file.
Most users only need:
Host:
native_messaging::host::event_loopnative_messaging::host::Sendernative_messaging::host::NmErrorInstaller:
native_messaging::installnative_messaging::verify_installednative_messaging::removenative_messaging::Scopecargo test
cargo test --doc
cargo clippy -- -D warnings
MIT