← Back to context

Comment by neonsunset

2 years ago

Mixing managed and unmanaged code being an issue is simply not true in programming in general.

It may be an issue in Go or Java, but it just isn't in C# or Swift.

Calling `write` in C# on Unix is as easy as the following snippet and has almost no overhead:

    var text = "Hello, World!\n"u8;
    Interop.Write(1, text, text.Length);

    static unsafe partial class Interop
    {
        [LibraryImport("libc", EntryPoint = "write")]
        public static partial void Write(
            nint fd, ReadOnlySpan<byte> buffer, nint length);
    }

In addition, unmanaged->managed calls are also rarely an issue, both via function pointers and plain C exports if you build a binary with NativeAOT:

    public static class Exports
    {
        [UnmanagedCallersOnly(EntryPoint = "sum")]
        public static nint Sum(nint a, nint b) => a + b;
    }

It is indeed true that more complex scenarios may require some form of bespoke embedding/hosting of the runtime, but that is more of a peculiarity of Go and Java, not an actual technical limitation.

That's not the direction being talked about here. Try calling the C# method from C or C++ or Rust.

(I somewhat recently did try setting up mono to be able to do this... it wasn't fun.)

  • It is very easy to call a C# method from C++, since .NET has a COM interop layer. From C++ this will just look as a class with no fields but a bunch of virtual methods. Alternatively, you can easily convert a static method to a native function pointer and then invoke that - this way it's also easy to do from C, Rust, and just about anything else that speaks the C ABI.

    If your C# method doesn't take any arguments like managed strings or arrays that require marshaling, it's also very cheap (and there's unsafe pointers, structs, and fixed arrays that can be used at interop boundary to avoid marshaling even for fairly complicated data structures).

    .NET was very much designed around these kinds of things. It's not a coincidence that its full type system covers everything that you can find in C.

  • What you may have been looking for is these:

    - https://learn.microsoft.com/en-us/dotnet/core/deploying/nati...

    - https://github.com/dotnet/samples/blob/main/core/nativeaot/N...

    With that said, Mono has been a staple choice for embedding in game-script style scenarios, in particular, because of the ability to directly call its methods inside (provided the caller honors the calling convention correctly), but it has been slowly becoming more of a liability as you are missing out on a lot of performance by not hosting CoreCLR instead.

    For .dll/.so/.dylib's, it is easier and often better to just build a native library with naot instead (the links above, you can also produce statically linkable binaries but it might have issues on e.g. macOS which has...not the most reliable linker that likes to take breaking changes).

    This type of library works in almost every scenario a library implemented in C/C++/Rust with C exports does. For example, here someone implemented a hello-world demonstration of using C# to write an OBS plugin: https://sharovarskyi.com/blog/posts/dotnet-obs-plugin-with-n...

    Using the exports boils down to just this https://github.com/kostya9/DotnetObsPluginWithNativeAOT/blob... and specifying correct build flags.

    • I haven't been looking for those because I don't work with .NET. Regardless, what you're linking still needs callers and callees to agree on calling convention and special binding annotations across FFI boundaries which isn't particularly interesting from the perspective of language implementation like the promises of Graal or WASM + GC + component model.

      1 reply →

There are more managed langauges than Go, Java, and C#. Swift (and Objective C with ARC) are a bit different in that they don't use mark and sweep/generational GCs for automatic memory management so it's significantly less of an issue. Compare with Lua, Python, JS, etc where there's a serialization boundary between the two.

But I stand by what I said. It's generally unwise to mix the two, particularly calling unmanaged code from managed code.

I wouldn't say it's "not a problem" because there are very few environments where you don't pay some cost for mixing and matching between managed/unmanaged code, and the environments designed around it are built from first principles to support it, like .NET. More interesting to me are Graal and WASM (once GC support lands) which should make it much easier to deal with.

Except that is only true since those attributes were introduced in recent .NET versions, and it doesn't account for COM marshaling issues.

Plenty of .NET code still using the old ways that isn't going to be rewritten, either for these attributes, or the new Cs/WinRT, or the new Core COM interop, which doesn't support all COM use cases anyway.

  • Code written for .NET Framework is completely irrelevant to conversation since it does not evaluate it.

    You should treat it as dead and move on because it does not impact what .NET can or can’t do.

    There is no point to bring up “No, but 10 years ago it was different”. So what? It’s not 2014 anymore.

    • My remarks also apply to modern .NET, as those improvements were introduced in .NET 6 and .NET 8, and require a code rewrite to adopt them, instead of the old ways which are also available, in your blind advocacy you happened to miss out.

      Very few code gets written from scratch unless we are talking about startups.

Swift is not a "managed" (i.e. GC) language.