Raylib-cs icon indicating copy to clipboard operation
Raylib-cs copied to clipboard

Generate binding and LibraryImport usage

Open Odex64 opened this issue 1 year ago • 13 comments

Consider using raylib's definitions from the official repo to auto generate source files. This will make the latest APIs available in raylib-cs and drastically reduce the manual work of porting raylib's structs, enums and functions - make sure that all the structs are blittable.

Also consider these other changes

  • Upgrade to .NET Core 8 and C# 12.
  • Switch to LibraryImport for better AOT compatibility.

Odex64 avatar Oct 04 '24 08:10 Odex64

Looks like a nice way to generate bindings. Could probably handle most of the bindings. Would help with maintenance since the library is now passively maintained.

Open to updating .net and c# versions but only if we need to for specific features etc. As for LibraryImport, can you explain how the bindings would be better for AOT compared to the current approach?

rgebee avatar Oct 06 '24 15:10 rgebee

Upgrading to .NET 8 should be fine for most users since it is LTS and defaults to C# 12. It it also required for the new p/invoke source generator (LibraryImport) to work.

As of LibraryImport there are the following benefits

  • NativeAOT support.
  • Better debugging the marshalling logic since LibraryImport generates some code at compile time rather than being completely runtime.
  • It should lead to better performance.
  • Can handle the string marshalling automatically instead of manually writing helper methods.

If you want I can work on a generator in order to generate all the source files from raylib's definitions.

Odex64 avatar Oct 06 '24 17:10 Odex64

Still undecided but examples comparing/showing the pros and cons for both ideas would be useful either way.

rgebee avatar Oct 16 '24 20:10 rgebee

Here's more details of pros and cons of both.

LibraryImport

Pros

  1. Source Generation: LibraryImport leverages source generators to auto-generate P/Invoke calls. This means that some code is generated at compile time, minimizing errors that can occur with manual marshalling and having a better debugging experience.

  2. Improved Performance: Generated code via LibraryImport can be more optimized for performance because the marshalling logic is generated at compile-time. This can lead to more efficient interop calls compared to runtime-based in DllImport.

  3. More Flexible Type Marshaling: LibraryImport provides better support for complex types and marshaling scenarios, making it easier to handle custom marshalling, though we don't really need this since all structs should be blittable.

  4. NativeAOT Support: LibraryImport is compatible with NativeAOT, allowing you to use source-generated P/Invoke with ahead-of-time compilation, which can significantly reduce application startup time and improve performance in some scenarios.

  5. Automatic String Marshalling: LibraryImport can automatically handle string marshalling for you, reducing the boilerplate for helper methods.

Cons

  1. Newer Feature: LibraryImport is relatively new (introduced in .NET 7), and not all legacy systems or projects may fully adopt or support it. It might not be as battle-tested as DllImport in every edge case.

  2. Potential Compatibility Issues: LibraryImport may not work with older versions of .NET (prior to .NET 7), limiting its use in projects that need to maintain compatibility with older .NET frameworks.

  3. Less Documentation and Examples: Since it's newer, there are fewer examples and documentation available compared to DllImport, which has been in use for a much longer time.


DllImport

Pros

  1. Widespread Adoption: DllImport is widely used, well-documented, and supported in all versions of .NET, making it a safe and reliable choice for P/Invoke across different projects and .NET versions.

  2. Simpler to Use: DllImport might be simpler to use compared to LibraryImport, which can be beneficial for certain scenarios.

  3. Broad Compatibility: Since it has been around for a long time, DllImport is supported by all versions of .NET, including .NET Framework, ensuring better backward compatibility.

Cons

  1. Manual Code: Developers need to handle marshaling manually, which can lead to errors in complex scenarios, such as dealing with complex data types or memory management issues.

  2. Performance Overhead: Runtime marshaling in DllImport may introduce performance overhead, particularly in scenarios where complex types need to be marshaled frequently.

  3. Lack of Compile-Time Checks: DllImport does not offer the same level of compile-time diagnostics as LibraryImport, meaning issues with interop signatures may only be detected at runtime.


In Practice

Let's take for example the InitWindow function in this project:

[DllImport("raylib", CallingConvention = CallingConvention.Cdecl)]
public static extern void InitWindow(int width, int height, sbyte* title);

However this is unsuitable for managed code, therefore there's an helper method for that:

public static void InitWindow(int width, int height, string title)
{
    using var str1 = title.ToUtf8Buffer();
    InitWindow(width, height, str1.AsPointer());
}

Whereas we can achieve the same thing with only two lines of code:

[LibraryImport(Name, StringMarshalling = StringMarshalling.Utf8)]
public static partial void InitWindow(int width, int height, string title);

Notice the StringMarshalling = StringMarshalling.Utf8, that's used to handle the string marshalling automatically in the generated code.

Now if you want to expose the unsafe version as well, you can do the following:

[LibraryImport("raylib", StringMarshalling = StringMarshalling.Utf8)]
public static partial void InitWindow(int width, int height, string title);

[LibraryImport("raylib")]
public static partial void InitWindow(int width, int height, sbyte* title);

Here we get the following benefits:

  • NativeAOT compatibility.
  • Automatically handle the string marshalling for InitWindow without writing a helper function.
  • Eventually you can write an unsafe version as well with minimal code boilerplate.
  • The unsafe version should also be faster than the one using DllImport.

Odex64 avatar Oct 18 '24 13:10 Odex64

if you generate the binding you need a github action to build it for every platform or having a Windows, Linux and Mac pc.

MrScautHD avatar Oct 18 '24 16:10 MrScautHD

I was working on generator + new native bindings builder based on raylib zig build that I would want to eventually contribute to raylib-cs after i even out some remaining small issues, but thanks to zig being so easy to work with the natives support linux/osx/windows/wasm already together with support for cross-compilation so huge step up from what raylib-cs has now.

And for generator, the biggest issue is how raylib-cs is structured, e.g a lot of extra helper methods are spread out on the raylib structs, so im not sure how to deal with that. Ideally they should be just partial structs and the helper methods elsewhere, but moving all of them is about as big task as the effort it took to write the generator.

Anyway for anyone interested here is the code now for both bindings + generator:

https://github.com/deathbeam/Raylib.NET

And as i said the end goal is to PR it here ideally as im not interested in further fragmentation of CS raylib bindings (even though its mostly fault of the maintainers, raylib-cs included with its project structure, makes bigger contributions like this very hard).

EDIT:

So got the native generator to good state but the build.zig in 5.0 release is kinda unusable so raylib-cs would first need to get updated to 5.5, rip

deathbeam avatar Oct 24 '24 20:10 deathbeam

This is how Raylib-csLo is made right? Unfortunately they do not update the binding yet it is supposed to be easy this way. I really hope Raylib-cs will be maintained. Dozen of french developer are using it. I even know personally a world famous game developer using it these days to prototype his next game...

dmekersa avatar Dec 08 '24 20:12 dmekersa

Updated title to focus on binding generation and LibraryImport. Discussion about net8.0 can be done in #295.

rgebee avatar Apr 24 '25 20:04 rgebee

If we upgrade to .NET 8 or 9, I've explored two possible approaches for automating the majority of the code generation for this binding.


raylib's API files

These files, maintained in the raylib repository, are regularly updated and provide structured API definitions. I'd suggest using the json format, deserializing it, and using that to drive the binding generation.

Pros

  • Very simple to set up and deserialize.
  • Easy to quickly generate the core API bindings.

Cons

  • We depend on the raylib team to continue maintaining these files. If they stop, we'll need a new strategy.
  • These files only cover raylib's core APIs - other libraries like raygui, rlgl, etc., are excluded.
  • While deserialization is straightforward, generating code (especially involving pointers or advanced marshalling scenarios) might require significant custom logic.

CppAst (or similar)

This approach involves parsing raylib's actual C headers directly. This would allow us to automatically reflect the latest changes and support all raylib related libraries.

Pros

  • No dependency on raylib's team to maintain external API definition files.
  • Covers the entire ecosystem - including raygui, rlgl, etc.
  • Always reflects the latest upstream changes directly from source.

Cons

  • Likely more complex than JSON based generation, particularly for handling C to C# mapping nuances.

Additional Notes

With the new LibraryImport feature, string marshalling can be handled automatically (as mentioned here), which simplifies the binding generation for many functions.

However, we'll likely still need a list of functions that require special handling... for example:

  • Enums as ints: raylib often uses int in place of enums, so we'll need metadata indicating which parameters are really enums.
  • Pointer handling: For functions accepting pointers, we'll have to decide how best to represent them, are they arrays? shall we pass by ref? etc. This may require analysis of raylib's internals.

Lastly, all structs should ideally remain unmanaged to minimize marshalling overhead.

Odex64 avatar Apr 25 '25 09:04 Odex64

@Odex64 flibitijibibo uses c2ffi for his SDL3 binding.

JupiterRider avatar Apr 27 '25 09:04 JupiterRider

.NET 8 has been enabled by #297 PR.

However, I'm still unsure if we can switch from DllImport to LibraryImport attribute.

LibraryImport became available since .NET 7, however this binding still targets both .NET6 and .NET8. How will C# handle this? I'm not expert enough.

Odex64 avatar May 07 '25 14:05 Odex64

LibraryImport became available since .NET 7, however this binding still targets both .NET6 and .NET8. How will C# handle this? I'm not expert enough.

You can use preprocessor directives:

#if NET7_0_OR_GREATER
    Console.WriteLine("This is .NET 7 or newer");
#else
    Console.WriteLine("This is not .NET 7");
#endif

~~Instead of declaring every function twice (with DllImport and LibraryImport) we can try to create our own custom attribute which directs to DllImport or LibraryImport.~~ Never mind. LibraryImportAttribute and DllImportAttribute are sealed :(

JupiterRider avatar May 09 '25 19:05 JupiterRider

Never mind. LibraryImportAttribute and DllImportAttribute are sealed :(

Well yeah. I can't think of other methods without a lot of duplicated code. The best alternative would be to target only .NET 8.

Odex64 avatar May 12 '25 15:05 Odex64

To clarify it is out of scope to include both binding types in the project. Over time when we are no longer supporting .net6 then we will be open to pull requests moving the bindings across to LibraryImport.

rgebee avatar Jul 02 '25 07:07 rgebee