etra0 2 days ago

I'm very much looking up to NativeAOT on C#, being able to compile to native dlls is very attractive to me, as I'd love to do (offline) game modding. I currently use Rust and it works quite well, but it's never too late to wish for a more 'forgiving' language for smaller projects!

  • Dwedit 2 days ago

    I think that .NET DLLs can still be loaded by native code? I could throw together a project to check if that actually works or not.

    Edit: Using the tool "dllexport" (https://github.com/3F/DllExport), you can create a .NET DLL that exports a function that is callable from C/C++ code. Just LoadLibrary and GetProcAddress.

    • pjc50 a day ago

      How does this differ from the Microsoft solution https://github.com/dotnet/samples/tree/main/core/nativeaot/N... ? Is it still MSIL rather than compiled, and if so where does it get the runtime from?

      • Dwedit a day ago

        .NET DLL files are not only MSIL code, they are also native code. If built for the same architecture (x86, x64...) as the importing program, it can be loaded like any other DLL.

        The DLL links to MSCOREE.DLL. When loaded into a process, the DLL can initialize the .NET runtime when necessary. The DLL's entry point is a jump to _CorDllMain within MSCOREE.DLL, which takes care of loading and initializing .NET if it hasn't been done before.

        As for how it manages to initialize .NET from within DllMain (you're under DLL loader lock at this point), I don't know. It has to somehow get out of loader lock at some point before it loads a bunch of DLLs.

    • etra0 2 days ago

      oh, neat. I recently needed to export some functions from a C# project to be used by C++ code, but at the time I researched it (June 2024), the dllexport project seemed to be frozen (last commit before that date was on 2021).

      Thanks to NativeAOT, though, it seems that you can do this kind of stuff without extra dependencies which is great too!

      • rpeden 2 days ago

        It works quite well especially in .NET 9!

        And exporting to WASM works nearly identically to DllExport.

        I used that to shift all the heavy lifting to .NET 9 AOT compiled to WASM in a fun little side project I've been working on: https://evo.ryanpeden.com

        • rasmuskl a day ago

          Can you share more on this? Any examples that helped you or anything you've written on the WASM .NET topic?

          • rpeden a day ago

            Not yet - I just created a browser-wasm project using the .NET CLI and then experimented with it. I spent a bunch of the digging through .targets files to see what optimization options were available.

            I plan to put the source on GitHub shortly so others can use it as an example. Just need to clean things up a little first.

            • rasmuskl a day ago

              Sounds good. It is a jungle to find the right optimization options. It's not getting that much publicity.

  • MrLeap 2 days ago

    Interesting. You and I are reasonably adjacent it seems like. I've been cooking up a pleasant way to enable modding support in games I make.

    I have this idea in my head that I want this system to work even all the way up to letting people host their own servers that run their own mods without allowing them the power to do nefarious things to the clients who connect. This means a scripting language layer, most commonly LUA. For a bunch of reasons, many are vibes based, I've decided to go with a lisp instead.

    After doing a bit of research I found this repo to use as a starting point.

    https://github.com/microsoft/schemy

    Checks all my boxes. Big ones being no new dependencies and not many lines of code. It took just a few minutes to get a handle on the whole thing end-to-end. I'm working right now on setting a little reflection metaprogramming that would expose any functions that I put an attribute above to the lisp layer.

    There's a few things to delete so it's safe to serialize over the wire, but it looks like it'll enable what I'm after. I hope I can trust my serializers :D

    What kinds of mods do you want to make?

    • etra0 2 days ago

      I'm interested in something that we call "Virtual Photography", which is basically capture beauty within virtual worlds. For that, there are games that include photo modes and some that don't, but it's often that those they do have several limitations (like range, controls, etc).

      I mostly do free-cameras, a way to detach the camera from the player to allow different angles that otherwise would be impossible to appreciate.

      Lately I did a project to spawn lights on The Witcher 3 [1], to allow 'virtual photographers' to take some amazing portraits [2, 3], I did this by using Rust with a little bit of x86 assembly.

      Rust has been neat for this, because despite working on a safety-hazardous territory, the amount of crashes has been minimal because we still have safeguards within the unsafe stuff, but sometimes I just want to not worry about lifetimes.

      If you like games, you can check this amazing gallery from the FRAMED community to see the extents the people do to take amazing shots within games engines (and admittedly, external tools like reshade, mods, etc):

      https://framedsc.com/HallOfFramed

      [1] https://github.com/etra0/litcher

      [2] https://framedsc.com/HallOfFramed/?game=The+Witcher+3&title=...

      [3] https://framedsc.com/HallOfFramed/?game=The+Witcher+3&title=...

      • MrLeap 2 days ago

        This is absolutely incredible work! How hard was it to get your foot in the door on having code execute on an already running process? Do you do this from a separate process at runtime or is it all modifying the executable before you run it?

        I've never done anything like that. I can imagine getting the memory offsets for things like camera transforms being reasonably straight forward with cheat engine, but mutating the values feels a like converting a train from diesel to electric while it's carrying a load. It reminds me of something Ross Scott (he makes youtube videos about games) has expressed a desire to have -- the ability to comprehensively map and record the 3d worlds of games that are going to get shut down. Admittedly I initially dismissed it a little on the grounds that if you want that, just extract the 3d model data from the client and load up blender.

        Seeing what you and others have done is making me appreciate what he's after more. The whole composition, environments and characters and lighting and visual effects and post processing would not be preserved just by yanking out the .fbx files and textures. A lot would be lost. Despite being a gamedev myself, seems like I somehow lost the perspective a little.

        It's awesome to see there's a community and a hobby doing what you're doing. Thanks for showing this to me.

        • etra0 a day ago

          > This is absolutely incredible work!

          Thanks for your kind words!

          > How hard was it to get your foot in the door on having code execute on an already running process? Do you do this from a separate process at runtime or is it all modifying the executable before you run it?

          All is done at runtime. By nature of how Windows DLL work, they have a DllMain that's executed every time you load a dynamic library into a running process. From there you spawn a thread and you can do pretty much whatever you want within the same memory space of the games. There are multiple ways of injecting a DLL into a running process, and since I only care about offline games, I don't have to fight against any anticheat and stuff so it is pretty straightforward, it is a very interesting topic to read about to be honest!

          > I can imagine getting the memory offsets for things like camera transforms being reasonably straight forward with cheat engine

          Cheat Engine is an amazing tool, for real. I remember using it when I was a kid to do very rudimentary cheating -- just scanning and changing values; years have passed and one day I opened Cheat Engine again to... skip over an annoying mechanic of a game and I noticed it can do so much more! it has a very good debugger, disassembler, memory view, you can even inject your own assembly code. It was the gateway drug that I needed to get started on everything I do.

          > but mutating the values feels a like converting a train from diesel to electric while it's carrying a load

          Well, compilers are pretty straightforward (at least, most of the time), it's all about memory layout.

          If you have something like

            struct vec {
              float x;
              float y;
              float z;
            };
          
          Once you find the pointer to the objects you need, most certainly they're going to be each one after the other. This looks like obvious knowledge, but at least on my brain I had to build an extra bridge to realize how simple it is :) (of course things can always get complicated but this is the basic gist of it).

          And as long as you know your programming language has a stable memory layout, you can map it from your code. I even was able to map some very basic virtual class from C++ within Rust by mapping the virtual tables.

          What I enjoy the most, is that I learned a lot about low-level programming, assembly, how OS and compilers work together, while having fun playing games, and developing tools that people can use to create pretty images -- it felt like the outcome was extremely positive and it brings me a lot of joy.

          • MrLeap 21 hours ago

            Ah yeah, what you're saying about memory layouts makes sense to me. I'm writing a custom netcode that sends MessagePack serialized structs over udp. Looking at the raw bytes of the message makes debugging easier sometimes. When things get spooky, the first thing I do is turn off things-that-occlude the struct. Things like RLE / encryption / signing / compression and see if it fixes things. Divide and conquer from there. Series of techniques I honed writing a lidar driver with no documentation on a device years ago. Lot of staring at hex dumps from wireshark. :)

            I guess I imagined more challenges in not getting any writes you do stomped by some other code that really wants to set a value on your vec every frame. Maybe I'm overthinking it and it's an easy happy path to get your injection to occur "at the end of the update" where your assignment actually makes it to the screen.

            Cheers.

DeathArrow 2 days ago

I am curious if there is a way to discard GC and free the memory by hand.

This article might point me in the right direction to disable GC, but freeing memory can't be as simple.

  • Dwedit 2 days ago

    If you really really want to, you can use Marshal.AllocHGlobal and allocate unmanaged memory. All access happens through pointers and `unsafe` code blocks.

    • kevingadd a day ago

      Modern C# has Span<T> and Memory<T> types to give you the ability to work with unmanaged memory without directly touching pointers or `unsafe`, FWIW. Worth messing with the next time you need to do it.

    • neonsunset 2 days ago

      You're supposed to use NativeMemory.Alloc/.Free methods instead.

      Obviously nothing prevents you from PInvoking malloc and free directly, or linking to a custom allocator, or even using https://www.nuget.org/packages/TerraFX.Interop.Mimalloc which is a fully managed Mimalloc implementation (which is competitive with the original).

      There is also an advanced undocumented API to register a specific segment of memory as nongc-heap where you can allocate otherwise managed objects manually.

      But the most important thing to understand that normally in languages with generational GCs the objects are not freed, instead, the surviving objects are copied to an older generation while the rest of the memory is reclaimed and immediately made available for subsequent allocations. As a result, you can't "free" an object since such operation does not exist in the design.

      Overall, you're not forced to use objects in low-level code - structs can implement methods, interfaces, used in generics for zero cost abstractions,etc., and if you don't allocate or allocate only a little - the GC will never run.

      • algorithmsRcool a day ago

        I didn't realize that the GC copied the surviving objects to a separate location. Come to think of it, i don't actually know how the GC keeps track of what object lies in what generation.

        • mystified5016 a day ago

          GC is free to move things all over memory, though I'm not savvy enough to say why this is necessary.

          C# allows you to "pin" a variable to combat this. Any pinned memory will never be moved by GC, so if you pass a pointer to an object out of your managed program, the pointer address will always stay valid.

          • Dwedit a day ago

            If you can't move objects around in memory, you get memory fragmentation.

  • mystified5016 a day ago

    You actually have a lot of control over the C# GC. One common technique is to suspend GC before entering a tight loop and then manually running GC when your program has downtime. For instance, you might pause GC during your game's frame render routine, then unpause in the space between frames. Otherwise, GC runs when it feels like it, which always seems to happen in an inconvenient place that causes visible stutter.

    You also have the concept of "pinning" a variable, which prevents GC from moving it around memory (for instance, if you pass a very long-lived pointer out of C#, you might want to pin it so the pointer address is always constant).

    So you can suspend and resume GC globally, you can manually run GC at any time, and you have ways of exempting certiain objects from GC.

    I don't know offhand if you can GC one specific object manually, but I would be very surprised if you couldn't.

    Also, all of the above is considered very bad practice. As a rule of thumb, you are not smarter than the GC and you will do a worse job unless you are very sure of what you're doing.

    • Dwedit a day ago

      The thing really needed is a "Collect with timeout" function. Something like "Garbage collect now, but abandon the job in a consistent state if X milliseconds have passed".

      (Perhaps even some other event. Like "Do GC until a File IO operation finishes")

      • kevingadd a day ago

        I think this is referred to as Incremental GC, and runtimes like Spidermonkey, V8 and Lua have it. I'm not sure whether there's any hope of getting it for .NET or whether Java has one though.

amne 2 days ago

This right here is peak Microsoft:

  set DOTNET_GCName=clrgc.dll
  set COMPlus_GCPath=ManagedDotnetGC.dll
Good luck understanding this when you come back 2 months later.
  • pjmlp a day ago

    Every mature programming language ecosystem has quirks that only those that were around since the early days get why they are there.

  • MortyWaves a day ago

    It was my understanding the ridiculous and historic COMPlus name had been replaced with just DOTNET several years ago.

    • pjc50 a day ago

      Deeper horror:

      > I got closer to the goal when I realized an interesting quirk: .NET supports environment variables prefixed by either DOTNET_ or COMPlus_, whereas NativeAOT only supports DOTNET_. So if we set COMPlus_GCName=ManagedDotnetGC.dll, only the .NET runtime will pick it up, and the NativeAOT runtime will ignore it.

    • pjmlp a day ago

      It is rather hard to name environment variables as COM+.

  • CharlieDigital a day ago

    Just write a batch file? I don't see the issue

    • mystified5016 a day ago

      Explain what these lines do and why they're necessary, then you'll understand the problem

      • CharlieDigital a day ago

        They are environment variables specifying DLLs to use.