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:
- What happens when I click a link? (Intro to subsystems and overall network and rendering pipeline)
- Nuts and bolts of Gecko C++
- Code generation
- Memory management
- Smart pointers
- Reference counting schemes
- Gecko C++ data structures
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.
/parser
directory- Multiple parsers:
- XML (
/parser/xm
,/parser/expat
) - old-style HTML
/parser/htmlparser
, only used forabout: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
- XML (
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
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
- These definitions are also used to code generate JS bindings in
- Documents, elements all live in
DOM builds up the linked tree of DOM nodes Next, we need to position nodes on a 2D page.
- 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
- Our CSS implementation is in
Given a bunch of boxes, now we need to paint them to the screen.
/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
- Most implementations of the abstract interface live in
- 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.
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
- Fallible allocation:
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
- Used extensively in Gecko
- 3 main kinds of code generation
- XPIDL: old bindings between JS and C++
- IPDL: multi-process communication
- 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
generatesnsIFile.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 ofnsIFoo
's interface in C++- Every interface has a UUID, there are
NS_IFILE_IID
andNS_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
orout
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 forvirtual 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)
orNS_FAILED(val)
, don't check directly - Complete list of errors:
/xpcom/base/ErrorList.h
- Success case:
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 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
TODO
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>
andnsAutoArrayPtr<T>
- Similar to
std::auto_ptr
- Like
auto_ptr
, these are deprecated too
- Similar to
Shared pointers
- We do this by reference counting
AddRef()
andRelease()
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
- found in
nsCOMPtr<T>
- Very old, XPCOM smart pointer
- Used with interface classes: XPIDL,
nsI*
- Cooperates with many other things
- Lots of
do_Thing
helpers
- Lots of
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-threadedNS_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 interfacesNS_IMPL_SUPPORTS(nsThing, nsIThing, ...);
- Same syntax for thread-safe and non-thread-safe classes
- For non-
nsISupports
things:NS_INLINE_DECL_REFCOUNTING
single-threadedNS_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
- Some class returns a pointer to
- 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>
ornsCOMPtr<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
- 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 iffoo
was non-null and supportsnsIBar
- Handles null-checking
foo
, looking up interface UUID fornsIBar
, looking up list of interface UUIDs supported byfoo
- Because QI does the null-check for you, you can QI through multiple interfaces and only null-check once at the end.
- 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
frominit()
and relay to JS as a JS Exception
- Makes it easy to get
- C++ syntax:
- construction:
nsCOMPtr<nsIFoo> f = do_CreateInstance("@mozilla.org/foo;1");
- initialization:
nsresult rv = f->init();
- construction:
- JS syntax:
- construction:
let f = Cc["@mozilla.org/foo;1"].createInstance(nsIFoo);
- initialization:
f.init();
- construction:
- 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
- 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
- Weirdness is especially likely if you
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
- defined in
- Syntax examples:
do_GetService
:nsCOMPtr<nsICatchyService> service = do_GetService("@mozilla.org/catchy;1");
- faster
services
global:nsCOMPtr<nsICatchyService> service = services::GetCatchyService();
- 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.
- e.g. browser code will QI to
- Long, ugly chain of operations to do something relatively simple
- To make matters worse, it's not always clear if you should
do_GetInterface
orQueryInterface
- 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 annsIBar
fromfoo
?
nsCOMPtr<nsIInterfaceRequestor> iir = do_QueryInterface(foo);
nsComPtr<nsIBar> bar = do_GetInterface(iir);
bar
is non-null iffoo
was non-null and supportednsIInterfaceRequestor
and knew how to providensIBar
things- null-check
bar
just once at the end and you're done
- Example: given
- Again: you may see this, but don't use it in new code.