16.06.2025 • DevOps

The Linux Version: New C# Wrappers in C3D Toolkit

Maxim Pylaev, software engineer at C3D Labs, discusses the evolution of the C# wrappers in the C3D Toolkit, the challenges developers face, and how they are addressing them to improve user experience.

There are always requests to reduce barriers to product usage. That’s one of the main purposes of wrappers. Let’s take a look at how we are developing the C# wrapper for the kernel as part of the C3D Toolkit SDK.

The requirements for the wrapper are standard: support, scalability, and cross-platform capabilities. Some background. At the moment, we have C# wrappers using the CLI/C++ technology. Unfortunately, this results in some limitations. The most obvious downside is that we cannot use these wrappers in Linux. The technology just doesn’t fit. The development of the .NET Framework stopped after version 4.8.2 and is not going to be resumed. Therefore, we started switching to another technology, also to make the wrappers cross-platform.

The Linux Version: New C# Wrappers in C3D Toolkit, photo 1
Fig. 1

What steps have we taken and what milestones have we passed? The frying pan in the image vividly represents the process. The kernel is based on C++. To smoothly integrate everything, we had to fry the eggs". To achieve this, we needed a C wrapper, because by switching to pure C, we could later take advantage of P/Invoke technology and wrap everything in C#. We launched the C wrapper project in 2022. Later, in Q1 2023, we began the development of the primary C# wrapper. The final wrapper project was started in the second half of 2023. Now we’ll look at each of the steps in more detail.

Making a C wrapper on top of our C++ kernel is a multi-phase process. First, we had to identify the classes and data structure to be wrapped, and we did. Since pure C also has some limitations, we had to consider how to use template classes. We analyzed the kernel API and selected the minimum functionality to be implemented with С templates only.

Second, we had to make the C wrapper itself from the C++ headers. As a result, we got builds for various operating systems.

The Linux Version: New C# Wrappers in C3D Toolkit, photo 2
Fig. 2

Third, we had to turn the C wrapper, an intermediate solution, into a C# wrapper. What did we need for all this? The C headers. We use MonoProject’s CppSharp for converting C declarations into C# declarations. The image shows an example of a primary conversion for a single function. This is the declaration of mb_collection with the mb_mesh parameters. The C representation and P/Invoke declaration after making the primary wrapper are as shown. We can say that Step 1 went smoothly. Technically speaking, everything worked, but we had to analyze the outcome.

The Linux Version: New C# Wrappers in C3D Toolkit, photo 3
Fig. 3

What were the first results? Admittedly, disappointing. There were several reasons for this. First, all the methods were static. Second, there were no tools for unmanaged memory manipulation. As a consequence, the Garbage Collector did not know what to do and how to free the native resources. Third, we lost the concept of original classes and inheritance. Why? Because we switched to C first, and then made a primary wrapper using C. Obviously, few developers would be willing to code such a method, even using the auto-complete feature. The result would be unreadable. In theory, it is a viable option, but in real life, it is quite difficult to use.

We started working on the final wrapper. What was to be done? Initially, we lacked information about the classes we had gotten rid of at the C wrapper phase. To overcome this, we add meta information to the C wrapper. We selected the common, easy-to-read JSON format to store all the wrapped methods and classes. This was for the sole purpose of correctly restoring these classes afterwards. We also had to develop a parser for the existing primary wrapper to analyze it. It was important to identify the P/Invoke declarations, API declarations, and method implementations. The final wrapper generator combines all these building blocks. It summarizes metadata and primary wrapper information to reconstruct the classes.

The Linux Version: New C# Wrappers in C3D Toolkit, photo 4
Fig. 4

Let me explain how the C# final wrapper generator works. We can introduce a final Process function that uses the primary wrapper and meta information. The result is the final C# file. We parse the code into entities. FileCreator creates a certain template for the output CS file, which contains the names of the restored classes. APICreator browses through all the wrapped API functions for that class. Then, APICreator calls FunctionCreator for each function. FunctionCreator performs the conversion. All this required a number of changes.

The Linux Version: New C# Wrappers in C3D Toolkit, photo 5
Fig. 5

If we examine the primary wrapper, it is not obvious that it is a constructor. Only comments indicate this. The constructor returns an IntPtr-type value. It is very unclear how to use it. The final wrapper converts into a class with the correct parameters. It is not just some pointer to a mesh, but to a full-fledged .NET MbMesh object prepared for calling the declared P/Invoke function.

The Linux Version: New C# Wrappers in C3D Toolkit, photo 6
Fig. 6

Another comparison: the image clearly shows that after the initial C header-based wrapping, the class name is generated just from the filename. Next, we declare data structures in which the P/Invoke methods are declared. The restored class has all the names of the original C++ class and the additions from the final wrapper to manage the memory properly, implement destructors, and release native resources.

The Linux Version: New C# Wrappers in C3D Toolkit, photo 7
Fig. 7

What are the current results? For now, we are building the C# wrapper for different operating systems. This may not be unique, but it certainly adds some functionality. We are building the wrapper for Windows and Linux at least. It replicates the functionality of the original code, except for some limitations. There are some intricacies concerning the inheritance. One can use the access functions to convert the current object into either the parent or child class.

We’ve done many additions, primarily for resource management, as there was an urgent need to combine unmanaged memory with the managed C# code.

For debugging, we’ve added a native counter to the restored classes, which are derived from the classes with a reference counter. For convenience, we’ve added the ICollection interface for native container classes.

The final C# version is much more intuitive. The kernel’s containers are easier to use for C# developers. Another addition is that any object created inside a class method is linked to the class. This is to save this object when the Garbage Collector is running. There is also a basic .NET class with an IDisposable interface for proper memory management.

Now, about our plans for the future. They are quite understandable. We should at least fine-tune a high-quality CI to automate all the processes. There is a lot of work to optimize the C# test suite. For now, there are few tests, they need to be structured and formatted. We will also expand the coverage of the kernel functionality. The generator just works, but this is not enough. (It works automatically. Any changes to the kernel, new or removed API functions, are mirrored in the wrappers. Some functionality cannot be wrapped for technical reasons. One day, we’ll cover it as well.

A special point is the builds for Astra Linux, and, hopefully, for more operating systems. I’m confident these builds will work fine.

Maxim Pylaev, Software engineer, C3D Labs
Maxim Pylaev,
Software engineer,
C3D Labs
Share
Up