How to implement a Native Messaging host using only Rust standard library?

First attempt, using C converted to Rust online includes references to libc Code shared from the Rust Playground · GitHub.

Second attempt, using this code Native Messaging - Rust · GitHub throws

The sender sent an invalid JSON message; message ignored.

The code also does not echo standard input.

My guess is you're sending the length as 8 bytes, not 4.

Try:

    let len = msg.len();
    let len = if len > 1024 * 1024 {
        let msg = format!("Message was too large, length: {}", len);
        return Err(io::Error::other(msg));
    } else {
        len as u32
    };

You should also use read_exact and write_all for the length, like you are for the message.

2 Likes

That sends the message back. Then the host exists.
Screenshot_2024-08-06_23-24-25

It should remain running, inside some kind of loop.

Additionally, instead of sending back a predefined message, I'm trying to read input, and echo back the message, as I do with other hosts GitHub - guest271314/NativeMessagingHosts: Native Messaging hosts.

 fn main() -> Result<(), Box<dyn std::error::Error>> {
+    loop {
         let msg = read_input()?;
         eprintln!("{:?}", std::str::from_utf8(&msg));
         write_output("{\"msg\":\"pang\"}")?;
-    Ok(())
+    }
}

Oh, just send the received message back? (I thought you were trying to print in a terminal despite, presumably, piping stdin and stdout.)

Sending the received message back:

    loop {
        let msg = read_input()?;
        write_output(&msg)?;
    }
pub fn write_output(msg: &[u8]) -> io::Result<()> {
    // ...
    outstream.write_all(msg)?;
2 Likes

The host exits.

There is no terminal or TTY involved. You can test for yourself following these instructions NativeMessagingHosts/README.md at main · guest271314/NativeMessagingHosts · GitHub.

Create a local folder named native-messaging-rust.

Include the following files.

manifest.json

{
  "name": "nm-rust",
  "short_name": "nm_rust",
  "version": "1.0",
  "manifest_version": 3,
  "description": "Rust Native Messaging host",
  "permissions": ["nativeMessaging"],
  "background": {
    "service_worker": "background.js",
    "type": "module"
  },
  "action": {}
}

background.js

globalThis.name = chrome.runtime.getManifest().short_name;
globalThis.port = chrome.runtime.connectNative(globalThis.name);
port.onMessage.addListener((message) => {
  console.log(message);
});
port.onDisconnect.addListener((p) => console.log(chrome.runtime.lastError));
port.postMessage(new Array(209715));

chrome.runtime.onInstalled.addListener((reason) => {
  console.log(reason);
});

nm_rust.json

{
  "name": "nm_rust",
  "description": "Rust Native Messaging Host",
  "path": "/absolute/path/to/native-messaging-rust/nm_rust.rs",
  "type": "stdio",
  "allowed_origins": [
    "chrome-extension://<ID>"
  ]
}

You'll get the generated ID in chrome://extensions when you install the unpacked extension.

nm_rust.rs

#!/usr/bin/env -S /home/user/.cargo/bin/cargo -Zscript

use std::io::{self, Read, Write};

fn main() {
    loop {
        let msg = read_input()?;
        write_output(&msg)?;
    }
}

pub fn read_input() -> io::Result<Vec<u8>> {
    let mut instream = io::stdin();
    let mut length = [0; 4];
    instream.read(&mut length)?;
    let mut buffer = vec![0; u32::from_ne_bytes(length) as usize];
    instream.read_exact(&mut buffer)?;
    Ok(buffer)
}

pub fn write_output(msg: &str) -> io::Result<()> {
    let mut outstream = io::stdout();
    let len = msg.len();
    let len = if len > 1024 * 1024 {
    let msg = format!("Message was too large, length: {}", len);
        return Err(io::Error::other(msg));
    } else {
        len as u32
    };
    outstream.write(&len.to_ne_bytes())?;
    outstream.write_all(msg.as_bytes())?;
    outstream.flush()?;
    Ok(())
}

What the C++ version looks like

// C++ Native Messaging host
// https://browserext.github.io/native-messaging/
// https://developer.chrome.com/docs/apps/nativeMessaging/
// https://www.reddit.com/user/Eternal_Weeb/
// guest271314, 2022
#include <iostream>
#include <vector>
using namespace std;

vector<uint8_t> getMessage() {
  uint32_t length = 0;
  size_t size = fread(&length, sizeof(length), 1, stdin);
  vector<uint8_t> message(length); 
  size = fread(message.data(), sizeof(*message.data()), message.size(), stdin);
  return message;
}

void sendMessage(const vector<uint8_t> &message) {
  const uint32_t length = message.size();
  fwrite(&length, sizeof(length), 1, stdout);
  fwrite(message.data(), message.size(), sizeof(*message.data()), stdout);
  fflush(stdout);
}

int main() {
  while (true) {
    sendMessage(getMessage());
  }
  return 0;
}

If we are going to send that error message back to the client, it need to be formatted, too, e.g., using QuickJS to read standard input to V8's d8 shell in a subprocess, because d8's readline() is expecting text to execute we have to work around that and use a different means to read stdin to Goolge V8's d8 shell NativeMessagingHosts/nm_d8.js at main · guest271314/NativeMessagingHosts · GitHub

  } catch (e) {
    const json = JSON.stringify({error:e.message});
    std.out.write(Uint32Array.of(json.length).buffer, 0, 4);

Now I don't know Rust, though if we do

    loop {
        let msg = read_input()?;
        write_output(&msg)?;
    }

don't we have to change the type from msg: &str) to this type io::Result<Vec<u8>>?

  • read_input() returns an io::Result<Vec<u8>>

  • the type of read_input()? (after the ? applies) is Vec<u8>

  • so the type of msg in let msg = read_input()? is inferred to be Vec<u8>

  • and I changed write_output to take a &[u8] instead of a &str

  • &Vec<u8> coerces to &[u8]

2 Likes

How to echo standard input?

Unfortunately that is exiting.

@quinedot

The code in Rust Playground works to echo input. Thanks.

Native Messaging doesn't handle standard error.

How to send the fomatted string as JSON to the client here, then just exit?

let len = if len > 1024 * 1024 {
        let msg = format!("Message was too large, length: {}", len);
        return Err(io::Error::other(msg));

If you want a type-based/structured way to create JSON, with proper escaping and all of that, you'll need some third-party crate.

Otherwise, you can just make the formatted message valid json. The formatting documentation may be useful. In particular, { and } must be escaped as {{ and }}.

Sloppy example:

        let msg = format!("{{ \"error\": \"Message was too large, length: {}\" }}", len);
        outstream.write_all(&msg.len().to_ne_bytes())?;
        outstream.write_all(msg.as_bytes())?;
        outstream.flush()?;
        return Err(io::Error::other(msg));

Slightly less sloppy example using the serde and serde_json crates:

use serde::Serialize;

#[derive(Serialize)]
struct JsonError<'msg> {
    error: &'msg str,
}
        let msg = format!("Message was too large, length: {}", len);
        let err = JsonError { error: &msg };
        let json = serde_json::to_string(&err)?;
        outstream.write_all(&json.len().to_ne_bytes())?;
        outstream.write_all(json.as_bytes())?;
        outstream.flush()?;
        return Err(io::Error::other(msg));

Minimized serde/serde_json example.

1 Like

The first example throws in the client

The sender sent an invalid JSON message; message ignored.

This is the exact code I'm running

pub fn write_output(msg: &[u8]) -> io::Result<()> {
    let mut outstream = io::stdout();
    let len = msg.len();
    let len = if len > 1024 * 1024 {
        let msg = format!("{{ \"error\": \"Message was too large, length: {}\" }}", len);
        outstream.write_all(&msg.len().to_ne_bytes())?;
        outstream.write_all(msg.as_bytes())?;
        outstream.flush()?;
        return Err(io::Error::other(msg));
    } else {
        len as u32
    }; 
    outstream.write_all(&len.to_ne_bytes())?;
    outstream.write_all(msg)?;
    outstream.flush()?;
    Ok(())
}

Meaning the formatted message is never sent to the client from the host when 1024*1024 is exceeded from client.

Possibly I messed up my ad-hoc JSON or possibly only certain JSON messages are supported.

An example of a basic JSON string will work for now, e.g., "Message length exceeds 1024*1024", including double quotes.

Just play around with something that prints to stdout until you get the JSON you want. You don't need me to debug JSON.

1 Like

Alright. Thanks. I'll probably publish the code so far on GitHub with this as the comments

// Rust Native Messaging Host
// https://gist.github.com/TotallyNotChase/c747c55d4a965954f49a7fa5c3f344e0
// https://users.rust-lang.org/t/how-to-implement-a-native-messaging-host-using-only-rust-standard-library/115603/13
// 8-7-2024
1 Like

@quinedot Do you know the source of the following error message?

Error: Error { kind: UnexpectedEof, message: "failed to fill whole buffer" }