In C# and Python, people expect to not have dangling references, and for them to be magically solved with GC magic.
This is especially important in C#. In Python you can just be careful and manually avoid dangling, but in C# the compiler will aggressively destroy local variables early if it thinks they are no longer needed, and it can't see when C++ objects store references to each other internally, so mrbind has to actively counteract this, and we can't just rely on the user being careful.
The dangling prevention annotations are unified across target languages, they are implemented in the parser.
We recycle some of Clang's attributes ([[clang::lifetimebound]] and [[clang::lifetime_capture_by(x)]]), which in addition to helping mrbind also give you nice warnings. We also have some of our own attributes, for cases when those are not expressive enough.
In C those attributes have no effect, but we dump them in function comments in a human-readable form.
Each Python/C# object stores a list of (shared references to) other Python/C# objects that it wants to "keep alive", i.e. that should continue to exist while this object exists. In Python, this is accomplished with pybind11::keep_alive(). In C#, we manually maintain such lists in each object. Note that GC is those languages is supposed to be able to handle cyclic references, so those shouldn't be an issue.
When you call a Python/C# function generated by mrbind, the function can modify the keep-alive lists of its parameters, its return value, and of this.
Functions can be annotated to tell mrbind what (a parameter/return value/this) should keep-alive what.
Each function has a list of (holder, target) pairs associated with it at compile-time, where holder and target are any of:
-
A parameter of this function.
-
this, if this is a member function. -
The return value (only allowed for
holder, nottarget).
When the function is called, we do the equivalent of holder.__keep_alive.push_back(target); for every such pair.
To add such pair to a function, you need to add a certain C++ attribute to the target. Which attribute you add determines what the holder is.
holder and target are normally raw pointers or references, or classes holding those, or references to such classes. The annotations are ignored if they don't make sense for those holder and target types, so you can freely add them in templates, and they'll be ignored if not needed for specific template arguments.
If holder is the return value, add [[clang::lifetimebound]] to the target. It can be either immediately after a parameter name, or after the method parameter list to apply to this (after const if any). For example:
struct A
{
std::string arr[3];
// holder = result, target = this
std::string &front() [[clang::lifetimebound]] {return arr[0];}
const std::string &front() const [[clang::lifetimebound]] {return arr[0];}
};
// holder = result, target = param
std::string &foo(std::string &ref [[clang::lifetimebound]]) {return ref;}
std::string &foo(std::string &a [[clang::lifetimebound]], std::string &b [[clang::lifetimebound]]) {return a < b ? a : b;}Note that when applied to a constructor, lifetimebound considers the constructed object to be the return value.
If holder is a function parameter or this, add [[clang::lifetime_capture_by(holder)]] to the target, where holder is either a parameter name or this. For example:
struct A
{
std::string &ref;
};
// holder = vec, target = ref
void foo(std::string &ref [[clang::lifetime_capture_by(vec)]], std::vector<A> vec)
{
vec.push_back({ref});
}
struct B
{
std::vector<A> vec;
// holder = this, target = ref
void foo(std::string &ref [[clang::lifetime_capture_by(this)]])
{
vec.push_back({ref});
}
};
struct C
{
struct Ptr
{
C *c;
};
// holder = ptr, target = this
void foo(Ptr &ptr) [[clang::lifetime_capture_by(ptr)]]
{
ptr.c = this;
}
})Note that when applied to a constructor, lifetime_capture_by(this) acts as lifetimebound.
Note that [[clang::lifetime_capture_by(x)]] requires Clang 20 or newer. If you build mrbind with an older Clang, you must use our alternative custom attributes instead. Sadly, those have to be spelled differently when target is a parameter vs this. For example:
struct A
{
std::string &ref;
};
void foo(std::string &ref [[clang::annotate("mrbind::lifetime_capture_by=vec")]], std::vector<A> vec)
{
vec.push_back({ref});
}
struct C
{
struct Ptr
{
C *c;
};
// holder = ptr, target = this
void foo(Ptr &ptr) [[clang::annotate_type("mrbind::lifetime_capture_by=ptr")]]
{
ptr.c = this;
}
})Notice annotate vs annotate_type when applied to a parameter vs to this.
Later in this document we provide macros to dispatch between those custom attributes and Clang's ones depending on Clang version.
We have "nested" alternatives to [[clang::lifetimebound]] and [[clang::lifetime_capture_by(x)]], that are custom attributes.
Consider std::vector<T>::push_back(const T &). The vector doesn't need to keep-alive the entire parameter. If you tried to apply lifetime_capture_by(this) to the parameter, and then tried to insert a temporary in C++ code, Clang would warn, because the attribute makes it look like that would form a dangling reference (as if vector stores a reference to the parameter), which isn't true.
Yet, the parameter clearly needs some annotation, since if the parameter is a class with a reference/pointer in it, that nested reference would dangle on insertion.
Notice that the Python/C# wrapper for std::vector<T> stores C++ T objects, not Python/C# T objects. If you insert a Python/C# temporary into it, the underlying C++ object will be copied into the vector, but the corresponding Python/C# object will get destroyed quickly, if it's a temporary. And the keep-alive lists are stored in Python/C# objects, not in C++ objects.
Therefore the keep-alive list of the inserted object will not be automatically preserved in the vector, unless we annotate push_back() in some way.
Enter nested attributes.
struct A
{
std::string &ref;
}
struct VectorA
{
std::vector<A> vec;
void push_back(const A &a [[clang::annotate("mrbind::lifetime_capture_by_nested=this")]]) {vec.push_back(a);}
// This one doesn't need to be nested.
void push_back(std::string &str [[clang::lifetime_capture_by(this)]]) {vec.push_back({str});}
};And similarly we have [[clang::annotate("mrbind::lifetimebound_nested")]] to act as a nested [[clang::lifetimebound]].
The nested attributes indicate that we don't need to preserve the entire parameter, just its keep-alive list. Note that it's not wrong for a language backend to preserve the entire parameter anyway (which is what we're currently doing at the time of writing). This only affects memory usage, not correctness.
Once again, notice that if [[clang::annotate(...)]] applies to this (is spelled after a method parameter list), you must use [[clang::annotate_type(...)]] instead. This means that while [[clang::lifetimebound]] works with the same spelling on both parameters and this, the nested "mrbind::lifetimebound_nested" needs different spellings on those, using annotate vs annotate_type respectively.
It's recommended that you use following macros to apply those attributes, instead of using them directly.
This lets you disable them when not using Clang, to avoid warnings about unused attributes. It also lets you dispatch between different attributes on older vs newer Clang.
// Replace `@MYLIB@` with your library name.
#ifdef __clang__
# define @MYLIB@_LIFETIMEBOUND [[clang::lifetimebound]]
# if __clang_major__ >= 20 // Added in Clang 20.
# define @MYLIB@_LIFETIME_CAPTURE_BY(x) [[clang::lifetime_capture_by(x)]]
# define @MYLIB@_THIS_LIFETIME_CAPTURE_BY(x) [[clang::lifetime_capture_by(x)]]
# else
# define @MYLIB@_LIFETIME_CAPTURE_BY(x) [[clang::annotate ("mrbind::lifetime_capture_by=" #x)]]
# define @MYLIB@_THIS_LIFETIME_CAPTURE_BY(x) [[clang::annotate_type("mrbind::lifetime_capture_by=" #x)]]
# endif
# define @MYLIB@_LIFETIMEBOUND_NESTED [[clang::annotate ("mrbind::lifetimebound_nested")]]
# define @MYLIB@_THIS_LIFETIMEBOUND_NESTED [[clang::annotate_type("mrbind::lifetimebound_nested")]]
# define @MYLIB@_LIFETIME_CAPTURE_BY_NESTED(x) [[clang::annotate ("mrbind::lifetime_capture_by_nested=" #x)]]
# define @MYLIB@_THIS_LIFETIME_CAPTURE_BY_NESTED(x) [[clang::annotate_type("mrbind::lifetime_capture_by_nested=" #x)]]
#else
# define @MYLIB@_LIFETIMEBOUND
# define @MYLIB@_LIFETIME_CAPTURE_BY(x)
# define @MYLIB@_THIS_LIFETIME_CAPTURE_BY(x)
# define @MYLIB@_LIFETIMEBOUND_NESTED
# define @MYLIB@_THIS_LIFETIMEBOUND_NESTED
# define @MYLIB@_LIFETIME_CAPTURE_BY_NESTED(x)
# define @MYLIB@_THIS_LIFETIME_CAPTURE_BY_NESTED(x)
#endifUse the THIS versions of the attributes when placing them after the parameter list of a method (to apply to this), and the non-THIS versions when applying them to a parameter. Using the wrong one will cause a compilation error, so you won't miss it.
If you're sure you only want Clang 20 or newer, you can get rid of the #if __clang_major__ >= 20. Then you can also merge @MYLIB@_THIS_LIFETIME_CAPTURE_BY into @MYLIB@_LIFETIME_CAPTURE_BY(x), since they will expand to the same thing anyway.
Here's an incomplete list of what functions need to be annotated.
-
Anything that takes a reference and returns it, or stores it in
this. -
Anything that returns a reference to
thisor to a class field.
This applies not only to references, but also to raw pointers, and to classes storing references or raw pointers.
In particular, you need to annotate all custom containers:
- Methods returning references to elements:
@MYLIB@_LIFETIMEBOUNDonthis. - Methods inserting new elements:
@MYLIB@_LIFETIME_CAPTURE_BY_NESTED(this)on the inserted parameter (this begins to matter only if the element type is a class storing raw pointers or references).
In some cases, we're able to guess the intent from the code:
-
For copy and move constructors and assignments, we assume that the resulting object will store the same references as the argument it was copied from (i.e. the first argument of copy and move constructors and assignments is implicitly
lifetime-capture-by-nested(this)). -
For copy and move assignments, we assume that they return a reference to
*this, as long as their return type is a reference to the enclosing class (i.e. are implicitly[[clang::lifetimebound]]if that condition holds). -
Functions named
begin(),end(), andoperator*are assumed to have their usual iterator-related meaning (this can be disabled with--no-infer-lifetime-iterators).
When mrbind generates custom bindings for something, it's mrbind's responsibility to automatically provide the annotations for it. E.g.:
-
When generating the bindings for standard containers.
-
When generating elementwise constructor for an aggregate (a struct or a class with no custom constructors, that can be initialized with a list of its members in C++).
In some cases, you can tell the parser to guess the annotations, if you don't feel like spelling them manually.
-
--infer-lifetime-constructorsfor all constructors other than the copy/move constructors. This adds[[clang::lifetimebound]]to every parameter of those constructors.This assumes that the constructor might store a reference to the parameter in the resulting class instance. While not always the case, the worst that can happen is an increased memory usage.
Hidden annotations
Certain annotations are used in mrbind's custom bindings (such as those for the standard containers), but we currently don't have corresponding C++ attributes to add them in custom code. In particular:
-
There's no way to express that a function drops any existing references stored in an object.
This is added to
clear()of standard containers, and to assignment operators of all classes.This has effect only in C#, not in Python.