User:Ehsan/Safe C++

From MozillaWiki
< User:Ehsan
Revision as of 00:13, 4 July 2015 by Ehsan (talk | contribs)
Jump to navigation Jump to search

This page tries to explain some rough ideas about what a safe subset of C++ suitable for use inside Gecko would look like. The ideas below mostly focus on the high level semantics, and there is a lot of details to be figured out still.

Note that the goal here is protecting against use-after-free bugs, and also some runtime crashes. This proposal specifically doesn't protect against data races as it does not (yet) address mutability.

Owning references

When creating a new object on the heap, use OwningRef<T>, for example:

 OwningRef<T> object(1, 2, 3); // instead of nsRefPtr<T> object(new T(1, 2, 3))

OwningRef's do not convert to a raw pointer. They do provide an operator->(), which can *only* be called as part of a member access. This part will be ensured through custom analysis.

OwningRef's cannot be copied. They can hand out borrowed refs, explained below. They can be moved inside a scope (possibly to the caller of the function) if they haven't handed out borrowed refs. If they have, a custom analysis will prevent the code from being compiled.

After an object has been moved, calling operator->() on it is a compile time error. This will be ensured through a custom analysis.

 void good() {
   OwningRef<T> object(1, 2, 3);
   object->DoSomething();
   OwningRef<T> newObject(std::move(object)); // compiler is happy!
   newObject->DoSomethingElse();
 }
 void bad() {
   OwningRef<T> object(1, 2, 3);
   T* sneaky = object.operator->(); // error: operator->() can only be used to access the members.  nice try though!
   OwningRef<T> newObject1(std::move(object));
   object->DoSomething(); // error: operator->() cannot be used after the object has been moved.
   BorrowedRef<T> borrow1(object.Borrow());
   goo(borrow1);
   BorrowedRef<T> borrow2(object.Borrow());
   quo(borrow2);
   OwningRef<T> newObject2(std::move(newObject1)); // error: Cannot move object because of outstanding borrowed refs!
 }
 
 void takeBorrowed(BorrowedRef<T>);
 void takeOwnership(OwningRef<T>&&);
 void passer() {
   OwningRef<T> object(1, 2, 3);
   takeBorrowed(object.Borrow());
   takeOwnership(object); // This moves the object
   object->DoSomething(); // error: operator->() cannot be used after the object has been moved.
   BorrowedRef<T> borrowed(object.Borrow()); // error: Borrow() cannot be used after the object has been moved.
 }
 
 // Returning an OwningRef to the caller
 OwningRef<T>&& goodCreator() {
   OwningRef<T> object(1, 2, 3);
   goo(object.Borrow()); // Pass a borrowed ref in a temporary to a function
   return std::move(object);
 }
 OwningRef<T>&& badCreator() {
   OwningRef<T> object(1, 2, 3);
   BorrowedRef<T> borrowed(object.Borrow());
   goo(borrowed); // Pass a borrowed ref in a non-temporary to a function
   return std::move(object); // error: Cannot move object because of outstanding borrowed refs!
 }

I believe that all of the checks required for OwningRef should be enforceable at compile time.

Borrowed references

Borrowed references can be obtained from owning references. It is guaranteed at compile time that a BorrowedRef cannot outlive the *validity* of the OwningRef that handed it out. Note that the validity is potentially shorter than the lifetime of the object. See bad() above, for example.

Borrowed references can be copied and moved. They cannot be used to obtain a raw pointer out of the object though, and similar to OwningRef, their operator-> will only be usable for member access.

One very common use case of BorrowedRef is for passing pointers to functions where the function doesn't want to own the data passed to it.

Instantiating a BorrowedRef with global storage is a compile-time error. This is necessary in order to prevent creating global borrowed refs that would prevent doing anything with the OwningRef they were created from after they are assigned to.

I believe that all of the checks required for BorrowedRef should be enforceable at compile time.

Mutability

Making a C++ code base const correct is very difficult. But assuming that works, we should be able to borrow some of Rust's mutability handling mechanisms as well. I have no good ideas here that are baked enough yet.

One part of Rust's semantics that should be possible to implement without relying on const correctness much would be to disallow operator->() on an OwningRef while there are outstanding borrowed refs to it. This will implement something similar to the idea of freezing in Rust, but it remains to be seen how ergonomic that would be (given that outlawing operator-> in that case prevents read-only access to the underlying data as well.)