Real-world Cesium users will inevitably encounter a range of problems involving interop with native code.
They might want to convert their existing programs to Cesium file by file, while keeping interoperability between the managed and unmanaged parts (#pragma unmanaged, looking at you right now!).
This will be useful in cases when some language features are not yet supported in Cesium, or if certain translation units are too reliant on native dependencies.
Cesium may support that by compiling the managed part of the code and providing an ability to dynamically link to its native part. In this case, the managed and native parts of the program would share the same set of the header files, but the classical linking stage would be augmented by Cesium stepping in and wiring it all via P/Invoke.
They might want to use some native dependencies for their programs. Of course they will.
The classical solution from the native world is for the libraries to provide native link modules (for example, .lib on Windows). The compiler will just note that certain functions are not known at the compilation stage, and will delay the resolution to the linker. And the linker would analyze the set of available link modules and seek for the mentioned dependencies among them.
Cesium is unable to use that solution because we cannot link managed and native parts together. But CLI has an ability to P/Invoke. Let's use that!
Currently, it is theoretically possible to interop with this code using Cesium, but it is very clunky and far from being a good solution. In particular, you may take the following strategy:
Build a native library somehow.
Write a C# wrapper around it using P/Invoke.
Write a Cesium wrapper around C# wrapper using __cli_import, e.g. __cli_import("MyClass::MyFunction") int MyFunction(int x) — and that for every function.
Build a Cesium program while referencing the C# library that P/Invokes the native library.
While possible in theory, this is, as you can imagine, not very practical.
The Solution
[x] Introduce a new #pragma pinvoke that is invoked in two forms.
#pragma pinvoke("mydll.dll"). This command would make all the following function declarations (if not provided by the current translation unit) to be looked up in mydll.dll, effectively turning them into P/Invoke declarations. For example, this program:
#pragma pinvoke("mydll.dll")
int foo_bar(int*);
would be an equivalent of the following C# code:
// partial static class MyCurrentTranslationUnit
[DllImport("mydll.dll")]
static extern int foo_bar(int* _a);
#pragma pinvoke(end) that will restore the default resolve behavior.
Note that DllImport has some other options that are omitted from the example. We may consider adding them to the pragma, or on a per-function basis.
One detail I would like to emphasize is that it's important to allow the pragma commands to not be written near every function. We intend the feature to be used with third-party headers, ideally without modifying them. So, if some sort of per-function customization is required, then it should be possible to augment the functions with additional pragmas that are not tied to their declarations.
[ ] Add a separate opt-out check stage that will verify that the libraries have the required P/Invoke entry points. If we cannot find a DLL or it doesn't contain a module we require, that should be a compilation error by default.
That check should be opt-out, i.e. there should be an option to skip it. This is for cases when you build C code for libraries you don't have on the build machine, or when the absence of some symbols is expected (e.g. when building a wrapper around a third-party library that has several different versions).
Proposed command-line syntax is cesium --omit-interop-check.
The Future
This proposal is not final, because it skips the dual side of the problem: the native dependencies may want to call back to Cesium.
For the best backward integration with native code, we'd need to
support a mechanism similar to [UnmanagedCallersOnly], or older [DllExport] extension in C#, to allow Cesium to expose functions from its binaries
learn to build native link modules around these Cesium libraries, to pass them to native compilers for seamless integration with native build systems
But that's not a part of the current proposal, and open for grabs.
The Examples
Own Code Interop
Consider we have a C program consisting of two translation units:
// foo.h
int foo();
// foo.c
int foo() { return 1; }
// main.c
#include <foo.h>
int main() { return foo(); }
If we want to start converting to Cesium, they may choose to start from main.c. So, we set up our build system to compile main.c with Cesium, and foo.c using our old compiler while providing a DLL, and then modify the code of main.c as this:
And voilá, we are in the brave new world of Cesium (partially).
Note that it was not required to modify the foo.h file.
Library Interop
Consider we use a library that provides several functions, such as SDL.
#include <SDL2/SDL.h>
int main() {
int code = SDL_Init(0);
}
If we want to start converting this code to Cesium, it would be possible to do the following:
#pragma pinvoke("SDL2.dll")
#include <SDL2/SDL.h>
#pragma pinvoke(end)
int main() {
int code = SDL_Init(0);
}
Once again, we've achieved the result without modifying the library headers.
The Details
One detail of note is that if we wanna to do that, we'd like the type layout to be compatible with whatever the native library uses.
Cesium mostly follows the CLI conventions (at least on the default architecture set settings), and CLI mostly follows the platform-specific conventions. But there are different compilers for different platforms.
Whenever we encounter a case requiring that, we may consider adding some pragmas to control the member layout in the shared header files.
The Problem
Real-world Cesium users will inevitably encounter a range of problems involving interop with native code.
They might want to convert their existing programs to Cesium file by file, while keeping interoperability between the managed and unmanaged parts (
#pragma unmanaged
, looking at you right now!).This will be useful in cases when some language features are not yet supported in Cesium, or if certain translation units are too reliant on native dependencies.
Cesium may support that by compiling the managed part of the code and providing an ability to dynamically link to its native part. In this case, the managed and native parts of the program would share the same set of the header files, but the classical linking stage would be augmented by Cesium stepping in and wiring it all via P/Invoke.
They might want to use some native dependencies for their programs. Of course they will.
The classical solution from the native world is for the libraries to provide native link modules (for example,
.lib
on Windows). The compiler will just note that certain functions are not known at the compilation stage, and will delay the resolution to the linker. And the linker would analyze the set of available link modules and seek for the mentioned dependencies among them.Cesium is unable to use that solution because we cannot link managed and native parts together. But CLI has an ability to P/Invoke. Let's use that!
Currently, it is theoretically possible to interop with this code using Cesium, but it is very clunky and far from being a good solution. In particular, you may take the following strategy:
__cli_import
, e.g.__cli_import("MyClass::MyFunction") int MyFunction(int x)
— and that for every function.While possible in theory, this is, as you can imagine, not very practical.
The Solution
[x] Introduce a new
#pragma pinvoke
that is invoked in two forms.#pragma pinvoke("mydll.dll")
. This command would make all the following function declarations (if not provided by the current translation unit) to be looked up inmydll.dll
, effectively turning them into P/Invoke declarations. For example, this program:would be an equivalent of the following C# code:
#pragma pinvoke(end)
that will restore the default resolve behavior.Note that
DllImport
has some other options that are omitted from the example. We may consider adding them to the pragma, or on a per-function basis.One detail I would like to emphasize is that it's important to allow the pragma commands to not be written near every function. We intend the feature to be used with third-party headers, ideally without modifying them. So, if some sort of per-function customization is required, then it should be possible to augment the functions with additional pragmas that are not tied to their declarations.
[ ] Add a separate opt-out check stage that will verify that the libraries have the required P/Invoke entry points. If we cannot find a DLL or it doesn't contain a module we require, that should be a compilation error by default.
That check should be opt-out, i.e. there should be an option to skip it. This is for cases when you build C code for libraries you don't have on the build machine, or when the absence of some symbols is expected (e.g. when building a wrapper around a third-party library that has several different versions).
Proposed command-line syntax is
cesium --omit-interop-check
.The Future
This proposal is not final, because it skips the dual side of the problem: the native dependencies may want to call back to Cesium.
For the best backward integration with native code, we'd need to
[UnmanagedCallersOnly]
, or older[DllExport]
extension in C#, to allow Cesium to expose functions from its binariesBut that's not a part of the current proposal, and open for grabs.
The Examples
Own Code Interop
Consider we have a C program consisting of two translation units:
If we want to start converting to Cesium, they may choose to start from
main.c
. So, we set up our build system to compilemain.c
with Cesium, andfoo.c
using our old compiler while providing a DLL, and then modify the code ofmain.c
as this:And voilá, we are in the brave new world of Cesium (partially).
Note that it was not required to modify the
foo.h
file.Library Interop
Consider we use a library that provides several functions, such as SDL.
If we want to start converting this code to Cesium, it would be possible to do the following:
Once again, we've achieved the result without modifying the library headers.
The Details
One detail of note is that if we wanna to do that, we'd like the type layout to be compatible with whatever the native library uses.
Cesium mostly follows the CLI conventions (at least on the default architecture set settings), and CLI mostly follows the platform-specific conventions. But there are different compilers for different platforms.
Whenever we encounter a case requiring that, we may consider adding some pragmas to control the member layout in the shared header files.