Beyond.NET

repository·main·Indexed 40 days ago

https://github.com/royalapplications/beyondnet

A code generation toolset that bridges .NET and native languages like C and Swift. It automates the creation of unmanaged wrappers and language-specific bindings using .NET NativeAOT, enabling .NET libraries to be consumed as native frameworks, such as XCFrameworks on Apple platforms.

Tokens
11.7K
Snippets
36
Records
47
Agent score
99%

What's inside Beyond.NET

  1. What is Beyond.NET and how does it work?

    main

    Beyond.NET is a toolset that enables calling .NET code from other programming languages (currently supporting C and Swift).

    How it works

    1. Reflection & Wrapper Generation: The Beyond.NET code generator loads a compiled .NET assembly, reflects over its types, and generates wrapper functions for all public APIs. These wrappers are decorated with the UnmanagedCallersOnly attribute, making them callable from native code.
    2. Binding Generation: The generator creates language-specific bindings (starting from C bindings) for the generated wrappers.
    3. Native Compilation: The generated C# code is compiled using .NET NativeAOT, allowing the resulting library to be consumed via the generated language bindings.

    Call Tree Example (Swift): Swift $\rightarrow$ C $\rightarrow$ .NET APIs (UnmanagedCallersOnly) $\rightarrow$ Original .NET API

  2. Manage memory for C bindings

    main

    When using the generated C bindings, you must follow a manual memory management model. Every object received from .NET (including structs, value types, and delegates) must be manually freed when no longer needed to prevent memory leaks.

    The Rule: Every object obtained from .NET must be manually destroyed using its generated destructor method.

    Exceptions: Primitive types (e.g., int, bool) and enums do not need to be freed.

    Destructor Pattern: The generator creates a destructor for every exposed type with the signature void [TypeName]_Destroy([TypeName]_t self). For example, System.Guid uses void System_Guid_Destroy(System_Guid_t self).

    // Example: Obtaining and destroying a System.Guid
    System_Guid_t myGuid = System_Guid_Empty();
    // ... use myGuid ...
    System_Guid_Destroy(myGuid);
  3. Manage memory for Swift bindings

    main
    When using the generated Swift bindings, memory management is handled transparently. You do not need to manually allocate or deallocate .NET objects. Standard Swift memory management rules apply, and you can treat .NET objects (including .NET delegates mapped to Swift closures) like regular Swift objects.
  4. Handle Exceptions in C and Swift bindings

    main

    Since C does not support exceptions, Beyond.NET implements a pattern to capture .NET exceptions during calls.

    C Bindings

    Most generated C APIs append an "out" parameter (a double pointer System_Exception_t*) to the function signature. After the call, this parameter will either be null (if no exception occurred) or contain the caught exception.

    Example C Signature:

    void WriteLine(System_String_t text, System_Exception_t* exception)

    Swift Bindings

    The Swift code generator automatically annotates APIs with the throws keyword, allowing you to use native Swift error handling (do-catch).

    Example Swift Signature:

    func writeLine(_ text: System_String) throws

    Note: Field getters and setters are currently exceptions to this rule as they are assumed never to throw.

    // C Example
    void WriteLine(System_String_t text, System_Exception_t* exception);
    // Swift Example
    func writeLine(_ text: System_String) throws
  5. Understand Opaque Types in C bindings

    main

    In C bindings, every .NET type that is not a primitive or an enum is exposed as an opaque type. This is implemented as a typealias for void* for .NET classes and structs.

    To interact with these types, you cannot access properties directly. Instead, you must call the generated methods and pass the instance as the first parameter (the self parameter).

    In Swift bindings, these opaque types are handled internally and are not exposed to the consumer; you can use the generated APIs as regular Swift types.

  6. Compare .NET objects in C and Swift

    main

    C Bindings

    Do not compare pointers to .NET objects directly. Because of how GCHandle works, two different pointers might refer to the same object. Instead, use the bindings for System.Object.Equals or System.Object.ReferenceEquals.

    Swift Bindings

    Swift provides a seamless experience by overriding comparison operators. You can use standard Swift operators to compare .NET objects:

    • == calls System.Object.Equals
    • === calls System.Object.ReferenceEquals
  7. Generator Modes: Automatic XCFramework builds

    main

    The generator always produces language bindings (C header files and optionally Swift source files). However, it can also be configured to automatically compile a native version of the target assembly.

    Note: Automatic build support is currently only available for Apple platforms.

    When enabled, the generator builds an XCFramework containing compiled binaries for:

    • macOS ARM64
    • macOS x64
    • iOS ARM64
    • iOS Simulator ARM64
    • iOS Simulator x64

    This XCFramework can be dropped directly into an Xcode project. If you choose not to use automatic builds, you must manually compile the generated C# using NativeAOT and link the resulting dynamic library.

  8. Work with .NET Interfaces in Swift

    main

    In Swift, .NET interfaces are represented as protocols. Types implementing these interfaces are generated as protocol-conforming types.

    Note on Metatypes: Because Swift does not allow extending protocol metatypes, you cannot use IInterfaceName.typeOf. Instead, use the generated DNInterface suffix: IInterfaceName_DNInterface.typeOf.

  9. Handle method overloads and overrides in C and Swift

    main

    C Bindings

    C does not support inheritance or method overloading. To resolve this:

    • Overloads: A counter suffix is added to the method name (e.g., MethodName_1, MethodName_2).
    • Overrides/Shadowing: Members are simply redeclared in the subclass bindings.

    Swift Bindings

    Swift supports native overloading and inheritance:

    • Overloads: Signatures use standard Swift overloading rules.
    • Overrides: Overridden or shadowed members use the override keyword.
    // C Overload Example
    void OverloadTests_Print(int32_t value, System_Exception_t* outException);
    void OverloadTests_Print_1(System_DateTime_t value, System_Exception_t* outException);
    void OverloadTests_Print_2(System_String_t value, System_Exception_t* outException);
  10. Run Beyond.NET Swift unit tests

    main

    The Beyond.NET unit test suite is written in Swift. To execute the tests, follow these steps in Xcode:

    1. Open the workspace: Samples/Beyond.NET.Sample.Swift/BeyondNETSamplesSwift.xcworkspace.
    2. Build the necessary components: Select the Build .NET Stuff scheme and run Product - Build.
    3. Wait for the build to complete.
    4. Run the tests: Select the BeyondNETSampleSwift scheme and run Product - Test.
    1. Open Samples/Beyond.NET.Sample.Swift/BeyondNETSamplesSwift.xcworkspace in Xcode.
    2. Scheme: 'Build .NET Stuff' -> Product -> Build
    3. Scheme: 'BeyondNETSampleSwift' -> Product -> Test
  11. Quick Start: Create a native .NET classlib for Apple platforms

    main

    Follow these steps to create a .NET class library, generate Swift bindings, and build an XCFramework for macOS/iOS.

    1. Create the project:

      mkdir BeyondDemo && cd BeyondDemo
      dotnet new classlib
      mv Class1.cs Hello.cs
    2. Implement the logic in Hello.cs:

      namespace BeyondDemo;
      public class Hello {
          public string Name { get; }
          public Hello(string name) { Name = name; }
          public string GetGreeting() => $"Hello, {Name}!";
      }
    3. Publish the library:

      dotnet publish

      Note the output path (e.g., bin/Release/net11.0/publish/BeyondDemo.dll).

    4. Configure Beyond.NET: Create a Config.json in your project root:

      {
          "AssemblyPath": "bin/Release/net11.0/publish/BeyondDemo.dll",
          "Build": {
              "Target": "apple-universal"
          }
      }
    5. Run the generator:

      beyondnetgen Config.json
    6. Locate output: The build output (including the .xcframework) will be in the directory specified in the logs, typically within your publish folder.

    beyondnetgen Config.json