Bug 1287007 - Move native messaging to child process draft
authorRob Wu <rob@robwu.nl>
Wed, 14 Sep 2016 03:52:35 -0700
changeset 414480 fa08f4f671041a6fa3901b8bd227d44e0e2f3067
parent 414479 4111018be2c2fe2db471abb212e65689c88d6c08
child 414481 dfdbb8ad148c03b202c5c887e3ef5172cc2cd69d
push id29672
push userbmo:rob@robwu.nl
push dateFri, 16 Sep 2016 10:27:40 +0000
bugs1287007
milestone51.0a1
Bug 1287007 - Move native messaging to child process Move `runtime.connectNative` and `runtime.sendNativeMessage` to `addon_child`. Note: This does not change the behavior for launching the native app, it is still launched from the main process. Some other significant changes: ExtensionUtils.Port: - Refactor Port in ExtensionUtils to allow using Port without wrappers via `registerOnMessage` and `registerOnDisconnect`. - Add "port" parameter to Port's `onMessage` method (Chrome compat). - Do not clone the message received by Port's `onMessage` (per & Chrome compat). ExtensionUtils.Messenger - Add connectGetRawPort to allow `postMessage` with already-sanitized values without having to use `cloneInto` with `context.cloneScope`. Native messaging: - Do not throw an error when `disconnect()` is called on an already-disconnected port (Chrome compat). - Do not dispatch `onDisconnect` when `.disconnect()` was called (Chrome compat). - Use `context.extension` instead of passing the extension separately. - Serialize the message in the child instead of the main process. MozReview-Commit-ID: FJUJYSb3A81
toolkit/components/extensions/Extension.jsm
toolkit/components/extensions/ExtensionChild.jsm
toolkit/components/extensions/ExtensionUtils.jsm
toolkit/components/extensions/NativeMessaging.jsm
toolkit/components/extensions/ext-c-runtime.js
toolkit/components/extensions/ext-runtime.js
toolkit/components/extensions/test/xpcshell/test_ext_native_messaging.js
toolkit/components/extensions/test/xpcshell/test_native_messaging.js
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -41,16 +41,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "Log",
                                   "resource://gre/modules/Log.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "MatchGlobs",
                                   "resource://gre/modules/MatchPattern.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
                                   "resource://gre/modules/MatchPattern.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
                                   "resource://gre/modules/MessageChannel.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NativeApp",
+                                  "resource://gre/modules/NativeMessaging.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
                                   "resource://gre/modules/Preferences.jsm");
@@ -106,16 +108,17 @@ const COMMENT_REGEXP = new RegExp(String
         " (?:[^"\\\n] | \\.)* "
       )*?
     )
 
     //.*
   `.replace(/\s+/g, ""), "gm");
 
 var GlobalManager;
+var ParentAPIManager;
 
 // This object loads the ext-*.js scripts that define the extension API.
 var Management = new class extends SchemaAPIManager {
   constructor() {
     super("main");
     this.initialized = null;
   }
 
@@ -179,16 +182,31 @@ let ProxyMessenger = {
 
     MessageChannel.addListener(messageManagers, "Extension:Connect", this);
     MessageChannel.addListener(messageManagers, "Extension:Message", this);
     MessageChannel.addListener(messageManagers, "Extension:Port:Disconnect", this);
     MessageChannel.addListener(messageManagers, "Extension:Port:PostMessage", this);
   },
 
   receiveMessage({target, messageName, channelId, sender, recipient, data, responseType}) {
+    if (recipient.toNativeApp) {
+      let {childId, toNativeApp} = recipient;
+      if (messageName == "Extension:Message") {
+        let context = ParentAPIManager.getContextById(childId);
+        return new NativeApp(context, toNativeApp).sendMessage(data);
+      }
+      if (messageName == "Extension:Connect") {
+        let context = ParentAPIManager.getContextById(childId);
+        NativeApp.onConnectNative(context, target.messageManager, data.portId, sender, toNativeApp);
+        return true;
+      }
+      // "Extension:Port:Disconnect" and "Extension:Port:PostMessage" for
+      // native messages are handled by NativeApp.
+      return;
+    }
     let extension = GlobalManager.extensionMap.get(sender.extensionId);
     let receiverMM = this._getMessageManagerForRecipient(recipient);
     if (!extension || !receiverMM) {
       return Promise.reject({
         result: MessageChannel.RESULT_NO_HANDLER,
         message: "No matching message handler for the given recipient.",
       });
     }
@@ -224,18 +242,16 @@ let ProxyMessenger = {
     // runtime.sendMessage / runtime.connect
     if (extensionId) {
       // TODO(robwu): map the extensionId to the addon parent process's message
       // manager when they run in a separate process.
       let pipmm = Services.ppmm.getChildAt(0);
       return pipmm;
     }
 
-    // Note: No special handling for sendNativeMessage / connectNative because
-    // native messaging runs in the chrome process, so it never needs a proxy.
     return null;
   },
 };
 
 class BrowserDocshellFollower {
   /**
    * Follows the <browser> belonging to the `xulBrowser`'s current docshell.
    *
@@ -396,17 +412,17 @@ function findPathInObject(obj, path, pri
     }
 
     obj = obj[elt];
   }
 
   return obj;
 }
 
-var ParentAPIManager = {
+ParentAPIManager = {
   proxyContexts: new Map(),
 
   init() {
     Services.obs.addObserver(this, "message-manager-close", false);
 
     Services.mm.addMessageListener("API:CreateProxyContext", this);
     Services.mm.addMessageListener("API:CloseProxyContext", this, true);
     Services.mm.addMessageListener("API:Call", this);
@@ -493,21 +509,17 @@ var ParentAPIManager = {
     if (!context) {
       return;
     }
     context.unload();
     this.proxyContexts.delete(childId);
   },
 
   call(data, target) {
-    let context = this.proxyContexts.get(data.childId);
-    if (!context) {
-      Cu.reportError("WebExtension context not found!");
-      return;
-    }
+    let context = this.getContextById(data.childId);
     if (context.currentMessageManager !== target.messageManager) {
       Cu.reportError("WebExtension warning: Message manager unexpectedly changed");
     }
 
     function callback(...cbArgs) {
       let lastError = context.lastError;
 
       context.currentMessageManager.sendAsyncMessage("API:CallResult", {
@@ -531,21 +543,17 @@ var ParentAPIManager = {
         childId: data.childId,
         callId: data.callId,
         lastError: msg,
       });
     }
   },
 
   addListener(data, target) {
-    let context = this.proxyContexts.get(data.childId);
-    if (!context) {
-      Cu.reportError("WebExtension context not found!");
-      return;
-    }
+    let context = this.getContextById(data.childId);
     if (context.currentMessageManager !== target.messageManager) {
       Cu.reportError("WebExtension warning: Message manager unexpectedly changed");
     }
 
     function listener(...listenerArgs) {
       context.currentMessageManager.sendAsyncMessage("API:RunListener", {
         childId: data.childId,
         path: data.path,
@@ -555,23 +563,30 @@ var ParentAPIManager = {
 
     context.listenerProxies.set(data.path, listener);
 
     let args = Cu.cloneInto(data.args, context.sandbox);
     findPathInObject(context.apiObj, data.path).addListener(listener, ...args);
   },
 
   removeListener(data) {
-    let context = this.proxyContexts.get(data.childId);
-    if (!context) {
-      Cu.reportError("WebExtension context not found!");
-    }
+    let context = this.getContextById(data.childId);
     let listener = context.listenerProxies.get(data.path);
     findPathInObject(context.apiObj, data.path).removeListener(listener);
   },
+
+  getContextById(childId) {
+    let context = this.proxyContexts.get(childId);
+    if (!context) {
+      let error = new Error("WebExtension context not found!");
+      Cu.reportError(error);
+      throw error;
+    }
+    return context;
+  },
 };
 
 ParentAPIManager.init();
 
 // All moz-extension URIs use a machine-specific UUID rather than the
 // extension's own ID in the host component. This makes it more
 // difficult for web pages to detect whether a user has a given add-on
 // installed (by trying to load a moz-extension URI referring to a
--- a/toolkit/components/extensions/ExtensionChild.jsm
+++ b/toolkit/components/extensions/ExtensionChild.jsm
@@ -106,19 +106,17 @@ class WannabeChildAPIManager extends Chi
     // Synchronously unload the ProxyContext because we synchronously create it.
     this.context.callOnClose({close: proxyContext.unload.bind(proxyContext)});
   }
 
   getFallbackImplementation(namespace, name) {
     // This is gross and should be removed ASAP.
     let shouldSynchronouslyUseParentAPI = false;
     // Incompatible APIs are listed here.
-    if (namespace == "runtime" && name == "connectNative" || // Returns a custom Port.
-        namespace == "runtime" && name == "sendNativeMessage" || // Fix together with connectNative.
-        namespace == "webNavigation" || // ChildAPIManager is oblivious to filters.
+    if (namespace == "webNavigation" || // ChildAPIManager is oblivious to filters.
         namespace == "webRequest") { // Incompatible by design (synchronous).
       shouldSynchronouslyUseParentAPI = true;
     }
     if (shouldSynchronouslyUseParentAPI) {
       let proxyContext = ParentAPIManager.proxyContexts.get(this.id);
       let apiObj = findPathInObject(proxyContext.apiObj, namespace, false);
       if (apiObj && name in apiObj) {
         return new LocalAPIImplementation(apiObj, name, this.context);
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -1175,16 +1175,17 @@ function Port(context, senderMM, receive
   this.senderMM = senderMM;
   this.receiverMMs = receiverMMs;
   this.name = name;
   this.id = id;
   this.sender = sender;
   this.recipient = recipient;
   this.disconnected = false;
   this.disconnectListeners = new Set();
+  this.unregisterMessageFuncs = new Set();
 
   // Common options for onMessage and onDisconnect.
   this.handlerBase = {
     messageFilterStrict: {portId: id},
     filterMessage: (sender, recipient) => {
       if (!sender.contextId) {
         Cu.reportError("Missing sender.contextId in message to Port");
         return false;
@@ -1205,69 +1206,103 @@ Port.prototype = {
     let portObj = Cu.createObjectIn(this.context.cloneScope);
 
     let publicAPI = {
       name: this.name,
       disconnect: () => {
         this.disconnect();
       },
       postMessage: json => {
-        if (this.disconnected) {
-          throw new this.context.cloneScope.Error("Attempt to postMessage on disconnected port");
-        }
-
-        this._sendMessage("Extension:Port:PostMessage", json);
+        this.postMessage(json);
       },
       onDisconnect: new EventManager(this.context, "Port.onDisconnect", fire => {
-        let listener = () => {
-          if (this.context.active && !this.disconnected) {
-            fire.withoutClone(portObj);
-          }
-        };
-
-        this.disconnectListeners.add(listener);
-        return () => {
-          this.disconnectListeners.delete(listener);
-        };
+        return this.registerOnDisconnect(() => fire.withoutClone(portObj));
       }).api(),
       onMessage: new EventManager(this.context, "Port.onMessage", fire => {
-        let handler = Object.assign({
-          receiveMessage: ({data}) => {
-            if (this.context.active && !this.disconnected) {
-              fire(data);
-            }
-          },
-        }, this.handlerBase);
-
-        MessageChannel.addListener(this.receiverMMs, "Extension:Port:PostMessage", handler);
-        return () => {
-          MessageChannel.removeListener(this.receiverMMs, "Extension:Port:PostMessage", handler);
-        };
+        return this.registerOnMessage(msg => {
+          msg = Cu.cloneInto(msg, this.context.cloneScope);
+          fire.withoutClone(msg, portObj);
+        });
       }).api(),
     };
 
     if (this.sender) {
       publicAPI.sender = this.sender;
     }
 
     injectAPI(publicAPI, portObj);
     return portObj;
   },
 
+  postMessage(json) {
+    if (this.disconnected) {
+      throw new this.context.cloneScope.Error("Attempt to postMessage on disconnected port");
+    }
+
+    this._sendMessage("Extension:Port:PostMessage", json);
+  },
+
+  /**
+   * Register a callback that is called when the port is disconnected by the
+   * *other* end. The callback is automatically unregistered when the port or
+   * context is closed.
+   *
+   * @param {function} callback Called when the other end disconnects the port.
+   * @returns {function} Function to unregister the listener.
+   */
+  registerOnDisconnect(callback) {
+    let listener = () => {
+      if (this.context.active && !this.disconnected) {
+        callback();
+      }
+    };
+    this.disconnectListeners.add(listener);
+    return () => {
+      this.disconnectListeners.delete(listener);
+    };
+  },
+
+  /**
+   * Register a callback that is called when a message is received. The callback
+   * is automatically unregistered when the port or context is closed.
+   *
+   * @param {function} callback Called when a message is received.
+   * @returns {function} Function to unregister the listener.
+   */
+  registerOnMessage(callback) {
+    let handler = Object.assign({
+      receiveMessage: ({data}) => {
+        if (this.context.active && !this.disconnected) {
+          callback(data);
+        }
+      },
+    }, this.handlerBase);
+
+    let unregister = () => {
+      this.unregisterMessageFuncs.delete(unregister);
+      MessageChannel.removeListener(this.receiverMMs, "Extension:Port:PostMessage", handler);
+    };
+    MessageChannel.addListener(this.receiverMMs, "Extension:Port:PostMessage", handler);
+    return unregister;
+  },
+
   _sendMessage(message, data) {
     let options = {
       recipient: Object.assign({}, this.recipient, {portId: this.id}),
       responseType: MessageChannel.RESPONSE_NONE,
     };
 
     return this.context.sendMessage(this.senderMM, message, data, options);
   },
 
   handleDisconnection() {
     MessageChannel.removeListener(this.receiverMMs, "Extension:Port:Disconnect", this.disconnectHandler);
+    for (let unregister of this.unregisterMessageFuncs) {
+      unregister();
+    }
     this.context.forgetOnClose(this);
     this.disconnected = true;
   },
 
   disconnectByOtherEnd() {
     if (this.disconnected) {
       return;
     }
@@ -1383,22 +1418,27 @@ Messenger.prototype = {
 
       MessageChannel.addListener(this.messageManagers, "Extension:Message", listener);
       return () => {
         MessageChannel.removeListener(this.messageManagers, "Extension:Message", listener);
       };
     }).api();
   },
 
-  connect(messageManager, name, recipient) {
+  connectGetRawPort(messageManager, name, recipient) {
     let portId = `${gNextPortId++}-${Services.appinfo.uniqueProcessID}`;
     let port = new Port(this.context, messageManager, this.messageManagers, name, portId, null, recipient);
     let msg = {name, portId};
     this._sendMessage(messageManager, "Extension:Connect", msg, recipient)
       .catch(e => port.disconnectByOtherEnd());
+    return port;
+  },
+
+  connect(messageManager, name, recipient) {
+    let port = this.connectGetRawPort(messageManager, name, recipient);
     return port.api();
   },
 
   onConnect(name) {
     return new SingletonEventManager(this.context, name, callback => {
       let listener = {
         messageFilterPermissive: this.filter,
 
@@ -2052,15 +2092,16 @@ this.ExtensionUtils = {
   BaseContext,
   DefaultWeakMap,
   EventEmitter,
   EventManager,
   IconDetails,
   LocalAPIImplementation,
   LocaleData,
   Messenger,
+  Port,
   PlatformInfo,
   SchemaAPIInterface,
   SingletonEventManager,
   SpreadArgs,
   ChildAPIManager,
   SchemaAPIManager,
 };
--- a/toolkit/components/extensions/NativeMessaging.jsm
+++ b/toolkit/components/extensions/NativeMessaging.jsm
@@ -1,15 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = ["HostManifestManager", "NativeApp"];
+/* globals NativeApp */
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 const {EventEmitter} = Cu.import("resource://devtools/shared/event-emitter.js", {});
 
 XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
@@ -157,43 +158,38 @@ this.HostManifestManager = {
     if (!VALID_APPLICATION.test(application)) {
       throw new Error(`Invalid application "${application}"`);
     }
     return this.init().then(() => this._lookup(application, context));
   },
 };
 
 this.NativeApp = class extends EventEmitter {
-  constructor(extension, context, application) {
+  constructor(context, application) {
     super();
 
     this.context = context;
     this.name = application;
 
     // We want a close() notification when the window is destroyed.
     this.context.callOnClose(this);
 
-    this.encoder = new TextEncoder();
     this.proc = null;
     this.readPromise = null;
     this.sendQueue = [];
     this.writePromise = null;
     this.sentDisconnect = false;
 
-    // Grab these once at startup
-    XPCOMUtils.defineLazyPreferenceGetter(this, "maxRead", PREF_MAX_READ, MAX_READ);
-    XPCOMUtils.defineLazyPreferenceGetter(this, "maxWrite", PREF_MAX_WRITE, MAX_WRITE);
-
     this.startupPromise = HostManifestManager.lookupApplication(application, context)
       .then(hostInfo => {
         if (!hostInfo) {
           throw new Error(`No such native application ${application}`);
         }
 
-        if (!hostInfo.manifest.allowed_extensions.includes(extension.id)) {
+        if (!hostInfo.manifest.allowed_extensions.includes(context.extension.id)) {
           throw new Error(`This extension does not have permission to use native application ${application}`);
         }
 
         let command = hostInfo.manifest.path;
         if (AppConstants.platform == "win") {
           // OS.Path.join() ignores anything before the last absolute path
           // it sees, so if command is already absolute, it remains unchanged
           // here.  If it is relative, we get the proper absolute path here.
@@ -215,32 +211,69 @@ this.NativeApp = class extends EventEmit
         this._startStderrRead();
       }).catch(err => {
         this.startupPromise = null;
         Cu.reportError(err instanceof Error ? err : err.message);
         this._cleanup(err);
       });
   }
 
+  /**
+   * Open a connection to a native messaging host.
+   *
+   * @param {BaseContext} context The context associated with the port.
+   * @param {nsIMessageSender} messageManager The message manager used to send
+   *     and receive messages from the port's creator.
+   * @param {string} portId A unique internal ID that identifies the port.
+   * @param {object} sender The object describing the creator of the connection
+   *     request.
+   * @param {string} application The name of the native messaging host.
+   */
+  static onConnectNative(context, messageManager, portId, sender, application) {
+    messageManager.QueryInterface(Ci.nsIMessageSender);
+    let app = new NativeApp(context, application);
+    let port = new ExtensionUtils.Port(context, messageManager, [messageManager], "", portId, sender, sender);
+    app.once("disconnect", () => port.disconnect());
+    /* eslint-disable mozilla/balanced-listeners */
+    app.on("message", (what, msg) => port.postMessage(msg));
+    /* eslint-enable mozilla/balanced-listeners */
+    port.registerOnMessage(msg => app.send(msg));
+    port.registerOnDisconnect(msg => app.close());
+  }
+
+  /**
+   * @param {BaseContext} context The scope from where `message` originates.
+   * @param {*} message A message from the extension, meant for a native app.
+   * @returns {ArrayBuffer} An ArrayBuffer that can be sent to the native app.
+   */
+  static encodeMessage(context, message) {
+    message = context.jsonStringify(message);
+    let buffer = new TextEncoder().encode(message).buffer;
+    if (buffer.byteLength > NativeApp.maxWrite) {
+      throw new context.cloneScope.Error("Write too big");
+    }
+    return buffer;
+  }
+
   // A port is definitely "alive" if this.proc is non-null.  But we have
   // to provide a live port object immediately when connecting so we also
   // need to consider a port alive if proc is null but the startupPromise
   // is still pending.
   get _isDisconnected() {
     return (!this.proc && !this.startupPromise);
   }
 
   _startRead() {
     if (this.readPromise) {
       throw new Error("Entered _startRead() while readPromise is non-null");
     }
     this.readPromise = this.proc.stdout.readUint32()
       .then(len => {
-        if (len > this.maxRead) {
-          throw new Error(`Native application tried to send a message of ${len} bytes, which exceeds the limit of ${this.maxRead} bytes.`);
+        if (len > NativeApp.maxRead) {
+          throw new Error(`Native application tried to send a message of ${len} bytes, which exceeds the limit of ${NativeApp.maxRead} bytes.`);
         }
         return this.proc.stdout.readJSON(len);
       }).then(msg => {
         this.emit("message", msg);
         this.readPromise = null;
         this._startRead();
       }).catch(err => {
         if (err.errorCode != Subprocess.ERROR_END_OF_FILE) {
@@ -299,26 +332,23 @@ this.NativeApp = class extends EventEmit
       }
     });
   }
 
   send(msg) {
     if (this._isDisconnected) {
       throw new this.context.cloneScope.Error("Attempt to postMessage on disconnected port");
     }
+    if (Cu.getClassName(msg, true) != "ArrayBuffer") {
+      throw new this.context.cloneScope.Error("The message is not an ArrayBuffer");
+    }
 
-    let json;
-    try {
-      json = this.context.jsonStringify(msg);
-    } catch (err) {
-      throw new this.context.cloneScope.Error(err.message);
-    }
-    let buffer = this.encoder.encode(json).buffer;
+    let buffer = msg;
 
-    if (buffer.byteLength > this.maxWrite) {
+    if (buffer.byteLength > NativeApp.maxWrite) {
       throw new this.context.cloneScope.Error("Write too big");
     }
 
     this.sendQueue.push(buffer);
     if (!this.startupPromise && !this.writePromise) {
       this._startWrite();
     }
   }
@@ -371,62 +401,20 @@ this.NativeApp = class extends EventEmit
     }
   }
 
   // Called from Context when the extension is shut down.
   close() {
     this._cleanup();
   }
 
-  portAPI() {
-    let port = {
-      name: this.name,
-
-      disconnect: () => {
-        if (this._isDisconnected) {
-          throw new this.context.cloneScope.Error("Attempt to disconnect an already disconnected port");
-        }
-        this._cleanup();
-      },
-
-      postMessage: msg => {
-        this.send(msg);
-      },
-
-      onDisconnect: new ExtensionUtils.SingletonEventManager(this.context, "native.onDisconnect", fire => {
-        let listener = what => {
-          this.context.runSafeWithoutClone(fire, port);
-        };
-        this.on("disconnect", listener);
-        return () => {
-          this.off("disconnect", listener);
-        };
-      }).api(),
-
-      onMessage: new ExtensionUtils.SingletonEventManager(this.context, "native.onMessage", fire => {
-        let listener = (what, msg) => {
-          msg = Cu.cloneInto(msg, this.context.cloneScope);
-          this.context.runSafeWithoutClone(fire, msg, port);
-        };
-        this.on("message", listener);
-        return () => {
-          this.off("message", listener);
-        };
-      }).api(),
-    };
-
-    port = Cu.cloneInto(port, this.context.cloneScope, {cloneFunctions: true});
-
-    return port;
-  }
-
   sendMessage(msg) {
     let responsePromise = new Promise((resolve, reject) => {
-      this.on("message", (what, msg) => { resolve(msg); });
-      this.on("disconnect", (what, err) => { reject(err); });
+      this.once("message", (what, msg) => { resolve(msg); });
+      this.once("disconnect", (what, err) => { reject(err); });
     });
 
     let result = this.startupPromise.then(() => {
       this.send(msg);
       return responsePromise;
     });
 
     result.then(() => {
@@ -437,8 +425,11 @@ this.NativeApp = class extends EventEmit
       responsePromise.catch(() => {});
 
       this._cleanup();
     });
 
     return result;
   }
 };
+
+XPCOMUtils.defineLazyPreferenceGetter(NativeApp, "maxRead", PREF_MAX_READ, MAX_READ);
+XPCOMUtils.defineLazyPreferenceGetter(NativeApp, "maxWrite", PREF_MAX_WRITE, MAX_WRITE);
--- a/toolkit/components/extensions/ext-c-runtime.js
+++ b/toolkit/components/extensions/ext-c-runtime.js
@@ -1,9 +1,11 @@
 "use strict";
+XPCOMUtils.defineLazyModuleGetter(this, "NativeApp",
+                                  "resource://gre/modules/NativeMessaging.jsm");
 
 function runtimeApiFactory(context) {
   let {extension} = context;
 
   return {
     runtime: {
       onConnect: context.messenger.onConnect("runtime.onConnect"),
 
@@ -50,16 +52,39 @@ function runtimeApiFactory(context) {
         // TODO(robwu): Validate option keys and values when we support it.
 
         extensionId = extensionId || extension.id;
         let recipient = {extensionId};
 
         return context.messenger.sendMessage(context.messageManager, message, recipient, responseCallback);
       },
 
+      connectNative(application) {
+        let recipient = {
+          childId: context.childManager.id,
+          toNativeApp: application,
+        };
+        let rawPort = context.messenger.connectGetRawPort(context.messageManager, "", recipient);
+        let port = rawPort.api();
+        port.postMessage = message => {
+          message = NativeApp.encodeMessage(context, message);
+          rawPort.postMessage(message);
+        };
+        return port;
+      },
+
+      sendNativeMessage(application, message) {
+        let recipient = {
+          childId: context.childManager.id,
+          toNativeApp: application,
+        };
+        message = NativeApp.encodeMessage(context, message);
+        return context.messenger.sendMessage(context.messageManager, message, recipient);
+      },
+
       get lastError() {
         return context.lastError;
       },
 
       getManifest() {
         return Cu.cloneInto(extension.manifest, context.cloneScope);
       },
 
--- a/toolkit/components/extensions/ext-runtime.js
+++ b/toolkit/components/extensions/ext-runtime.js
@@ -12,19 +12,16 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/ExtensionManagement.jsm");
 
 var {
   EventManager,
   SingletonEventManager,
   ignoreEvent,
 } = ExtensionUtils;
 
-XPCOMUtils.defineLazyModuleGetter(this, "NativeApp",
-                                  "resource://gre/modules/NativeMessaging.jsm");
-
 extensions.registerSchemaAPI("runtime", "addon_parent", context => {
   let {extension} = context;
   return {
     runtime: {
       onStartup: new EventManager(context, "runtime.onStartup", fire => {
         extension.onStartup = fire;
         return () => {
           extension.onStartup = null;
@@ -54,30 +51,17 @@ extensions.registerSchemaAPI("runtime", 
         } else {
           // Otherwise, reload the current extension.
           AddonManager.getAddonByID(extension.id, addon => {
             addon.reload();
           });
         }
       },
 
-      connectNative(application) {
-        let app = new NativeApp(extension, context, application);
-        return app.portAPI();
-      },
-
-      sendNativeMessage(application, message) {
-        let app = new NativeApp(extension, context, application);
-        return app.sendMessage(message);
-      },
-
       get lastError() {
-        // TODO(robwu): Figure out how to make sure that errors in the parent
-        // process are propagated to the child process.
-        // lastError should not be accessed from the parent.
         return context.lastError;
       },
 
       getBrowserInfo: function() {
         const {name, vendor, version, appBuildID} = Services.appinfo;
         const info = {name, vendor, version, buildID: appBuildID};
         return Promise.resolve(info);
       },
--- a/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging.js
@@ -216,18 +216,17 @@ add_task(function* test_sendNativeMessag
 add_task(function* test_disconnect() {
   function background() {
     let port = browser.runtime.connectNative("echo");
     port.onMessage.addListener((msg, msgPort) => {
       browser.test.assertEq(port, msgPort, "onMessage handler should receive the port as the second argument");
       browser.test.sendMessage("message", msg);
     });
     port.onDisconnect.addListener(msgPort => {
-      browser.test.assertEq(port, msgPort, "onDisconnect handler should receive the port as the second argument");
-      browser.test.sendMessage("disconnected");
+      browser.test.fail("onDisconnect should not be called for disconnect()");
     });
     browser.test.onMessage.addListener((what, payload) => {
       if (what == "send") {
         if (payload._json) {
           let json = payload._json;
           payload.toJSON = () => json;
           delete payload._json;
         }
@@ -264,27 +263,24 @@ add_task(function* test_disconnect() {
 
   let procCount = yield getSubprocessCount();
   equal(procCount, 1, "subprocess is running");
 
   extension.sendMessage("disconnect");
   response = yield extension.awaitMessage("disconnect-result");
   equal(response.success, true, "disconnect succeeded");
 
-  yield extension.awaitMessage("disconnected");
-
   do_print("waiting for subprocess to exit");
   yield waitForSubprocessExit();
   procCount = yield getSubprocessCount();
   equal(procCount, 0, "subprocess is no longer running");
 
   extension.sendMessage("disconnect");
   response = yield extension.awaitMessage("disconnect-result");
-  equal(response.success, false, "second call to disconnect failed");
-  ok(/already disconnected/.test(response.errmsg), "disconnect error message is reasonable");
+  equal(response.success, true, "second call to disconnect silently ignored");
 
   yield extension.unload();
 });
 
 // Test the limit on message size for writing
 add_task(function* test_write_limit() {
   Services.prefs.setIntPref(PREF_MAX_WRITE, 10);
   function clearPref() {
@@ -443,17 +439,18 @@ add_task(function* test_child_process() 
   let exitPromise = waitForSubprocessExit();
   yield extension.unload();
   yield exitPromise;
 });
 
 add_task(function* test_stderr() {
   function background() {
     let port = browser.runtime.connectNative("stderr");
-    port.onDisconnect.addListener(() => {
+    port.onDisconnect.addListener(msgPort => {
+      browser.test.assertEq(port, msgPort, "onDisconnect handler should receive the port as the first argument");
       browser.test.sendMessage("finished");
     });
   }
 
   let {messages} = yield promiseConsoleOutput(function* () {
     let extension = ExtensionTestUtils.loadExtension({
       background,
       manifest: {
--- a/toolkit/components/extensions/test/xpcshell/test_native_messaging.js
+++ b/toolkit/components/extensions/test/xpcshell/test_native_messaging.js
@@ -1,13 +1,14 @@
 "use strict";
 
 /* global OS, HostManifestManager, NativeApp */
 Cu.import("resource://gre/modules/AppConstants.jsm");
 Cu.import("resource://gre/modules/AsyncShutdown.jsm");
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/Schemas.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 const {Subprocess, SubprocessImpl} = Cu.import("resource://gre/modules/Subprocess.jsm");
 Cu.import("resource://gre/modules/NativeMessaging.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
 
 let registry = null;
@@ -251,30 +252,33 @@ while True:
   } else {
     yield OS.File.writeAtomic(scriptPath, `#!${PYTHON} -u\n${SCRIPT}`);
     yield OS.File.setPermissions(scriptPath, {unixMode: 0o755});
     manifest.path = scriptPath;
     yield writeManifest(manifestPath, manifest);
   }
 
   let extension = {id: ID};
-  let app = new NativeApp(extension, context, "wontdie");
+  let context = new ExtensionUtils.BaseContext("testEnv", extension);
+  let app = new NativeApp(context, "wontdie");
 
   // send a message and wait for the reply to make sure the app is running
   let MSG = "test";
   let recvPromise = new Promise(resolve => {
     let listener = (what, msg) => {
       equal(msg, MSG, "Received test message");
       app.off("message", listener);
       resolve();
     };
     app.on("message", listener);
   });
 
-  app.send(MSG);
+  // This is equivalent to NativeApp.encodeMessage
+  let {buffer} = new TextEncoder().encode(JSON.stringify(MSG));
+  app.send(buffer);
   yield recvPromise;
 
   app._cleanup();
 
   do_print("waiting for async shutdown");
   Services.prefs.setBoolPref("toolkit.asyncshutdown.testing", true);
   AsyncShutdown.profileBeforeChange._trigger();
   Services.prefs.clearUserPref("toolkit.asyncshutdown.testing");