Skip to content

Instantly share code, notes, and snippets.

@jaredhirsch
Last active May 6, 2024 23:28
Show Gist options
  • Save jaredhirsch/da72e201d2fccde404efcbbae5715a1c to your computer and use it in GitHub Desktop.
Save jaredhirsch/da72e201d2fccde404efcbbae5715a1c to your computer and use it in GitHub Desktop.
My notes from the C++ and Gecko onboarding video

These notes are based on the 2016? Mozilla C++ and Gecko onboarding presentation by froydnj: https://mozilla.hosted.panopto.com/Panopto/Pages/Embed.aspx?id=65922cea-26e0-4d3b-a5da-ac1600012f45

The original slides are still available, which link off to some historical MDN pages (you can find them on the internet archive in many cases): https://docs.google.com/presentation/d/1ZHUkNzZK2TrF5_4MWd_lqEq7Ph5B6CDbNsizIkBxbnQ/edit#slide=id.p


Basic topics to cover:

  • Subsystems in Gecko
  • XPCOM strings and datatypes
  • infallible alloc
  • coding style
  • C++ guidelines

Additional topics suggested by Gecko experts:

  • Refcounting
  • Smart pointers
  • IDLs: XPIDL, WebIDL, IPDL
  • Electrolysis (e10s) (TODO: this video predates fission and should be brought up to date)
  • NSPR
  • Threading
  • C++ visibility

Outline of the talk:

  1. What happens when I click a link? (Intro to subsystems and overall network and rendering pipeline)
  2. Nuts and bolts of Gecko C++
  3. Code generation
  4. Memory management
  • Smart pointers
  • Reference counting schemes
  1. Gecko C++ data structures

What happens when I click a link?

Sequence: Network -> Parser -> DOM -> Styles -> Layout -> Display List -> Paint -> Composite

Docshell is where it all begins: Docshell sets up, then network loads

  • /docshell directory (see also /uriloader)
  • URI loading starts
  • Handles navigating between docs (back-forward movement)
  • Starts doc loading
  • Sets things up for network
  • Manages session history
  • See also uriloader (we probably should merge these eventually)

Networking code

  • /netwerk dir (the misspelling is deliberate and historical)
  • /netwerk/dns has DNS-related code
    • We use the OS for DNS resolution
    • We add in other stuff
    • DNS is done off the main thread and parallelized
  • /netwerk/protocol has code for protocols (HTTP, FTP, WebSocket, ...)
  • /netwerk/cache2 handles document caching
    • We cache documents on disk for speed
    • There's old code in /netwerk/cache, unclear why it's not deleted. Ignore it.
  • /netwerk/cookie manages cookies

Once the network returns data, DocShell sets up listeners to handle the results.

Next: the parser, which handles the network response.

Parsing

  • /parser directory
  • Multiple parsers:
    • XML (/parser/xm, /parser/expat)
    • old-style HTML /parser/htmlparser, only used for about:blank as of 2016
    • HTML5 (/parser/html)
    • Old-style parsers had a lot of quirks at handling parsing errors
    • HTML5 is much better specified for error handling
    • Expat is ~20 years old, we use it for XML parsing
      • It's very simple and very fast

As the parser parses, it builds DOM nodes for all elements Loading other formats (CSS, JS, images) is driven by the parser, but handled elsewhere.

DOM

  • dom directory
  • Absolutely massive directory
    • Documents, elements all live in dom/base, /dom/html
    • Various web APIs often have individual subdirectories, like /dom/indexedDB
    • WebAPI definitions are imported into /dom/webidl for spec conformance verification
      • These definitions are also used to code generate JS bindings in /dom/bindings

DOM builds up the linked tree of DOM nodes Next, we need to position nodes on a 2D page.

Layout

  • Translates the DOM tree to 2D canvas.
  • Every visible DOM node gets its own Frame, then Layout figures out how to position the Frames relative to each other
  • /layout directory
  • also massive
    • Our CSS implementation is in /layout/style
    • /base, /generic: Frames (box around each element), Display Lists (organizing Frames so we can paint them quickly)
    • /layout/svg holds the SVG code
    • /layout/xul XUL

Given a bunch of boxes, now we need to paint them to the screen.

Graphics

  • /gfx directory
  • also large, but mainly because we use third-party libraries
  • We have our own code for some 2D primitives in /gfx/2d, /gfx/thebes
  • Thebes: our old way of rendering to the screen
    • old enough that it doesn't map well to modern graphics APIs
    • rewrite in progress (as of 2016)
  • Layers for compositing in /gfx/layers
    • All the boxes we had get organized into Layers
    • The static parts of the page are grouped together as one layer, paint that into a texture on the graphics card
    • Things that are changing get their own layers, sent off as textures to the graphics card
    • Ship all the layers off to the GPU to be squashed together and composited all at once
  • Lots of third-party graphics code managed by the gfx team:
    • /angle, /cairo, /graphite2, /harfbuzz, /ots, /skia, /ycbcr

For this introductory talk, we're skipping the final stages of rendering.

What else is in Gecko?

Storage in /storage

  • We use SQLite for many things
  • IndexedDB, localStorage use SQLite
  • Browser history (the Places DB)
  • Cookies
  • ServiceWorker cache API is backed by SQLite
  • Not an ORM. We run SQL strings everywhere.
  • MozStorage provides a nice multithreaded API over SQLite
    • e.g. run query in background thread, then return results async

Images, /image

  • We wrote a .bmp decoder
  • For newer formats, we use third-party libraries: libpng, libjpeg, libgif
  • Decoding images & transforming into suitable form for the gfx code
  • Decoding is done off the main thread
  • We use a thread pool to parallelize up to the computer's limits

Media in /media

  • Largely imported code for sound, image, video handling
  • Also where (imported) WebRTC implementation lives

Widget code in /widget

  • Interface between Gecko and OS native event and window handling
  • Abstraction layer (generic IDLs)
  • Separate directories for various platforms:
    • /android, /cocoa (mac), /donk (B2G, now deleted as of 2021), /gtk (linux), uikit (iOS), /windows

IPC (inter-process communication) code lives in /ipc

  • Code for managing multiple processes
  • Electrolysis (e10s) separates out browsing duties:
    • Parent process (handles user interaction)
    • Child proc: web content code, JS, etc. for responsiveness & security
  • This code originally came from Chromium ~5 years ago (i.e. 2011 I guess?)
    • It's now been modified beyond all mergeability. An unintentional fork.
    • If you hit bugs, likely worth looking upstream for existing fixes.
  • Big chunks here:
    • sending / receiving messages
    • Object serialization / deserialization
    • A separate event loop (more on this in a bit)
    • IPDL code generator

JavaScript engine in /js

  • Our JS implementation: parsing, interpretation, JIT compiler, GC, etc.
  • To encourage embedding, we have public interfaces in /js/public
    • But we don't invest in this right now
  • The source code lives in /js/src

XPConnect: /js/xpconnect

  • "Deep Magic": manages reflection of objects between JS and C++
  • Also defines our security architecture for JS
    • See Script Security in source docs for details

MFBT /mfbt

  • "Mozilla Framework Based on Templates" (backronym and quite a stretch)
  • Code shared between Gecko and the JS engine
    • Classes we find useful
    • Polyfills for C++ stdlib things
    • The stdlib is inconsistent across platforms, so we have code to smooth it out
    • Close to C++11, aiming to delete large chunks of /mfbt someday

XPCOM /xpcom

  • "Cross-platform COM" (component object model)
  • Idea: use code in multiple languages via shared language-agnostic interfaces
    • This was inspired by some Microsoft ideas from the 90s
  • We tried to do this across platforms, with varying degrees of success.
  • Lots of building blocks in here:
    • Data structures (arrays, hashtables, strings)
    • Core event loop (timers, threads)
      • Main thread and nonmain threads
      • Interacts with IPC event loop in various ways
    • Parts of the JS interface to chrome code (XPConnect uses this stuff)
      • Interacts with XPConnect code, mostly machine-dependent parts
    • Not quite complete abstract IO interface (stream abstraction that provides bytes, etc)
      • Most implementations of the abstract interface live in /netwerk for historical reasons
    • Cycle collector: part of the reference counting memory management code
      • Much more on this later in the talk

NSPR /nspr

  • Netscape Portable Runtime, 20+ years old
  • Abstracts threads, locks, I/O, logging, etc.
  • We use NSPR when it's convenient
    • Sometimes we have nice wrappers (XPCOM has other nice wrappers for some threading stuff)
    • Sometimes we use things directly (I/O)
    • Sometimes stdlib has better alternatives
  • Slowly working on removing NSPR
    • NSPR is a separate repo and team from Gecko
    • Separate release schedule, etc.

Part 2: Gecko C++

C++ Style Guide

  • We have a style guide
  • Not all code conforms to the style guide
    • We're working on it
  • Please use the style guide for new code
    • Follow local conventions when necessary
    • /dom, /layout are stricter than /gfx
    • Use clang-format if you like

C++ Features

  • We don't use exceptions
    • Historically, exceptions weren't good on some platforms
    • They've gotten better, but converting Gecko to be exception-safe would be a huge chore
  • We don't use RTTI (dynamic_cast, e.g.)
    • Bloat, doesn't work with JS-implemented things
    • We already have an equivalent dynamic cast that's JS compatible
    • RTTI not supported on Android
  • We have a page with our C++ feature support matrix across C++ versions and different compilers

Brief note on portability

  • Across platforms, rate of new feature adoption differs, quality of implementation differs
  • Guarantees provided by interfaces may not be strong enough for us
    • e.g. we need memory reporting, std::vector doesn't support this
  • Hence, we reimplement the wheel in some places

C++ std:: classes

  • Historically, we avoid std:: things
  • We don't use exceptions, std does
  • It's not necessarily available everywhere
  • Please use Gecko versions if possible
  • Please file bugs on Gecko versions if you find an API annoying

Infallible allocation (new T(...) or new T[])

  • By convention, if out of memory, allocation will crash, rather than returning a null pointer
  • We don't return null, so we never need to rely on consistent null-checking
  • Useful for debugging things
    • e.g. if you try to allocate 100MB, crash immediately.
    • Otherwise, you get a null pointer back, sometime later you may dereference a null pointer--but it's very hard to see how/where that happened.
    • So, this makes things simpler to debug
    • Also shorter: no need for lots of null checks
  • If you need an array but don't control the allocation amount, we do have options
    • Fallible allocation: new (fallible) T(...)
    • Similar to nothrow in stdlib, predates it

C++ Visibility

  • We ship one big shared library, xul.dll, /xul.so
  • Improves startup time
  • Exporting symbols from XUL is expensive
    • Runtime penalties, size penalties
    • Exposed interface penalties (malware hooks, etc)
  • Strive to export as little as possible
    • Generally the Right Thing happens by default

Threads

  • Main (UI) thread
    • Handles all user interaction, most content stuff
    • Important to avoid long blocking operations
    • Particularly important to avoid I/O on the main thread
  • Lots of (mostly idle) helper threads
    • SQLite, media/image decoding, network cache, JS JIT compilation, JS GC, timers

3. Code Generation

  • Used extensively in Gecko
  • 3 main kinds of code generation
    1. XPIDL: old bindings between JS and C++
    2. IPDL: multi-process communication
    3. WebIDL: new way of doing bindings between JS and C++
  • We have other, one-off code generators throughout the tree
  • All written in Python

XPIDL files

  • Most useful for understanding how we get from JS to C++
  • "Cross-platform IDL"
  • Defining interfaces for either C++ or JS to implement
  • Generates C++ class boilerplate
  • Generates metadata for calling into C++ from JS (argument conversions)
  • Docs on MDN (RIP, TODO find them and pull into firefox docs somewhere)

XPIDL: Generated code

  • Generates headers for each .idl in $OBJDIR/dist/include
  • nsIFile.idl generates nsIFile.h
  • IDL can define multiple interfaces per file => generated header will have multiple class definitions
  • Each interface nsIFoo generates:
    • nsIFoo class definition (empty methods)
    • NS_DECL_IFOO macro for declaring all of nsIFoo's interface in C++
    • Every interface has a UUID, there are NS_IFILE_IID and NS_IFILE_STR macros for the UUID
    • other macros, rarely needed

Some IDL -> C++ examples:

IDL: const unsigned long NORMAL_FILE_TYPE=0; (note: you don't get strongly-typed enums) C++: enum { NORMAL_FILE_TYPE=0 };

IDL: void append(in AString node); C++: NS_IMETHOD Append(const nsAString&);

  • Note the IDL parameters need to be annotated with in or out to indicate if they are passed in or received out

IDL: bool exists(); C++: NS_IMETHOD Exists(bool*);

IDL: readonly attribute long long fileSize; C++: NS_IMETHOD GetFileSize(int64_t*);

  • By convention, readonly attributes return a getter

IDL: attribute AString leafName; C++: NS_IMETHOD GetLeafName(nsAString&); and NS_IMETHOD SetLeafName(const nsAString&)

  • By convention, modifiable attributes generate getter and setter in C++

  • By convention, return values are a pointer.

  • NS_IMETHOD is short for virtual nsresult + windows-specific goo

  • nsresult is our status code

    • Success case: NS_OK
      • Please don't use the other success values
    • Failure: NS_ERROR_FAILURE and many others
      • Please return the most specific error available
    • Test the return value with NS_SUCCEEDED(val) or NS_FAILED(val), don't check directly
    • Complete list of errors: /xpcom/base/ErrorList.h

Methods

  • Return values are passed through outparams
  • Only set outparam when you're returning NS_OK
  • Don't use the outparam at caller until you check NS_SUCCEEDED
  • Always propagate errors to caller(s)
  • XPConnect maps errors to JS exceptions automatically

Getters / Setters

  • Both can fail
  • Same outparam convention as methods

IPDL

  • IPDL files describe multi-process messaging "protocols
    • List of messages sent by parent or child
  • Code generator generates the uninteresting, repetitive parts
    • e.g. serialization / deserialization
  • Protocols are sorted into a hierarchy
    • e.g. PContent does top-level e10s per tab
    • PContent manages other protocols
    • Similarly, child has its own hierarchy / tree of protocols
  • Provides C++ structure for the interesting parts (defines virtual handler methods)
  • Lives in ipc/ipdl/ipdl.py
    • Not great fun to work with, infrequently modified
    • C++ code goes to $OBJDIR/ipc/ipdl
    • C++ headers to $OBJDIR/ipc/ipdl/_ipdlheaders
    • Headers organized by C++ namespace

WebIDL

TODO

Memory Management

Owned pointers

std::unique_ptr - encapsulates lots of details of passing ownership of pointers around (for exclusive-ownership)

  • Not available because not all our platforms support it

But we do have Mozilla::UniquePtr (in mfbt)

  • Same interface as std::unique_ptr
  • Anywhere you might need to pass raw pointers around, use UniquePtr instead

The old way: (gone from Gecko as of 2021, it seems)

  • nsAutoPtr<T> and nsAutoArrayPtr<T>
    • Similar to std::auto_ptr
    • Like auto_ptr, these are deprecated too

Shared pointers

  • We do this by reference counting
  • AddRef() and Release() methods
    • Doesn't matter virtualness or not
    • You just need the names and parameters, if any
  • Unlike std::shared_ptr, which manages proxies to objects, in Gecko, objects manage their own refcounts directly
  • If you just want to pass around shared data, std::shared_ptr is easier
  • For Gecko, we control all the data, so we just manage refcount directly

Reference counting: thread safety

  • Thread safety: thread-safe and non-threadsafe options are supported
  • std::shared_ptr only available in thread-safe version
  • If you're passing objects among threads, please use thread-safe refcounting
  • DEBUG builds verify thread usage by:
    • marking the owning thread that allocates the object using nonsafe reference counting
    • all future refcount operations will check if they are on the owning thread, and crash if not
    • else, bad things may happen in non-debug builds

Smart pointers

  • Used to avoid needing to manually call AddRef() / Release()
  • RefPtr<T>
    • found in /mfbt
    • Used for concrete classes
  • nsCOMPtr<T>
    • Very old, XPCOM smart pointer
    • Used with interface classes: XPIDL, nsI*
    • Cooperates with many other things
      • Lots of do_Thing helpers

Cycles

  • Reference counting is not perfect
  • Easy to create cyclic references => memory leaks
  • Not just C++ <=> C++ cycles, either.
    • Cycles can include JS objects!
  • Avoiding this: we tried avoiding cycles, using raw pointers (scary)

Refcounting: Cycle Collector

  • Rather than manually break cycles, periodically detect & collect cycles
    • Integration with JS GC to trigger this
    • JS GC triggers things, then cycle collector looks for cycles
  • If an object participates in cycle collection, AddRef() / Release() do extra work to notify cycle collector
  • Generally only for DOM things (not network or gfx)
  • Lots of obtuse macros for making this almost-nice

Refcounting: declaration

  • Macros handle lots of this
  • For nsISupports things (anything with XPIDL associated to it):
    • NS_DECL_ISUPPORTS single-threaded
    • NS_DECL_THREADSAFE_ISUPPORTS thread-safe
    • Drop these macros in your class declaration

Refcounting: definition

  • In concrete nsThing.cpp, list concrete classname and list of implemented interfaces
    • NS_IMPL_SUPPORTS(nsThing, nsIThing, ...);
  • Same syntax for thread-safe and non-thread-safe classes
  • For non-nsISupports things:
    • NS_INLINE_DECL_REFCOUNTING single-threaded
    • NS_INLINE_DECL_THREADSAFE_REFCOUNTING multi-threaded

Refcounting: implementation

  • Generally, the macros provide all you'll need
  • See xpcom/glue/nsISupportsImpl.h for the grotty details

Leak Checking

  • DEBUG only
  • We have leak checking tooling for refcounted things
  • Not as complete as Valgrind / Asan
  • Refcounted classes are automatically leak checked
  • Individual malloc/new isn't counted
    • ASan / Valgrind test runs to check leaks, but totally separate from our debug builds

Leak Checking: macros

  • Also possible to leak check non-reference counted classes
  • Helpful to ensure new code manages memory correctly
  • Include reference to MOZ_COUNT_CTOR(klassname) in all constructors
  • In destructor, MOZ_COUNT_DTOR(klassname)

Limitations:

  • Checking hierarchies is difficult
    • We generally avoid hierarchies in Gecko
    • MOZ_COUNT_CTOR_INHERITED is available, but rarely used
    • Generally just use MOZ_COUNT_CTOR in the base class
  • Checking templates classes is difficult
    • Solution: use a non-templated base class
    • Same approach as inherited / hierarchies

More info:

  • BloatView, MDN article (RIP TODO find this)
    • Runs on DEBUG tests by default
    • Also has info on leaks not reproducible locally

Refcounting: ownership transfers

  • Want to avoid passing raw pointers to refcounted classes
  • You have a smart pointer to some class, allocate it, do stuff with it
  • How do you get a pointer to that object out of your method?
  • Can't just return the raw pointer: the ref will get released, you'll be passing a pointer to freed memory 🙀

Ownership transfer strategies

  • As usual with Gecko, there are many ways to do it
  • Oldest: outparam T**
    • Some class returns a pointer to T*, so, class pointer pointer
    • Common with XPIDL-style interfaces
  • More common on C++ side: already_AddRefed<T> return value
    • Generally indicates new object entirely yours
    • Means: I'm giving you a pointer that has had AddRef() called on it
    • Typically used in create interfaces as a return value
    • Means also: need to Release() it at some point
    • Can't do much with it except assign it to a smart pointer
  • "Modern" way: return RefPtr<T> or nsCOMPtr<T>
    • Pass around or return smart pointers themselves
    • With recent C++ standard changes, now actually efficient
      • In the past, might or might not be efficient
    • Not currently super safe with our current smart pointer implementation with current compilers
    • Planning to make improvements

Ownership transfers: summary table

Pointer outparam already_AddRefed<T> Smart pointer
Identifying feature T** argument (usually last in args list, in XPIDL) return type return type
Passing ownership from a smart pointer p.forget(outparam); (stores the wrapped pointer into the outparam) return p.forget(); return p;
Caller (Starting with RefPtr <T> p; o->Method(getter_AddRefs(p)); p = o->Method(); p = o->Method();
Use cases XPIDL-style interfaces, especially for helper methods called by XPIDL methods static Create() methods many

QueryInterface: XPCOM's dynamic_cast

Next couple sections: brief digression into nuts & bolts of XPIDL, XPCOM

  • Every class/interface in Gecko gets a UUID
    • tied specifically to the interface
  • Given a random pointer, nsISupports pointer
  • Can check if the pointer supports a given interface by passing it a UUID
  • We have infrastructure to simplify these checks

do_QueryInterface()

  • Given pointer nsCOMPtr<nsIFoo> foo
  • If you want to know if it supports nsIBar
  • Just do nsCOMPtr<nsIBar> bar = do_QueryInterface(foo);
  • bar will be non-null if foo was non-null and supports nsIBar
  • Handles null-checking foo, looking up interface UUID for nsIBar, looking up list of interface UUIDs supported by foo
  • Because QI does the null-check for you, you can QI through multiple interfaces and only null-check once at the end.

do_CreateInstance()

  • Used for creating objects across C++ and JS (for XPIDL)
  • Because we don't use exceptions in C++, the constructor cannot return an error
  • So, we construct a minimal object, then call an init() method to finish initializing
  • To avoid using UUIDs, we have a thing called "contract ID" (typically defined in IDL)
    • Example: "@mozilla.org/foo;1".
    • Stays the same even if the interface changes (hence, the UUID must be updated)
    • Reduces churn due to UUID evolution / interface evolution
  • Syntax enforces creation/initialization separation
    • Makes it easy to get ns_result from init() and relay to JS as a JS Exception
  • C++ syntax:
    • construction: nsCOMPtr<nsIFoo> f = do_CreateInstance("@mozilla.org/foo;1");
    • initialization: nsresult rv = f->init();
  • JS syntax:
    • construction: let f = Cc["@mozilla.org/foo;1"].createInstance(nsIFoo);
    • initialization: f.init();
  • On the C++ side, typically better to just directly init via the constructor, instead of doing all this indirection
  • On the JS side, this machinery is unavoidable, so these patterns are necessary

do_GetService()

  • Services are XPCOM's singletons
  • They are lazily created by do_GetService()
  • Enforced only by convention: if you do_CreateInstance instead, strange things may happen
    • Weirdness is especially likely if you do_CreateInstance more than once
  • do_GetService is prohibitively slow for common services
  • Instead, we have a faster way to get common services: services:: (aka Services.jsm on JS side)
    • defined in xpcom/build/ServiceList.h
  • Syntax examples:
    • do_GetService: nsCOMPtr<nsICatchyService> service = do_GetService("@mozilla.org/catchy;1");
    • faster services global: nsCOMPtr<nsICatchyService> service = services::GetCatchyService();

do_GetInterface()

  • Was meant to replace attribute access
  • nsIInterfaceRequestor is a fairly common interface
  • Used to ask an object if it has something corresponding to some interface UUID
  • Could return anything that conforms to the UUID:
    • might return a direct member of the concrete class
    • or the class itself
    • or something else entirely that happens to implement that interface UUID among many others
  • This is not a good pattern
    • e.g. browser code will QI to nsIInterfaceRequestor,
    • then get some interface,
    • then query that interface to nsIInterfaceRequestor, etc.
  • Long, ugly chain of operations to do something relatively simple
  • To make matters worse, it's not always clear if you should do_GetInterface or QueryInterface
  • Don't use this in new code.
  • Instead, just provide an attribute or method to access a concrete thing, and sidestep the UUID machinery
    • Example: given nsCOMPtr<nsIFoo> foo. How to get an nsIBar from foo?
    1. nsCOMPtr<nsIInterfaceRequestor> iir = do_QueryInterface(foo);
    2. nsComPtr<nsIBar> bar = do_GetInterface(iir);
    • bar is non-null if foo was non-null and supported nsIInterfaceRequestor and knew how to provide nsIBar things
    • null-check bar just once at the end and you're done
  • Again: you may see this, but don't use it in new code.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment