usertype memory

memory layout of usertypes

Note

sol does not take ownership of raw pointers, returned from functions or set through the set functions. Return a value, a std::unique_ptr, a std::shared_ptr of some kind, or hook up the unique usertypes traits to work for some specific handle structure you use (AKA, for boost::shared_ptr).

The userdata generated by sol has a specific layout, depending on how sol recognizes userdata passed into it. All of the referred to metatable names are generated from the name of the class itself. Note that we use 1 metatable per the 3 styles listed below, plus 1 additional metatable that is used for the actual table that you bind with the name when calling table::new/set_(simple_)usertype.

In general, we always insert a T* in the first sizeof(T*) bytes, so the any framework that pulls out those first bytes expecting a pointer will work. The rest of the data has some different alignments and contents based on what it’s used for and how it’s used.

Warning

The layout of memory described below does not take into account alignment. sol3 now takes alignment into account and aligns memory, which is important for misbehaving allocators and types that do not align well to the size of a pointer on their system. If you need to obtain proper alignments for usertypes stored in userdata pointers, please use the detail functions named sol::detail::align_usertype_pointer, sol::detail::align_usertype, and sol::detail::align_usertype_unique. This will shift a void* pointer by the appropriate amount to reach a certain section in memory. For almost all other use cases, please use void* memory = lua_touserdata(L, index);, followed by memory = sol::detail::align_usertype_pointer( memory ); to adjust the pointer to be at the right place.

Warning

The diagrams and explanations from below is only guaranteed to work 100% of the time if you define SOL_NO_MEMORY_ALIGNMENT. Be aware that this may result in unaligned reads/writes, which can crash some older processors and trigger static analyzer/instrumentation tool warnings, like Clang’s Address Sanitizer (ASan).

To retrieve a T

If you want to retrieve a T* pointer to the data managed by a sol3 userdata and are not using sol3’s abstractions to do it (e.g., messing with the plain Lua C API), simply use lua_touserdata to get the void* pointer. Then, execute a T* object_pointer = *static_cast<T**>(the_void_pointer);. Every type pushed into C++ that is classified as a userdata (e.g., all user-defined objects that are not covered by the stack abstraction’s basic types) can be retrieved in this format, whether they are values or pointers or unique_ptr. The reasons for why this works is below.

For T

These are classified with a metatable name generally derived from the class name itself.

The data layout for references is as follows:

|        T*        |               T              |
^-sizeof(T*) bytes-^-sizeof(T) bytes, actual data-^

Lua will clean up the memory itself but does not know about any destruction semantics T may have imposed, so when we destroy this data we simply call the destructor to destroy the object and leave the memory changes to for lua to handle after the “__gc” method exits.

For T*

These are classified as a separate T* metatable, essentially the “reference” table. Things passed to sol as a pointer or as a std::reference<T> are considered to be references, and thusly do not have a __gc (garbage collection) method by default. All raw pointers are non-owning pointers in C++. If you’re working with a C API, provide a wrapper around pointers that are supposed to own data and use the constructor/destructor idioms (e.g., with an internal std::unique_ptr) to keep things clean.

The data layout for data that only refers is as follows:

|        T*        |
^-sizeof(T*) bytes-^

That is it. No destruction semantics need to be called.

For std::unique_ptr<T, D> and std::shared_ptr<T>

These are classified as “unique usertypes”, and have a special metatable for them as well. The special metatable is either generated when you add the usertype to Lua using set_usertype or when you first push one of these special types. In addition to the data, a deleter function that understands the following layout is injected into the userdata layout.

The data layout for these kinds of types is as follows:

|        T*        |    void(*)(void*) function_pointer    |               T               |
^-sizeof(T*) bytes-^-sizeof(void(*)(void*)) bytes, deleter-^- sizeof(T) bytes, actal data -^

Note that we put a special deleter function before the actual data. This is because the custom deleter must know where the offset to the data is and where the special deleter is. In other words, fixed-size-fields come before any variably-sized data (T can be known at compile time, but when serialized into Lua in this manner it becomes a runtime entity). sol just needs to know about T* and the userdata (and userdata metatable) to work, everything else is for preserving construction / destruction semantics.