ForNeVeR / Cesium

C compiler for the CLI platform
MIT License
383 stars 42 forks source link

`#pragma pinvoke` #511

Open ForNeVeR opened 11 months ago

ForNeVeR commented 11 months ago

The Problem

Real-world Cesium users will inevitably encounter a range of problems involving interop with native code.

  1. 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.

  2. 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:

  1. Build a native library somehow.
  2. Write a C# wrapper around it using P/Invoke.
  3. 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.
  4. 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

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

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:

// main.c
#pragma pinvoke("foo.dll")
#include <foo.h>
#pragma pinvoke(end)

int main() { return foo(); }

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.

ForNeVeR commented 9 months ago

Thanks to @BadRyuner, the most significant part of the work is done.

BadRyuner commented 9 months ago

Now stdlib can be implemented using PInvoke instead of __cliimport :D