mirror of
https://github.com/carbon-language/carbon-lang.git
synced 2026-09-24 13:40:11 +01:00
C++ Interop: Mapping pointer types (#6357)
This proposal defines direct, zero-overhead mappings from C++ object pointer types and `std::nullptr_t` to corresponding Carbon types.
This commit is contained in:
@@ -0,0 +1,421 @@
|
||||
# C++ Interop: Mapping pointer types
|
||||
|
||||
<!--
|
||||
Part of the Carbon Language project, under the Apache License v2.0 with LLVM
|
||||
Exceptions. See /LICENSE for license information.
|
||||
SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||
-->
|
||||
|
||||
[Pull request](https://github.com/carbon-language/carbon-lang/pull/6357)
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Abstract](#abstract)
|
||||
- [Problem](#problem)
|
||||
- [Background](#background)
|
||||
- [Proposal](#proposal)
|
||||
- [Details](#details)
|
||||
- [Non-nullable pointers to object types](#non-nullable-pointers-to-object-types)
|
||||
- [Inconsistent type sugar](#inconsistent-type-sugar)
|
||||
- [Non-nullable pointers to void](#non-nullable-pointers-to-void)
|
||||
- [Nullable pointers and `Core.Optional`](#nullable-pointers-and-coreoptional)
|
||||
- [`nullptr_t`](#nullptr_t)
|
||||
- [`nullptr`](#nullptr)
|
||||
- [Indexing and pointer arithmetic](#indexing-and-pointer-arithmetic)
|
||||
- [Rationale](#rationale)
|
||||
- [Future work](#future-work)
|
||||
- [Alternatives considered](#alternatives-considered)
|
||||
- [Map all pointers to `T*`](#map-all-pointers-to-t)
|
||||
- [Map `void*` to `()*`](#map-void-to-)
|
||||
- [Map `void*` to `u8*`](#map-void-to-u8)
|
||||
- [Map `void f()` to `fn Cpp.f() -> Cpp.void`](#map-void-f-to-fn-cppf---cppvoid)
|
||||
- [Use the name `Core.CppCompat.Void` instead of `Core.CppCompat.VoidBase`](#use-the-name-corecppcompatvoid-instead-of-corecppcompatvoidbase)
|
||||
- [Map `Cpp.void` to `()` instead of `Core.CppCompat.VoidBase`](#map-cppvoid-to--instead-of-corecppcompatvoidbase)
|
||||
- [Do not provide `Cpp.void` at all](#do-not-provide-cppvoid-at-all)
|
||||
|
||||
<!-- tocstop -->
|
||||
|
||||
## Abstract
|
||||
|
||||
This proposal defines direct, zero-overhead mappings from C++ object pointer
|
||||
types and `std::nullptr_t` to corresponding Carbon types.
|
||||
|
||||
## Problem
|
||||
|
||||
In order to support interoperability between C++ and Carbon, we need to map
|
||||
types in each language to the other language. Currently we do not have a defined
|
||||
mapping for pointer types, which are an important concept in both languages.
|
||||
However, a direct mapping is not appropriate, as pointer types have different
|
||||
semantics in Carbon versus in C++.
|
||||
|
||||
## Background
|
||||
|
||||
Pointer types in Carbon and in C++ have different semantics. C++ pointers can be
|
||||
null, can be indexed if they point into an array, treat any non-array object as
|
||||
pointing to an array of a single element for indexing purposes, and can point to
|
||||
a position one past the last element of an array. Carbon pointers allow none of
|
||||
these things, and always point to an object.
|
||||
|
||||
There are three kinds of pointer type in C++:
|
||||
|
||||
- A _pointer to object type_ is a pointer whose pointee type is an object
|
||||
type, such as a scalar type or a class type.
|
||||
- A _pointer to void_ is a pointer type whose pointee type is (possibly
|
||||
`const` and/or `volatile`) `void`.
|
||||
- Collectively, pointer to object types and pointer to void types are
|
||||
called _object pointer types_, despite `void` not being an object type.
|
||||
- A _pointer to function type_, or equivalently _function pointer type_, is a
|
||||
pointer whose pointee type is a function type.
|
||||
|
||||
C++ pointers to object types support arithmetic operations:
|
||||
|
||||
- Addition and subtraction are supported, treating pointers as an affine space
|
||||
over the integers. Formally, arithmetic is only permitted between pointers
|
||||
that point to elements of the same array object, but in practice these
|
||||
operations are used substantially more broadly.
|
||||
- Pointers can be compared relationally, if they point to subobjects of the
|
||||
same object. The exact rules are a little more subtle than this, but
|
||||
formally, comparisons across distinct complete objects have unspecified
|
||||
results and most comparisons within a complete object have specified
|
||||
results. In practice, pointer comparisons on modern architectures are
|
||||
treated as a total order, except that the status of comparisons of
|
||||
bitwise-equal pointers where one points past the end of one complete object
|
||||
and the other points to the start of a distinct complete object are
|
||||
nebulous.
|
||||
- Pointers support array indexing notation `p[i]` -- and, to the surprise of
|
||||
many and the delight of few, `i[p]` -- which is mostly equivalent to
|
||||
`*(p + i)`.
|
||||
|
||||
In addition, the type of `nullptr`, known commonly by its standard library alias
|
||||
`std::nullptr_t`, is important to modern C++ as a mechanism for forming null
|
||||
pointer values. This type is not a pointer type, but implicitly converts to
|
||||
every pointer type, forming a null pointer value of that type.
|
||||
|
||||
C++ also has pointers to members, `T C::*`, which are a distinct type from
|
||||
pointer types in C++.
|
||||
|
||||
## Proposal
|
||||
|
||||
This proposal defines a mapping from C++ object pointer types and
|
||||
`std::nullptr_t` into Carbon types, introducing new Carbon types as necessary to
|
||||
provide the mapping:
|
||||
|
||||
| C++ type | Carbon type | Notes |
|
||||
| ---------------- | ----------------------------------------------- | ------------------------------------------- |
|
||||
| `T*` | `Core.Optional(T*)` | null pointers map to `None` |
|
||||
| `T* _Nonnull` | `T*` | non-nullable pointers don't need `Optional` |
|
||||
| `const T*` | `Core.Optional(const T*)` | |
|
||||
| `void*` | `Core.Optional(Core.CppCompat.VoidBase*)` | `void` maps to `()` in some other contexts |
|
||||
| `void* _Nonnull` | `Core.CppCompat.VoidBase*` | |
|
||||
| `const void*` | `Core.Optional(const Core.CppCompat.VoidBase*)` | |
|
||||
| `nullptr_t` | `Core.CppCompat.NullptrT` | |
|
||||
|
||||
Function pointer types and pointer to member types are out of scope for this
|
||||
propopsal.
|
||||
|
||||
## Details
|
||||
|
||||
### Non-nullable pointers to object types
|
||||
|
||||
C++ doesn't have non-nullable pointer types, but C++ implementations do, in
|
||||
various forms:
|
||||
|
||||
| C++ type | Supported by | Behavior if null |
|
||||
| ----------------------------------------- | ------------ | ---------------- |
|
||||
| `T* _Nonnull` | Clang | Erroneous |
|
||||
| `T* __attribute__((nonnull))` | Clang | Undefined |
|
||||
| `void f(T*) __attribute__((nonnull))` | GCC, Clang | Undefined |
|
||||
| `T* f() __attribute__((returns_nonnull))` | GCC, Clang | Undefined |
|
||||
| `void f(_Notnull_ T*)` | MSVC | Defined |
|
||||
| `_Ret_notnull_ T* f()` | MSVC | Defined |
|
||||
|
||||
We will support and encourage use of Clang's `_Nonnull` annotation, as it is the
|
||||
only form that applies to a general pointer type rather than to a function
|
||||
parameter or return type. Clang's `T* _Nonnull` maps to Carbon's `T*`.
|
||||
|
||||
The `__attribute__((nonnull))` and `__attribute__((returns_nonnull))` forms will
|
||||
also be mapped into non-nullable Carbon pointers, as these attributes are
|
||||
widespread in existing code. A pointer type that is the type of a function
|
||||
parameter or the return type of a function that is annotated with these
|
||||
attributes maps to Carbon's `T*`.
|
||||
|
||||
The MSVC attributes are intended for use by static analysis tools only, not as
|
||||
compiler inputs, so are not suitable for our uses, and are mentioned here only
|
||||
for completeness.
|
||||
|
||||
#### Inconsistent type sugar
|
||||
|
||||
The `_Nonnull` annotation is treated as _type sugar_, not as producing a
|
||||
different type. This has some significant consequences for its use:
|
||||
|
||||
- Type sugar is easily lost through incautious type navigation. Any operation
|
||||
that maps to a canonical type will lose the annotation. This is largely just
|
||||
a constraint that the toolchain implementation be careful, except...
|
||||
- ... type sugar is lost across template instantiations. There is work in
|
||||
Clang to preserve type sugar across template instantiations, but
|
||||
[at this time it is not complete](https://godbolt.org/z/hP35776zj). This
|
||||
means that in templates, the distinction between non-nullable pointers and
|
||||
nullable pointers should be expected to be lost.
|
||||
- The annotation can differ between redeclarations of the same entity.
|
||||
Inconsistent annotations may lead to a type being treated as either nullable
|
||||
or non-nullable. Clang will warn on this in some cases, but the fragility of
|
||||
type sugar means that such warnings should not be expected to be entirely
|
||||
robust.
|
||||
|
||||
The consequences in C++ for losing or incorrectly determining nullability are
|
||||
mostly not too severe -- loss of best-effort diagnostics, and the compiler
|
||||
treating the program as having defined behavior when the rules say its behavior
|
||||
is undefined. But if it causes a type to map to a different type in Carbon, that
|
||||
is potentially a larger issue, as it may affect whether the Carbon program
|
||||
compiles.
|
||||
|
||||
For now we will use these type sugar anontations to inform our type mapping, but
|
||||
will revisit this decision if they are too problematic in practice.
|
||||
|
||||
### Non-nullable pointers to void
|
||||
|
||||
`void` broadly has two different meanings in C++:
|
||||
|
||||
- A complete type with an empty representation, a size and alignment of 1, and
|
||||
a single unique value. This is the meaning that is used by `void f()`,
|
||||
`static_cast<void>(expr)`, and arguably by the special case `int f(void)`.
|
||||
- An incomplete type that is a supertype of all other types (sometimes known
|
||||
as a top type ⊤, or a universal type). This is the meaning that is used by
|
||||
`void*`, as well as related ideas like `dynamic_cast<void*>`. While C++
|
||||
models this form of `void` as incomplete, it would be more accurate to
|
||||
consider it to be abstract.
|
||||
|
||||
There have been long-lived attempts to replace the second meaning in C++ with
|
||||
the first, but so far they have not succeeded. If they do succeed, our approach
|
||||
will need to shift to accommodate that change.
|
||||
|
||||
We map the first kind of `void` to Carbon's `()` type. In particular, a function
|
||||
that returns `void` in C++ returns `()` in Carbon. While these types don't have
|
||||
the same representation in general, they do have the same representation as a
|
||||
return type, with both types corresponding to nothing being returned.
|
||||
|
||||
For the second kind of `void`, we introduce a new compatibility type:
|
||||
|
||||
```carbon
|
||||
abstract class Core.CppCompat.VoidBase {}
|
||||
```
|
||||
|
||||
Notionally, we think of every Carbon type, including types imported from C++, as
|
||||
inheriting from this type, but practically we support a conversion from any
|
||||
pointer type to a pointer to `VoidBase`, and we support a conversion from a
|
||||
value of any type to a value of type `VoidBase`. Other inheritance-based
|
||||
language properties should decide whether to treat `VoidBase` as a supertype of
|
||||
all other types by considering how C++ treats `void` in similar contexts.
|
||||
|
||||
This new type is our primary mapping for `void`; the mapping of `void` return
|
||||
types is treated as a special case that applies only in return position. Because
|
||||
`void` in C++ can only appear in very limited positions, this means that
|
||||
`VoidBase` is used as the mapping for `void` in only the following situations:
|
||||
|
||||
- As the pointee type of a pointer type.
|
||||
- As a type template argument.
|
||||
- As a type of a typedef or type alias.
|
||||
|
||||
This seems like the best balance: for example, given
|
||||
|
||||
```c++
|
||||
typedef void OpaqueObject;
|
||||
void Call(OpaqueObject* handle);
|
||||
```
|
||||
|
||||
... it seems useful to map `Cpp.OpaqueObject*` to `VoidBase*`, not to `()*`. And
|
||||
given:
|
||||
|
||||
```c++
|
||||
template<typename T> T ReturnAT() { return T(); }
|
||||
```
|
||||
|
||||
... the call `Cpp.ReturnAT(VoidBase)` will still have a return type of `()`
|
||||
rather than an abstract return type, because the type appeared in return
|
||||
position after instantiation.
|
||||
|
||||
As a convenience shorthand, the name `Cpp.void` is added to the `Cpp` package
|
||||
and refers to `Core.CppCompat.VoidBase`. C++'s `void* _Nonnull` maps to
|
||||
`Cpp.void*`, and similarly `const void* _Nonnull` maps to `const Cpp.void*`.
|
||||
|
||||
### Nullable pointers and `Core.Optional`
|
||||
|
||||
Nullable object pointers, including nullable pointers to `void`, map to
|
||||
`Core.Optional(T*)`. Null pointer values map to the optional's "none" value.
|
||||
This places a constraint on the implementation of `Core.Optional(T*)` that it
|
||||
has the same ABI as a C++ pointer, including that the "none" state is
|
||||
represented with a C++ null pointer representation.
|
||||
|
||||
While we do not yet have an approved design for the `Core.Optional` type, it is
|
||||
already in use in the design of `for` statements. Adding a design for
|
||||
`Core.Optional` is out of scope for this proposal.
|
||||
|
||||
### `nullptr_t`
|
||||
|
||||
The C++ null pointer type, `decltype(nullptr)`, is exposed in the standard
|
||||
library as `std::nullptr_t`. This type is a scalar type built into the C++
|
||||
language, and as such, we need a custom mapping for it -- our mappings for other
|
||||
categories of C++ types don't cover it.
|
||||
|
||||
C++'s `nullptr_t` has the same representation as `void*`, but its representation
|
||||
comprises only padding bits. This doesn't correspond to any existing type in
|
||||
Carbon, so we introduce a new compatibility type to model it:
|
||||
|
||||
```carbon
|
||||
class Core.CppCompat.NullptrT {
|
||||
adapt MaybeUnformed(VoidBase*);
|
||||
}
|
||||
```
|
||||
|
||||
The C++ type maps to the above type, so after `import Cpp library "<cstddef>";`,
|
||||
it can also be named as `Cpp.std.nullptr_t`.
|
||||
|
||||
#### `nullptr`
|
||||
|
||||
The name `Cpp.nullptr` is added to the `Cpp` package, and refers to the (empty)
|
||||
constant value of type `Core.Cpp.NullptrT`.
|
||||
|
||||
### Indexing and pointer arithmetic
|
||||
|
||||
This proposal provides no support for indexing or pointer arithmetic on Carbon
|
||||
pointers mapped from C++ pointers. Carbon pointers do not support indexing, and
|
||||
the result of mapping a C++ pointer into Carbon would be a pointer that does not
|
||||
support indexing. In the other direction, no mechanism prevents a Carbon pointer
|
||||
from being passed into C++ code and then indexed, even though that is not an
|
||||
operation that would be possible in pure, safe Carbon code.
|
||||
|
||||
Eventually we will need to provide some way to take a C++ pointer and perform
|
||||
the equivalent of indexing into it in Carbon code. However, this mechanism will
|
||||
require additional Carbon language features to be designed before it can be
|
||||
specified. In particular, we will need a safety story for bounds safety, and a
|
||||
type for representing an indexable location in some way, such as an array
|
||||
iterator or array cursor type. As a result, we leave support for indexing and
|
||||
pointer arithmetic as future work.
|
||||
|
||||
## Rationale
|
||||
|
||||
Goals:
|
||||
|
||||
- [Performance-critical software](/docs/project/goals.md#performance-critical-software)
|
||||
- C++ types are mapped to Carbon types with the same representation. This
|
||||
avoids the need for any deep copies on the interop boundary.
|
||||
- [Code that is easy to read, understand, and write](/docs/project/goals.md#code-that-is-easy-to-read-understand-and-write)
|
||||
- The convenience names `Cpp.void` and `Cpp.nullptr` make it easier to
|
||||
write code that interoperates with C++ void pointers and C++ nullable
|
||||
pointers.
|
||||
- [Practical safety and testing mechanisms](/docs/project/goals.md#practical-safety-and-testing-mechanisms)
|
||||
- C++ null pointers cannot "leak" into Carbon non-nullable pointer types,
|
||||
except by violating a "nonnull" annotation in the C++ code. If
|
||||
`_Nonnull` is violated, the result is not undefined behavior unless the
|
||||
pointer is used in a context that would result in undefined behavior in
|
||||
C++, such as a load or store through the pointer.
|
||||
- [Modern OS platforms, hardware architectures, and environments](/docs/project/goals.md#modern-os-platforms-hardware-architectures-and-environments)
|
||||
- We do not assume that a null pointer has an all-zero-bits
|
||||
representation. This is not true in practice on some modern
|
||||
heterogeneous compute architectures.
|
||||
- [Interoperability with and migration from existing C++ code](/docs/project/goals.md#interoperability-with-and-migration-from-existing-c-code)
|
||||
- This proposal significantly enhances interoperability with C++ code, by
|
||||
providing a mapping for very common vocabulary types in C++.
|
||||
|
||||
Principles:
|
||||
|
||||
- [All APIs are library APIs](/docs/project/principles/library_apis_only.md)
|
||||
- The types added to support C++ are all ordinary class types provided in
|
||||
the Carbon prelude.
|
||||
- [Low context-sensitivity](/docs/project/principles/low_context_sensitivity.md)
|
||||
- The Carbon `Cpp.void` type has only one meaning, rather than having a
|
||||
context-sensitive meaning. The C++ `void` type still has two different
|
||||
meanings, and therefore two different mappings into Carbon, but that
|
||||
problem is outside our domain.
|
||||
- [Namespace cleanliness](/docs/project/principles/namespace_cleanliness.md)
|
||||
- The names we are adding to the `Cpp` package, `void` and `nullptr`, are
|
||||
C++ keywords, and so do not conflict with any C++ identifier. While
|
||||
Clang does provide an extension to define entities with these names, for
|
||||
example `int __identifier(void);`, interoperting with such code is not a
|
||||
priority.
|
||||
|
||||
## Future work
|
||||
|
||||
- Add a design for `Core.Optional`, as we now have multiple language proposals
|
||||
that depend upon it.
|
||||
- Design interop for function pointers.
|
||||
- Design interop for pointers to members.
|
||||
- Design an approach for providing the functionality that C++ exposes as
|
||||
pointer indexing and pointer arithmetic.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
### Map all pointers to `T*`
|
||||
|
||||
We could avoid wrapping nullable pointers with `Core.Optional`. However, doing
|
||||
so opens a large hole in Carbon's story for pointers, wherein pointers are not
|
||||
nullable.
|
||||
|
||||
### Map `void*` to `()*`
|
||||
|
||||
We could map `void` to `()` in all contexts, and map C++'s `void*` to Carbon's
|
||||
`()*` -- or rather, to `Core.Optional(()*)`. However, in order to support
|
||||
passing arbitrary Carbon pointers to C++ `void*` parameters, we would need to
|
||||
allow `T*` to implicitly convert to `void*` in Carbon, which means we would need
|
||||
to allow `T*` to implicitly convert to `()*`. Therefore the C++ modeling of
|
||||
`void` as a supertype of all other types leaks out into pure Carbon code. This
|
||||
seems undesirable.
|
||||
|
||||
### Map `void*` to `u8*`
|
||||
|
||||
We could map `void*` to `u8*` or to a pointer to some other byte-like type, to
|
||||
reflect that it represents a pointer to storage. This would result in an N:1
|
||||
mapping from C++ types to Carbon types, because both `void*` and `uint8_t*`
|
||||
would map to the same Carbon type. The same would happen if we picked any other
|
||||
Carbon type that has a corresponding C++ type that is not `void`.
|
||||
|
||||
It's strongly desirable that our mapping between C++ and Carbon types fully
|
||||
round trips, because otherwise passing types between the two languages, such as
|
||||
in metaprogramming or by way of template argument deduction, would be lossy. For
|
||||
example, if both a `vector<void*>` and a `vector<uint8_t*>` map to the same
|
||||
Carbon type `buf(u8*)`, then passing an object of that type from C++ into Carbon
|
||||
and then back into C++ must result in a type that mismatches at least one of the
|
||||
original types.
|
||||
|
||||
It's possible that we could accept some N:1 mappings, but given how common
|
||||
`void*` is on C and C++ API boundaries, the risk of problems seems particularly
|
||||
significant in this case.
|
||||
|
||||
### Map `void f()` to `fn Cpp.f() -> Cpp.void`
|
||||
|
||||
We could use the custom `Cpp.void` type even as a function return type, removing
|
||||
the non-uniformity of mapping it to `()` in function returns and to `Cpp.void`
|
||||
elsewhere. However, `Cpp.void` is an abstract type, so there should not exist
|
||||
initializing expressions of this type.
|
||||
|
||||
We could address that by instead mapping `void` to `partial Cpp.void`, or
|
||||
aliasing `Cpp.void` to `partial Core.CppCompat.VoidBase`, but either way that
|
||||
means that `void*` maps to a type whose pointee doesn't have the abstract /
|
||||
incomplete behavior that we desire.
|
||||
|
||||
### Use the name `Core.CppCompat.Void` instead of `Core.CppCompat.VoidBase`
|
||||
|
||||
We could use a simpler name for the compatibillity type. However, given that
|
||||
there are two different meanings of `void` in C++, having some extra clarity
|
||||
about which meaning is intended seems useful.
|
||||
|
||||
### Map `Cpp.void` to `()` instead of `Core.CppCompat.VoidBase`
|
||||
|
||||
We could pick the other meaning of `void` as the meaning of `Cpp.void`. However,
|
||||
the `()` meaning is only really interesting as a function return type, and there
|
||||
is no reason to reach for `Cpp.void` if that meaning is desired. So mapping
|
||||
`Cpp.void` to `VoidBase` is more useful.
|
||||
|
||||
Also, we want the mappings between C++ and Carbon types to be bidirectional, to
|
||||
the extent that's possible. Mapping Carbon's `()` to C++'s `void` type would
|
||||
mean that we can't consistently map all Carbon tuple types to the same family of
|
||||
C++ tuple types, such as `std::tuple`.
|
||||
|
||||
### Do not provide `Cpp.void` at all
|
||||
|
||||
We could ask developers to write out the name of `VoidBase` when needed. But
|
||||
it's long and cumbersome, and we expect most other C++ types with a
|
||||
corresponding keyword to be provided in the `Cpp` package, so providing it is
|
||||
both useful and improves language consistency.
|
||||
Reference in New Issue
Block a user