Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#pragma pinvoke #511

Open
1 of 2 tasks
ForNeVeR opened this issue Jan 5, 2024 · 2 comments
Open
1 of 2 tasks

#pragma pinvoke #511

ForNeVeR opened this issue Jan 5, 2024 · 2 comments
Labels
area:cil-interop Related to CIL (.NET) interop kind:feature New feature or request status:help-wanted Open for contributors

Comments

@ForNeVeR
Copy link
Owner

ForNeVeR commented Jan 5, 2024

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

  • Introduce a new #pragma pinvoke that is invoked in two forms.

    1. #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);
    2. #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:

// 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 ForNeVeR added kind:feature New feature or request status:help-wanted Open for contributors area:cil-interop Related to CIL (.NET) interop labels Jan 5, 2024
@ForNeVeR
Copy link
Owner Author

ForNeVeR commented Mar 2, 2024

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

@BadRyuner
Copy link
Contributor

BadRyuner commented Mar 3, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area:cil-interop Related to CIL (.NET) interop kind:feature New feature or request status:help-wanted Open for contributors
Projects
None yet
Development

No branches or pull requests

2 participants