Skip to content

Instantly share code, notes, and snippets.

@marcoscaceres
Last active August 24, 2020 01:38
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save marcoscaceres/ffcdde82394b27a7cb7cd22be755a0c1 to your computer and use it in GitHub Desktop.
Save marcoscaceres/ffcdde82394b27a7cb7cd22be755a0c1 to your computer and use it in GitHub Desktop.
How we implemented web share

How we implemented Web Share

Written by: Marcos Cáceres Technical reviewers: Kagami Sascha Rosylight, Sid Vishnoi.

This post describes how we implemented the Web Share API in Gecko, which is exposed as a DOM API to the Web. As it can be challenging to know how to implement new features in Gecko, it's intended to serve as a reference guide for other Gecko developers who need to implement similar DOM APIs in the future. For the impatient, the final implementation is available in Phabricator.

Before you start - some general advice about how things work

The following subsections give some general advice about what you should expect as you embark on implementing a new DOM feature.

For your own mental health, it is very important that before you start implementing you discuss your implementation plan with the DOM Team. Because documentation is sorely lacking, or just grossly out of date for most of Gecko, talking to the other DOM Team members may save you literally weeks of needless pain, frustration, self-loathing, upset, and depression. If you do experience any of those feelings, that's (unfortunately) very normal too.

But don't fret! We are here to help and we all go through it, and we are here to help you get through it too!

Coding as a social process

It is very important that before you start implementing you discuss your implementation interest and plan with the DOM Team. Because a feature may not be well-spec’d enough to implement or a feature is harmful to the Web according to Mozilla standard position. Or, because documentation can be lacking or out of date, talking to the other DOM Team members may save you literally weeks of needless frustration and depression. Writing an architecture design document helps reviewers understand your thoughts and allows them to provide early feedback before actual implementation. Lazy image loading architecture review and OOP-iframe event delivery in Fission document are great examples. We understand how the feelings could be when working on such a complex large code base. But don't fret! We are here to help and we all go through it, and we are here to help you get through it too!

The other critical thing to remember is that coding is a social process: remember to be in contact with other folks throughout implementation. This includes code reviewers, product managers, folks in other teams depending on your feature, UI/UX folks, dev tools folks, and other DOM Team members, to name a few. Having good working relationships with, and support from those around you will help you significantly get your code actually shipping in the browser.

And even if you do your best - priorities can change, immediate user needs can change (thinking of COVID-19 as an example), so your work may get put on hold. We appreciate your understanding. Don’t get too attached to code. It happens. We do want you to know that your contributions have already been there and we appreciate that. Take what you learned and move on.

Code Review times

Code reviews can take a long time, plan ahead and have your expected timeline communicated in advance! Although we’d like to get reviews in 2-3 days, allow 1-3 weeks between review cycles sometimes because many review requests are awaiting, people are busy, they get sick, or they go on well-earned vacations, or have work travel. Kind repeated reminder - an effective approach to ensure smooth code review is to communicate ahead or to pay attention to a potential reviewers’ availability warning when you submit a patch.

If reviewers need a long time to be able to get to review your code, you can expect they at least communicate the review plan/timeline with you in a few days. If you are not getting any response after 2 weeks-ish, contact a manager or a triage owner to help you find a different reviewer. It’s likely that we have to switch reviewers 2-3 times. But this is also why having an architecture design document is helpful, so anyone who gets involved (even later) has a central source of references to catch up.

We view reviews as a priority. However, DOM Peers can sometimes be tricky to pin down, especially for a big new feature, because there are so few, and so many people depend on them - so we appreciate extra patience waiting for them as well as timeline communication ahead. Cultivating more DOM peers is ongoing and it takes time. Here is the list of DOM peers. See below about having a code buddy, which can do a first round of reviews for you.

Timelines

Generally plan that your 3-6 months for a feature, starting from implementation plan discussion, patch review, test writing & CI failure fixing, issue fixing on Nightly/Beta, then shipping to release and finally fixing issues on release for stabilization. It may look scary long to you, but thinking about the fact that we are shipping features to 100 million people, that may help you realize why it takes time..

Times can vary a lot due to the scope or size of a feature. It could take simply 2 months, but also possible takes about 12-18 months to actually ship a feature in a product, though 2-5 years is not uncommon - shipping features to 100 Million people comes with inherent privacy, security, and commercial risks. Stuff taking ages is a normal part of the process (see our “Intent to Prototype”, “Intent to Ship”, and getting a “Standards Positions” - each of which can take months). Times will decrease a little with practice and as there are more communication with reviewers, prior to or during the implementation - and hopefully this guide will help speed things up for you.

Get a coding buddy!

It's also strongly recommended that you find a coding buddy to work with you. That is, someone who can sanity check things along the way, and someone you can call upon quickly if you have questions or get stuck - or someone who can give you suggestions about different ways of doing things. Treat this as a peer programming task. If you don't have a buddy, then ask for one and we DOM Team can find someone for you.

Give back

By implementing a feature, you have a core responsibility to the Web to provide feedback to the standard process: If you find spec bugs, ambiguities, or things you think could be done better, please file a bug on the spec. If you find no issues, and you enjoyed reading the spec, please send that as feedback! Spec writers really value bugs and/or being told their specs make sense.

You can also give back by sharing your own implementation experience with other Gecko engineers. Consider writing your own helpful guide.

With those disclaimers out of the way, let's get started!

What is Web Share

In short, it's an API that lets you share URLs and bits of text primarily with native apps, and eventually (🤞) with other web applications.

In JS, the API is used like this:

// Share must be "triggered by user activation", such as a click.
async function click() {
  try {
    await navigator.share({ url, text, title });
  } catch (err) {
    // Various types of DOMExceptions...
  }
}

What we will cover in this document

This document will cover end-to-end how to implement a DOM API. The API is exposed to a web page and communicates with the OS (in this case, “shares data”). The result of sharing the data is eventually send data back to a web page, causing a promise to settle. We illustrate the path we will take along this coding journey below. It’s helpful to see which files are involved, and how they relate to each other.

Through our journey, we are going to do a deep dive into some of these things:

  • Adding a browser pref: "dom.webshare.enabled". This will allow our feature to be shipped in the browser, but not exposed to the web until it's ready.
  • Web IDL: the lingua franca and standard for defining JavaScript interfaces that are exposed to the Web. It's used by all browser engines - though each browser maker has added their own quirky set of extensions! We will get to the Mozilla specific extensions as needed.
  • XPCOM - your worst nightmare but expressed as code. I don't even know how to explain it... imagine everything that was terrible with programming during the 2000s (crazy enterprise Java, COM, etc), and it's basically that.
  • XPIDL - an interface definition language that lets JS and C++ talk to each other outside of the context of a web page (i.e., for internal stuff - mostly used for XPCOM).
  • Gecko Strings - Different kind of strings used by Gecko, along with corresponding APIs... because apparently you need like 10 different kinds of strings to get anything done. To be fair, there is often good reason for the distinction amongst them. But still... they don't make your life any easier and can be super confusing to know when to use which, and you constantly have to switch between them.
  • Creating URLs, Promises, and sending messages between processes.

Almost everything above, except WebIDL, is old, clunky, and should have been "taken out to pasture" a long time ago. Unfortunately they are core-infrastructure of Gecko, so we kinda have to live with what we got.

Gentle start - the Web IDL

All Web APIs start with two things: as set of WebIDL "fragments" that define the "shape" of the API (i.e., what its inputs and outputs are, what methods, and properties are exposed, etc.), and some text that describes what to do when a developer uses JavaScript to interface with the API (e.g., what should happen when a method is called)... what we call "spec prose".

In general, WebIDL fragments are pretty self-explanatory. For the Web Share API, the WebIDL is:

partial interface Navigator {
  [SecureContext] Promise<void> share(optional ShareData data = {});
};

dictionary ShareData {
  USVString title;
  USVString text;
  USVString url;
};

So, basically, it extends the browser's navigator object and adds a share() method that returns a Promise that eventually resolves with void (i.e., undefined in JS).

The [[SecureContext](https://heycam.github.io/webidl/#SecureContext)] extended attribute basically translates to "don't expose this in to http" (or whatever the browser deems to be a context that is not secure - but generally we mean "http:" contexts). The share() method expects an optional argument, of type ShareData - and when that argument is missing, it just behaves as if a JS object {} had been passed.

Finally ShareData just has a couple of optional properties (called "members") that can be passed along as the data to be shared.

Although not clear from the IDL above, the specification actually requires that at least one of ShareData's members be passed along. To quote the spec:

If none of data's members title, text, or url are present, return a promise rejected with a TypeError.

So, all the following would return a rejected promise:

  navigator.share();
  navigator.share({});
  navigator.share({"bananas": ""});

If you'd like, you can try the above out in your favorite browser that supports Web Share. If you want to try it in Firefox, and it hasn't shipped yet, go to: about:config, search for "dom.webshare.enabled", and set that pref to true.

To implement the above, we will be either modifying or creating about 25 files.

Let's get started.

Implementing navigator.share()

In Gecko, all web-exposed WebIDL interface definitions live in the dom/webidl directory. Generally speaking, the file name will be the name of the interface you want to modify. The Web IDL we need for our implementation is as follows:

// https://wicg.github.io/web-share/#navigator-interface
partial interface Navigator {
  [SecureContext, Throws, Pref="dom.webshare.enabled"]
  Promise<void> share(optional ShareData data = {});
};

// https://wicg.github.io/web-share/#sharedata-dictionary
dictionary ShareData {
  USVString title;
  USVString text;
  USVString url;
};

Note that we've had to make a few additions to the Web IDL originally found in the Web Share spec. We added the following "extended attributes" (an extended attribute modifies the behavior of the API in some interesting, yet spec-conforming, way):

  • Throws - tells the binding layer that, despite returning a Promise, this can also reject immediately. In C++, this will give us a handy object that we can use to reject the returned Promise immediately. If this method didn't return a promise, we would use this annotation to throw DOM exceptions or similar exceptions into JS-land.
  • Pref="dom.webshare.enabled" - that's the preference we will use to turn this feature on and off... eventually on by default if when this lands.
  • The comments we added don't do anything, obviously... but it's convention to add them, and DOM Peers will be looking for them to be there during code review.

So, we need to track down the Navigator interface. Predictably, it lives in a file called Navigator.webidl. All we need to do is add the above Web IDL fragments to Navigator.webidl.

If you tried to compile at this point it would fail. The generated binding layer will be looking for the actual implementation of the share() method somewhere in Gecko's codebase. All WebIDL interfaces are implemented in a similarly named c++ file pair: in this case Navigator.h and Navigator.cpp.

We will start with adding the "dom.webshare.enabled" Preference, as that is super quick. Then we can move onto the share() method definition of Navigator.h and then move onto implementing it in Navigator.cpp.

Adding the "dom.webshare.enabled" pref

The process of adding a preference for a Gecko/Web feature is super simple. Just open up modules/libpref/init/StaticPrefList.yaml and add your preference there. The file is well documented (yay!).

Note: modifying the StaticPrefList.yaml file will cause a complete recompile to happen when you ./mach build again. Depending on your setup, this can set you back 1 hour! Try not to modify things here unless you really have to.

For Web Share, our preference definition was:

# WebShare API - exposes navigator.share()
- name: dom.webshare.enabled
  type: bool
  value: false
  mirror: always

According to the file:

mirror indicates how the pref value is mirrored into a C++ variable.

And:

The mirror variable is always kept in sync with the pref value. This is the most common choice.

So we just went with that. Generally speaking, for usage with Web IDL, you can just set this and forget it until you need to flip the value to true.

It's worth having a quick read of the StaticPrefList.yaml file's documentation... especially if you are setting a new pref to do something else.

Navigator.h

As you would expect, the definition in Navigator.h just matches and uses Gecko's implementation of the various classes and structs, namely:

  • Promise - representing a JS Promise in Gecko.
  • ShareData - the binding layer created this for us from the IDL definition, but we need to declare that we are using it.

You can see the implementation of Navigator.h on Phabricator.

The Share() declaration

Thanks to Throws above, the share() method signature ends up as:

  Promise* Share(const ShareData& aData, ErrorResult& aRv);

Notable is that ErrorResult& aRV serves as a handy way to immediately reject a promise in JS: as the method returns a Promise, the Web IDL binding layer wraps the whole thing in a Promise. So aRv, if ever needed, can simply just rejects that wrapping promise. We shall see this in the actual implementation of .Share() in Navigator.cpp. It's quite useful and keeps our code really clean!

The other thing to note is the declaration of the private class member mSharePromise:

  RefPtr<Promise> mSharePromise;

The mSharePromise represents an attempt for a web page to perform a share. And, as a limitation of most OSs, a user can only perform one share at a time. We will now see how that works as a kind of "sentinel" (or simple state machine guard) in Navigator.cpp.

Navigator.cpp

We will now walk through the complete implementation of Web Share in Navigator.cpp. The following sections show the code as we implemented it: i.e., from top-to bottom, following what the W3C Spec says to do.

If you'd first like to see the completed code, take a quick look at Navigator.cpp in Phabricator.

Cycle collection and memory management bits

As we mentioned previously, our mSharePromise has two purposes:

  1. it represents the on-going attempt to "perform a web share".
  2. if not nullptr, it prevents a web page from performing more than one share at a time.

As such, mSharePromise is a long-lived object that will eventually need to be garbage collected by the browser (e.g., the tab is closed, the page navigates, or whatever).

The long-lived nature means we need that object "to stay alive" (and not be garbage collected) until the promise is resolved - thus it needs to participate in "cycle collection". Cycle collection is Gecko's fancy garbage collector. You can read about cycle collection on MDN, but basically comes down to hooking into the right C++ macros.

With some trial and error, we randomly chose to hook into the "UNLINK" macro:

NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Navigator)
...
  NS_IMPL_CYCLE_COLLECTION_UNLINK(mSharePromise)
...
NS_IMPL_CYCLE_COLLECTION_UNLINK_END

And, thanks to a recommendation from a reviewer, "TRAVERSE" too (make sure that you put UNLINK and TRAVERSE in the right block):

NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Navigator)
  ...
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSharePromise)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END(Navigator)

Only a few people know how the cycle collector beast works - so it's best to seek out help and ask someone on the DOM Team. Another huge problem with the cycle collector is that it's a stack of C++ macros, upon macros... upon more macros. So it's a complete nightmare to decipher what each marco does - plus it gets super into the deep Gecko weeds.

Anyway, don't feel bad if you don't understand it. Everyone knows it's insane and the docs are less than great. However, this can be a source of frustration because if you pick the wrong one, then your object can get garbage collected and unexpectedly vanish, leading to crazy bugs that only randomly occur.

So, don't waste too much time! Ask for help early on which macro to hook into. Additionally, not all macros are documented. It may turn out that you can use one of the undocumented macros and you'll get some unexpected benefit. Yep... good times.

The Share() method

The Share() method represents the primary entry point of the implementation. When navigator.share() is called in JS, that's where our C++ code takes over and we can start implementing what is in the spec.

Promise* Navigator::Share(const ShareData& aData, ErrorResult& aRv) {

As we mentioned previously, the Share() method is a Promise-returning method that can immediately reject. We can see the utility of this straight away:

  if (NS_WARN_IF(!mWindow || !mWindow->GetDocShell() ||
                 !mWindow->GetExtantDoc())) {
    aRv.Throw(NS_ERROR_UNEXPECTED);
    return nullptr;
  }

As in, "No window? No "doc shell"? no 'extant' doc (see below)? well, that's unexpected and exceptional. Reject!" (aRv.Throw(NS_ERROR_UNEXPECTED)). The cool part here is that we didn't need to create a rejected Promise... we could have, but aRv did the trick for us.

About "extant" in GetExtantDoc(), it means "still in existence; surviving" 🤓. We had to Google it too, don't worry. Cool word thought. And, our of interest, NS_WARN_IF gets printed in the terminal console.

Putting mSharePromise to good use

Now we can see explicitly how our friend mSharePromise serves as a guard against multiple simultaneous shares:

  if (mSharePromise) {
    NS_WARNING("Only one share picker at a time per navigator instance");
    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
    return nullptr;
  }

In JavaScript, imagine this as the problem we are solving for:

<script>
  async function clickHandler() {
    const p = navigator.share({title: "share 1"}); // clickHandler created
    navigator.share({title: "share 2"}); // mSharePromise rejects with InvalidStateError.
    await p; // resolves at some point. mSharePromise now nullptr.
    navigator.share({title: "share 3"}); // we are good to share again :)
  }
</script>
<button onclick="clickHandler">share!</button>

Following along with the spec

Continuing on, you can see we've copy/pasted spec prose directly into our C++ code as a guide.

  // If none of data's members title, text, or url are present, reject p with
  // TypeError, and abort these steps.
  bool someMemberPassed = aData.mTitle.WasPassed() || aData.mText.WasPassed() ||
                          aData.mUrl.WasPassed();

Including spec text into code is always useful, especially in situations where what the spec asks for is tricky or ambiguous. But note that there is a risk that the implementation and the spec text can get out of sync - so try to be careful and make sure that you keep the implementation up-to-date with the spec.

Historical aside: Mozilla and other browser vendors fought hard to legally have the right copy/paste W3C spec prose into source code. Believe it or not, this was not allowed because of copyright restrictions until 2015! The W3C then introduced a new software-document license with made this possible. Before that, to copy/paste spec prose meant you were violating copyright law.

Localization of error messages

Note that we may have a legitimate error condition here: although none of the ShareData members are required, at least one member MUST be present - otherwise, it's considered a TypeError.

  if (!someMemberPassed) {
    nsAutoString message;
    nsContentUtils::GetLocalizedString(nsContentUtils::eDOM_PROPERTIES,
                                       "WebShareAPI_NeedOneMember", message);
    aRv.ThrowTypeError<MSG_MISSING_REQUIRED_DICTIONARY_MEMBER>(message);
    return nullptr;
  }

At Mozilla, we always make an effort to localize the error messages and to make them as helpful as possible for web developers.

In order to do this, we store localized error messages into the dom/locales/en-US/chrome/dom/dom.properties file.

As you can see from the above, we pull our customized/localized error message from:

    nsContentUtils::GetLocalizedString(nsContentUtils::eDOM_PROPERTIES,
                                       "WebShareAPI_NeedOneMember", message);

And we store it into nsAutoString message. This gives us our first introduction into Gecko strings too. Gecko has something like 10 different kinds of strings you can use. We will come back to that (because... yes... it's crazy 😜)!

Note another old school thing that is convention at Mozilla: instead of assigning values to variables, variables whose values are to be assigned need to be declared first and then passed to functions. I.e., where one would normally expect:

  auto message = nsContentUtils::GetLocalizedString(
    nsContentUtils::eDOM_PROPERTIES, "WebShareAPI_NeedOneMember"
  );

We generally see this pattern a lot:

    nsAutoString message;
    fillMessage(message);

On the one hand, that can be annoying because it means you can't rely on auto. On the other hand, it's good to be explicit about types.

Other thing we note is "WebShareAPI_NeedOneMember", which is a unique identifier for the string we need. Long story short, the browser should pull the right string based on the user/developer's language settings.

And in dom.properties, we define localized strings like so:

# LOCALIZATION NOTE: Do not translate title, text, url as they are the names of JS properties.
WebShareAPI_NeedOneMember=title or text or url member of the ShareData dictionary. At least one of the members is required.

Notice how we leave special instructions for those doing the localization into another language. That tells our localizer community members what they should localize and what they should leave in English (because they are JS property names or whatever).

Back to our implementation of Share() in Navigator.cpp...

Creating a URL

Web Share allows web pages to share URLs. But those URLs might be any URL part (e.g., "{url: "./../foo/bar"}"), so we need to make sure that:

  1. the passed URL is valid.
  2. we convert the passed URL into an absolute URL, resolved against the document's "base URL".

Creating URLs that are based off the Document's base URL is really straight forward thanks to a newly added ResolveWithBaseURI() method. It takes customized base URIs into consideration in a simple way.

Note that instead of URL objects, Gecko uses its own old school nsIURI object. It dates back to 1999, the Netscape Navigator days!

  // null checked above
  auto doc = mWindow->GetExtantDoc();

  // If data's url member is present, try to resolve it...
  nsCOMPtr<nsIURI> url;
  if (aData.mUrl.WasPassed()) {
    auto result = doc->ResolveWithBaseURI(aData.mUrl.Value());
    if (NS_WARN_IF(result.isErr())) {
      aRv.ThrowTypeError<MSG_INVALID_URL>(aData.mUrl.Value());
      return nullptr;
    }
    url = result.unwrap();
  }

The nsIURI API is a bit weird and takes some getting used to. For instance, instead of accessing uri->href to get back the string representation of the URL, you call uri->spec instead. We will see more of that later.

Oh my, strings

As mentioned a few times already, there are a significant number of string types that Gecko relies on. Without sending you straight to the Mozilla internal string guide, which is quite useful and accessible, so well-worth reading, we are going to explore a little bit what happens in the following code.

DOMString and enums coming into C++ from JavaScript via Web IDL are always UTF-16 encoded. However, large parts of Gecko's infrastructure rely on UTF-8 strings instead. Being Object oriented, and for efficiency, C++ forces us to put those bytes into a suitable class container. These containers are either "wide" (holding 16 or more bits chars) or "narrow" (holding 8-bit chars).

As a general rule, "narrow" strings always have a "C" in the name: nsACString, nsCString, nsAutoCString. And wide strings don't: nsAString, nsString, and nsAutoString respectively. The container classes use the same APIs, so that keeps things somewhat sane when working with them.

Note: The string guide provides a handy table that can help you pick what you need: "What class to use when".

Let's first look at the code, and then we can explain a bit about what we are doing (and we'll learn a little bit about Mozilla's String API). Our goal in the code below is to convert the textual values passed with aData (title and text members) into UTF-8.

  // Process the title member...
  nsCString title;
  if (aData.mTitle.WasPassed()) {
    title.Assign(NS_ConvertUTF16toUTF8(aData.mTitle.Value()));
  } else {
    title.SetIsVoid(true);
  }

  // Process the text member...
  nsCString text;
  if (aData.mText.WasPassed()) {
    text.Assign(NS_ConvertUTF16toUTF8(aData.mText.Value()));
  } else {
    text.SetIsVoid(true);
  }

We use a "narrow" nsCString because, for whatever reason, Gecko's Inter-Process communication (IPC) only supports using nsCString. When we implemented this feature, we were unaware that IPDL supported marking things as optional via (?) - so we ended up treat all required data as mandatory (did we mention that IPDL’s documentation is awful?). This turned out to be ok, however, as there is a somewhat elegant solution for overcoming that - particularly for nsCString.

When we allocate nsCString title, it's literally empty - but not void!. This becomes important later, so we will come back to that. As we already discussed, data coming in from JS is UTF-16, so we need to convert it to UTF-8. We do that with the trusty NS_ConvertUTF16toUTF8() function. We simply pass in the .Value() of aData.mText.

Now, as we already mentioned, we need a way of distinguishing between "the string was passed, but empty" VS "the string was not passed at all, but it's required... so here it is." That's where the .SetIsVoid(true) comes into play. No idea what the true boolean trap does however.

Now that we've made copies and text to be shared, along with a resolved URL, we are almost ready to share all this data. Before we do though, we need to perform one more check.

Making compromises for the sake of interoperability

Generally, we prefer to do "triggered by user activation" checks (i.e., this feature can only be used by a click, press, etc.) earlier in the code. This is because we generally want to avoid doing a bunch of data checks early only to throw them all out if the one check fails.

However, because Safari and Chrome had already implemented this check late, we ended up copying their behavior for the sake of interoperability: we could argue with them to change it, but sometimes it's not worth the effort - Especially if it doesn't really buy us or our users too much. In any case, UserActivation::IsHandlingUserInput() is a very handy method to know, as we use it a lot on in DOM APIs.

  // The spec does the "triggered by user activation" after the data checks.
  // Unfortunately, both Chrome and Safari behave this way, so interop wins.
  // https://github.com/w3c/web-share/pull/118
  if (!UserActivation::IsHandlingUserInput()) {
    NS_WARNING("Attempt to share not triggered by user activation");
    aRv.Throw(NS_ERROR_DOM_NOT_ALLOWED_ERR);
    return nullptr;
  }

Creating promises in C++

Creating Web-compatible Promises in C++ is pretty straight forward. They just need a JS global object and a way of reporting if the creation process failed:

  // Let mSharePromise be a new promise.
  mSharePromise = Promise::Create(mWindow->AsGlobal(), aRv);
  if (aRv.Failed()) {
    return nullptr;
  }

As we know, a promise represents an ongoing operation that is eventually resolved or rejected.

The ongoing operation in our case is the "share", which generally involves calling an API on the OS side to actually pass data to another application. Because this is an ongoing operation that may never finish, we don't want to block the main, or "child" process, waiting for the share to happen: we want to send the data to the "parent" process and let it interact with the OS instead.

However, once our data reaches the "parent" process, the share can fail for a number of reasons: the user can decide they don't want to share that data, or the data that was passed may be incompatible in some way with the target application. These exceptional conditions must cause the promise to be rejected.

Finally, if nothing goes wrong and the share succeeds, the promise will be resolved successfully.

We will now look at how this child-parent communicate, and how the promise is used to handle both success and failure in this bi-directional communication process.

Preparing to go off main-thread

As mentioned briefly above, Gecko uses its own "Inter-Process Communication" (IPC) model to send messages between processes. IPC is a protocol that is described with its own interface definition language called the "Inter-Process-Communication Protocol Definition Language" or IPDL for short. Similarly to Web IDL, IPDL is a "language allowing C++ code to pass messages between processes or threads in an organized and secure way."

Much like Web IDL, IPDL gets transformed into C++ files. These come in "parent" and "child" pairs, and we are required to handle what happens when a child sends a message to a parent (i.e., when a child process wants to "share data" with a parent process).

Note: Unfortunately, IPDL is tremendously complicated and poorly documented. What limited documentation currently exists is outdated and mostly unhelpful (e.g., it talks about creating browser plug-ins, which Firefox mostly deprecated years ago for instance). We will do our best to provide basic guidance, but we strongly advise you to speak to a DOM Peer before you start putting down any code or, worse, trying to design a new IPDL protocol. Chances are, you can use an existing IPDL protocol, as we did with Web Share.

As we described in the Creating Promises in C++ section above, what we want to achieve in our code is as follows:

  1. Gather data to be shared into some transferable format/structure.
  2. Send that data to the "parent" process.
  3. "Await" a response from the parent, which will be either success or failure, thus allowing us to either resolve or reject a promise.

In order to achieve the above we will hook into the existing "dom/ipc/PWindowGlobal.ipdl" protocol file. PWindowGlobal.ipdl is a convenient place to put things into that it operates on the Window object. So, if we can get a reference to the Window object (mWindow in most DOM code), we can easily send a message to the parent process - as we will soon see! We will just quickly note that things we define in the PWindowGlobal.ipdl file will either end up in WindowGlobalChild.h or WindowGlobalParent.h - we will also see details of this in the upcoming sections below.

So, now that we have a rough overview of IDL, let's get our data into a data structure we can’t transfer from one process to another.

struct IPCWebShareData

We start by defining the data structure we need in order to perform the share. IPDL supports "structs", but is limited to only supporting nsCString for strings:

struct IPCWebShareData
{
  nsCString title;
  nsCString text;
  nsIURI url;
};

Thankfully it supports passing nsIURIs, so we don't need to serialize URLs into a string. As we mentioned previously, we don't have any way to express parts of the struct being optional or nullable, but that's ok. The nsCString handled that for us by allowing us to say when a string is "void".

async Share() protocol

Now for the more interesting part! We define the IPDL "Share()" method for "web sharing":

  async Share(IPCWebShareData aData) returns (nsresult rv);

Let's break down the above IPDL - because it generates and does a lot of cool things.

The Share() method generates a method pair:

  • one on the "child" (WindowGlobalChild.h), prefixed with Send - so SendShare().
  • one on the "parent" (WindowGlobalParent.h), prefixed with Recv - so RecvShare().

Marking the method as async generates a promise-like signature on the SendShare() method, so:

  • SendShare(data, shareResolver, shareRejector), where:
    • data is IPCWebShareData
    • And shareResolver and shareRejector are lambdas that are used to fulfil a promise. We shall see these soon!

And, on the RecvShare() - it adds a way of receiving the data we send, and a promise resolver:

  • RecvShare(IPCWebShareData&& aData, ShareResolver&& aResolver)

As you can see, ShareResolver is a custom resolver generated specifically for our method. And, it expects to be resolved with returns's data type nsresult! That's really useful.

So now we have our communication channel. We can send and receive messages async 😎👍.

Back to Navigator.cpp - preparing data to share

Now we have all we need to send the share to the parent process: We have all our data: the title, the text, and the URL, and we've appropriately indicated if they are void or not.

So, we create our data:

  IPCWebShareData data(title, text, url);

And now we get the "child" (WindowGlobalChild) that contains the SendShare() method, which will allow us to perform the share asynchronously:

  auto wgc = mWindow->GetWindowGlobalChild();
  if (!wgc) {
    aRv.Throw(NS_ERROR_FAILURE);
    return nullptr;
  }

Promise Resolvers and Rejectors

As we mentioned above, a consequence of having stated in IPDL that Share() is async is that wgc->SendShare() knows that it's a promise - so it was generated in such a way that it accepts:

  • The data - our trusty IPCWebShareData struct.
  • A "resolver" function - which will resolve our friend mSharePromise in JS land.
  • A "rejector" function - which, if needed, can reject our friend mSharePromise in JS land with an appropriate DOM Exception.

So, let's set up the resolver as a lambda:

  auto shareResolver = [self = RefPtr<Navigator>(this)](nsresult aResult) {
    MOZ_ASSERT(self->mSharePromise);
    if (NS_SUCCEEDED(aResult)) {
      self->mSharePromise->MaybeResolveWithUndefined();
    } else {
      self->mSharePromise->MaybeReject(aResult);
    }
    self->mSharePromise = nullptr;
  };

Nothing too magical in the above: it handles both expected success and the expected failure case. If sharing succeeded, we can now resolve mSharePromise, which resolves the promise in JS land. Similarly, if aResult was not a success, we are able to reject mSharePromise in JS land with the appropriate DOM Exception.

The final thing we do is self->mSharePromise = nullptr; at the end. That now frees the web page to again be allowed to call navigator.share().

And now we can also set up the share rejector:

  auto shareRejector = [self = RefPtr<Navigator>(this)](
                           mozilla::ipc::ResponseRejectReason&& aReason) {
    // IPC died or maybe page navigated...
    if (self->mSharePromise) {
      self->mSharePromise = nullptr;
    }
  };

This one basically handles the IPC pipe breaking or anything that's super unexpected. In theory, if shareRejector lambda is getting called, something really bad has happened (☠️🔥).

Do the share

And we are away! We can now "do the share" by passing in our data, our resolver, and our rejector.

  // Do the share
  wgc->SendShare(data, shareResolver, shareRejector);
  return mSharePromise;
}

In summary:

  • We've returned the promise to JavaScript,
  • we've kicked off an operation over in the parent process,
  • and we've set everything up to be garbage collected as needed.

That's pretty cool! 😎 Now we need to receive the share-data over on the "parent" process.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment