8

I am learning about Transferable Objects: http://updates.html5rocks.com/2011/12/Transferable-Objects-Lightning-Fast

They seem pretty awesome and I would like to use them in my extension to speed up passing data from inside of an iframe to outside of an iframe.

I've got this example code working, but it uses a Web Worker:

var s = document.createElement("script");
s.type = "text/js-worker";
s.innerHTML = 'console.log("i ran!");';
document.body.appendChild(s);

var blob = new Blob(Array.prototype.map.call(document.querySelectorAll("script[type=\"text\/js-worker\"]"), function (oScript) {
    return oScript.textContent;
}), { type: "text/javascript" });

var worker = new Worker(window.URL.createObjectURL(blob));

var arrayBuffer = new ArrayBuffer(1);

worker.onmessage = function (oEvent) {
    console.log("Called back by the worker!\n");
};

console.log("worker:", worker);
worker.postMessage(arrayBuffer, [arrayBuffer]);

if (arrayBuffer.byteLength) {
    console.error("nope");
} else {
    console.log("it worked");
}

Does anyone have any information about support for, or a timeline for support for / a crbug for using a port like:

var port = chrome.runtime.connect({
    name: 'youTubeIFrameConnectRequest'
});

//port.postMessage -- transferrable object.

I don't see any support for it or anything about supporting it, but that seems really odd!

Rob W
  • 341,306
  • 83
  • 791
  • 678
Sean Anderson
  • 27,963
  • 30
  • 126
  • 237

1 Answers1

14

Messages that go through the extension message passing APIs are always JSON-serialized. This format is not only used for passing messages between background page and content scripts, but also with native applications. So, I guess that it's not very likely that the message passing APIs support more items.

A request for the structured cloning algorithm (more powerful than JSON-serialization, less powerful than transferables) was requested in 2012 already (Chromium issue 112163). The issue is still open; someone has suggested to use a SharedWorker as a "trampoline".

The SharedWorker is affected by the same origin policy, so the callers need to reside at the same origin. To achieve this, you could add a page to web_accessible_resources, and embed this page in a frame.

At the end of this answer, I've attached a bare implementation of a trampoline. Create an extension with these files. Then, open a tab. This tab will contain the embedded frame, and the demo will send a message to the shared worker. This message will be transported to the background page, just view the console of the background page to see these messages.
The demo is minimal, you need to implement the port management (destruction) yourself.
The demo does not use transferable message passing (yet), because it's a general implementation that allows multiple ports. If you ensure that at most two ports exist at the same time, then you could change the code to use transferables (transferables only make sense when there's one received and one sender, because the ownership of the object is also transferred).

Special case: Same-process

If all of your code runs in the same process, then you can use a simpler approach without SharedWorkers.

The same origin policy forbids direct access from/to the frame and the extension, so you will use parent.postMessage to cross this bridge. Then, in the onmessage event of the page, you can use chrome.extension.getViews to get a direct reference to the window object of one of your extension pages (e.g. popup page, options page, ...).
From the other pages, chrome.extension.getBackgroundPage() gives a reference to the window object of the background page (for an event page, use chrome.runtime.getBackroundPage(callback)).

If you want to connect two frames, use the Channel messaging API (see whatwg specification and Opera's dev article). With this method, you'll establish a direct connection between the frames, even if they are located on different origins!

Example: Trampoline

worker.js

var ports = [];
onconnect = function(event) {
    var port = event.ports[0];
    ports.push(port);
    port.start();
    port.onmessage = function(event) {
        for (var i = 0; i < ports.length; ++i) {
            if (ports[i] != port) {
                ports[i].postMessage(event.data);
            }
        }
    };
};

trampoline.js

var worker = new SharedWorker(chrome.runtime.getURL('worker.js'));
worker.port.start();
// Demo: Print the message to the console, and remember the last result
worker.port.onmessage = function(event) {
    console.log('Received message', event.data);
    window.lastMessage = event.data;
};
// Demo: send a message
worker.port.postMessage('Hello');

trampoline.html

<script src="trampoline.js"></script>

contentscript.js

var f = document.createElement('iframe');
f.src = chrome.runtime.getURL('trampoline.html');
f.hidden = true;
(document.body || document.documentElement).appendChild(f);

manifest.json

Note: I put trampoline.js as a background script to save space in this answer. From the perspective of the Web worker, it doesn't matter who initiated the message, so I have re-used the code for sending and receiving messages (it's a simple demo, after all!).

{
    "name": "Trampoline demo",
    "version": "1",
    "manifest_version": 2,
    "background": {
        "scripts": ["trampoline.js"],
        "persistent": true
    },  
    "content_scripts": [{
        "js": ["contentscript.js"],
        "matches": ["<all_urls>"]
    }],
    "web_accessible_resources": [
        "trampoline.html"
    ]   
}
Community
  • 1
  • 1
Rob W
  • 341,306
  • 83
  • 791
  • 678
  • +1 One more great answer ! I have one question (unrelated to this answer): I have noticed you always use `document.head/body || document.documentElement`. Why the `documentElement` part ? – gkalpak Jan 13 '14 at 22:39
  • 2
    @ExpertSystem When a content script is run at `document_start`, `document.head/body` is usually `null`. `document.head/body` is always `null` in some documents (e.g. SVG). But all documents have a root, so I add `document.documentElement` as fallback. – Rob W Jan 13 '14 at 22:43
  • @RobW Thank you for your incredibly insightful answers, as always. It's a real pleasure to have you around the chrome-extension development area. – Sean Anderson Jan 13 '14 at 23:04
  • 2
    Thx, Rob. (I believe we should official declare Rob "Jon Skeet of Chrome Extensions" :P) – gkalpak Jan 13 '14 at 23:14
  • `f.style = "display: none !important;"` might be a more reliable way of hiding the frame instead of `f.hidden = true`. If the page has CSS like `iframe { display: block; }`, then the `hidden` attribute won't help. – Miscreant Apr 25 '16 at 01:09
  • One potential problem with this approach is that the page's JavaScript code might delete the `trampoline.html` frame. – Miscreant Apr 25 '16 at 01:16
  • Transferring an arraybuffer using this method doesn't actually work in Chrome. See https://bugs.chromium.org/p/chromium/issues/detail?id=334408 – Bill Schaller Aug 11 '16 at 23:27
  • @BillSchaller In which version? Both methods in this answer work in Chrome 52.0.2743.116 for me. That bug that you're referring to only applies to transferables, not to objects that are copied. – Rob W Aug 12 '16 at 18:36
  • I did try implementing your suggestion regarding transferables, and this demo doesn't work with transferables. You said, "The demo does not use transferable message passing (yet), because it's a general implementation that allows multiple ports. If you ensure that at most two ports exist at the same time, then you could change the code to use transferables (transferables only make sense when there's one received and one sender, because the ownership of the object is also transferred)." – Bill Schaller Sep 02 '16 at 13:28
  • Yep - when that bug is resolved, your method should in theory work fine with transferring transferable objects. – Bill Schaller Sep 13 '16 at 12:45
  • @RobW How do you send messages back and forth between contentscript.js and the instance of trampoline.js loaded in trampoline.html? – Brad.Smith Oct 09 '20 at 20:12