Understanding the Solution

Now that the problem has been outlined, and the six main areas of compatibility detailed, this section is going to discuss how the marshalling library addresses compatibility issues and removes burden on application developers to handle this issue manually. Without the marshalling library, compatibility issues relating to calling conventions and higher on the compatibility stack need to be addressed by the application itself. The following image shows where primary responsibility lies for managing compatibility when the marshalling library is introduced:

MarshalSupport_CompatibilitySixAreas_Addressed

The marshalling library is able to largely or entirely manage concerns related to everything except some aspects of the application-defined APIs, but even at that level it is able to help by providing a pattern for development and tools to assist with handling application-defined types.

How marshalling works

The marshalling library works by getting heavily involved at the "linkage points" between assemblies. At any point where the execution of code transitions from one assembly to another during program flow, where that transition happens as the direct result of application code with application-supplied data, the marshalling library seeks to act as a "man in the middle", establishing two-way communication across the assembly boundary, and adapting the supplied data from a form the sender understands, to a form the receiver understands. Through appropriate use of virtual functions and header-only inline functions, it is possible to create "assembly barriers", where it is guaranteed two given functions are on opposite sides of the assembly boundary. With this separation established, complex structures can be broken down in one assembly into primitive types, which are exchanged over the assembly boundary, with the complex type then recomposed on the other side. The marshalling library uses this principle of assembly barriers and decomposition as the primary method of marshalling. On top of that, several layers of templates sit to package up the marshalling process and present it to the application in a simple and efficient manner, while augmenting the marshalling process for specific types where required. The entire implementation is designed to keep within the limits of C/C++ ABI compatibility (as discussed in detail in the problem), to ensure that the marshalling library is able to operate between different compiler types and versions.

Basic marshalling

The simplest way to use the marshalling library is to apply it directly to function declarations which already exchange STL types. In this form, the caller usually does not need to change at all, and the implementation side generally requires only a small number of straightforward adjustments. Consider the example from the problem which exposes school, class, and student information through STL-based functions. That example can be converted to use marshalling by replacing the exposed STL types with marshal helpers, while keeping the same overall function intent:

Marshal::Ret<std::string> GetSchoolName();
bool GetClassNames(const Marshal::Out<std::vector<std::string>>& classNames);
bool GetStudentNamesInClass(const Marshal::In<std::string>& className, const Marshal::Out<std::list<std::string>>& studentsInClass);

From the caller's perspective, the usage remains unchanged. Existing call sites can continue to pass and receive normal STL objects, and the marshal helpers are created implicitly by the compiler when the function is called:

std::string schoolName = GetSchoolName();
std::vector<std::string> classNames;
if (GetClassNames(classNames))
{
    for (const std::string& className : classNames)
    {
        std::list<std::string> studentNames;
        if (GetStudentNamesInClass(className, studentNames))
        {
            std::cout << className << std::endl;
        }
    }
}

In this form, the return value is expressed through Marshal::Ret, output parameters are wrapped in Marshal::Out, and input parameters are wrapped in Marshal::In. This preserves the original shape of the API very closely. The caller can continue to use the functions in a natural way, passing and receiving STL objects exactly as before, while the marshalling library handles the exchange of those objects across the assembly boundary. On the implementation side, the code usually remains equally simple, because the marshal helpers expose the data in a form which can be assigned to or read from directly.

This basic level of marshalling is the primary use case for the library. It allows STL types to be used safely across assemblies with little effort, while keeping the code readable and retaining the intent of the original API. The following sections expand on this foundation by showing how the same approach applies to STL types in more detail, how custom types can be adapted with marshalling constructors, and how the same model can be applied when designing interfaces for use across assembly boundaries.

See also