Search Unity

CollectionMarshal.AsSpan() Support

Discussion in 'Scripting' started by stevphie123, Feb 5, 2022.

  1. stevphie123

    stevphie123

    Joined:
    Mar 24, 2021
    Posts:
    82
    I reckon the possibility of ditching out .Net 5 entirely and straight to 6 as mentioned by the staffs.

    Now that Unity has support for fast span, is there a chance for CollectionMarshal.AsSpan added without having to wait for 6?
     
    atcarter714 and ViacheslavRud like this.
  2. Liam2349

    Liam2349

    Joined:
    Mar 10, 2016
    Posts:
    83
    I just found out about this method. Would be nice for lists of structs so we don't have to copy the whole struct when accessing it.

    We can use arrays to get a reference to an item in the array, but lists are easier to work with.
     
    atcarter714 likes this.
  3. stevphie123

    stevphie123

    Joined:
    Mar 24, 2021
    Posts:
    82
    There's a workaround with custom List, but yeah if we have this out of the box it would be much nicer
     
    atcarter714 likes this.
  4. atcarter714

    atcarter714

    Joined:
    Jul 25, 2021
    Posts:
    66
    EDIT: Job done! Built a solution that beats ordinary for/foreach loops by a considerable margin, and there are some real benchmark logs and charts you can see.

    Code and pre-built binary plugin (.dll library) are all on my GitHub page, and I plan to do some more interesting Unity "hacks" for the community in the future. So you can let me know if you have cool ideas: if it's realistic and feasible and could help a broad range of users I might implement your idea.

    https://github.com/atcarter714/UnityH4xx

    ===================
    Original:
    ===================
    Oh? You guys want the CollectionMarshal class and AsSpan<T> in Unity? But that's all older Mono versions and .NET Framework or .NET Standard and CollectionMarshal wasn't added until .NET 5, right?! So what ... let's make one ... o_O

    So, I wrote a quick "hack" implementation of it that works as if it were literally the real .NET CollectionMarshal class exposing the exact, same AsSpan<T>( List<T> list ) method signature in System.Runtime.InteropServices. It works, but ... there's a few minor snags I've run into, and it actually needs a bit more work to make it both seamless (as I've already accomplished) and to be super fast like the real class in .NET Core 5/6/7 version ... the real CollectionMarshal has a distinct advantage because it's part of the same assembly as List<T> and it can simply access its underlying array directly, whereas we cannot ... it takes a little bit of sorcery to obtain it and create then Span ...

    So, to start off I just created a .NET Framework 4.7 console application and installed System.Memory and the Benchmark.NET package and then configured a benchmark test fixture with two "control groups" and the one "experimental group":

    Test Groups:
    1)
    regular
    foreach
    loop ...
    2) regular
    for
    loop ...
    3) my "hack-in-solution" of
    CollectionMarshal.AsSpan<T>( List<T> list )
    method ...

    How does it work? Well, I theorized that a "lazy" solution would work wherein I generated a cache of reflection at startup, paying the reflection cost only once and simply looking up the cached data for subsequent calls. I supposed that maybe that would work, but the Benchmark.NET test was necessary to find out if it actually did or not. It turned out to be a damned headache because I started having weird issues with Benchmark.NET package itself that I couldn't debug since it only runs in Release mode, forcing me to go read the documentation about how to force it build a debug build with symbols and all that good stuff ... I finally got that sorted out and fixed the problems with it so the benchmark could complete. When the benchmark results data is output to the build directory, I then process that data with an R script which turns it into a neat little set of charts/graphs to visualize. And here are the results:

    benchmarkNet_CollectionMarshal_Testing_01.png benchmarkNet_CollectionMarshal_candlestick_01.png
    ______________________________________________________________________________

    benchmarkNet_CollectionMarshal_bargraph_01.png


    So, the regular old "for loop" actually won at this benchmark, but there's more to the story than meets the eye and it's a bit more nuanced that it seems at first glance. In a test with only 100 to 500 items, the CollectionMarshal version was way behind both foreach and for loops. But as you up the number of items, you'll notice that using CollectionMarshal.AsSpan passes up the foreach loop and starts gaining on the for loop. And there's only one reason for this: because the technique I used of caching reflection data to speed up obtaining the internal "_items" array inside of a List<T> still has too much overhead and caching reflection data just wasn't enough ... it makes the AsSpan<T> method get off to a slow start, but once it has the Span to the underlying array it is indeed faster at the actual iteration than either foreach or for loops. Therefore, as you increase the number of items the more time the Span has catch up and regain the time lost when you call AsSpan to get the Span in the first place. It's like a race between between a Toyota Camry, a Ford Mustang and a Lamborghini Aventador LP 750-4 Superveloce and the Mustang keeps winning because the Lambo driver has to start the race standing 10ft away from his car ... the Toyota and Mustang drivers are already in their driver's seats with the engine running, and when the race starts, the Lambo driver has to run, jump in the car, start it up and then try to catch up. That's essentially what's happening here ... the longer the race track, the more time the Lambo has to close the gap and potentially pass up the competitors, as we can see when we cycle through 50,000 items instead of only about 500. So we know precisely what the problem is ... even caching the reflection data and looking it up just wastes too much time and isn't snappy enough to win the race.

    Potential solutions?
    I have a few in mind, but I think the best one is probably going to be to just emit some IL assembly code to replace any reflection logic and caching and create a "fake" getter method to simply access the underlying array reference instantly, so it can just be converted directly into a Span<T> and we can take off at full speed ahead and beat both foreach and for loops. IL has some caveats and quirks of its own, like it has no concept of "private" and "protected" members of classes, or anything like that, and it only respects access modifiers at the assembly level -- like what is simply public vs. non-public. The underlying array we're trying to get at is actually an internal array, but ... we can still get around this and obtain it with a little .NET wizardry.

    It was fun seeing how this all worked as what it would take to implement a drop-in CollectionMarshal and AsSpan<T> in these older versions of .NET and Mono, such as those Unity works with, but it's gonna take a little more effort and some IL-hacking to make it work as intended ... next time I have some free time to take a stab at it, I will implement the IL solution and post a Github gist of the CollectionMarshal drop-in / add-on solution for Unity and pre-.NET 5 runtimes ...

    ;)

    EDIT: Also, I could just give Unity the proper internal implementation of it, which is pretty simple, if they wanna just add it to the next build and make it a real feature since we already have Span<T>, haha :D
     
    Last edited: Dec 9, 2022
  5. Liam2349

    Liam2349

    Joined:
    Mar 10, 2016
    Posts:
    83
    Interesting research atcarter714. Unlucky on not beating the "for" loop but interesting nonetheless.
     
    atcarter714 likes this.
  6. stevphie123

    stevphie123

    Joined:
    Mar 24, 2021
    Posts:
    82
    Important to note, the above and any other variants of it have the same restriction that CollectionAsmarshal api has, which you can't edit/update/change the List while iterating. just be careful
     
    atcarter714 likes this.
  7. atcarter714

    atcarter714

    Joined:
    Jul 25, 2021
    Posts:
    66
    Generally speaking, yes, and foreach iteration variables are supposed to be read-only/immutable and guarded against any direct assignment with the = operator, but of course their state can be changed by interacting with them via other methods, fields, props, ops, etc. But, there are pretty much always some ways around such safety features ... if you're savvy enough to do it, though, and can obtain pointers to things you shouldn't be able to point to, for example, then I assume you also know enough to understand the consequences! If not, all I can say is: "You gonna learn today!" haha :D

    It's worth pointing out to people that when someone warns you about stuff like this or urges caution, it's not because we think it's gonna make your computer blow up or catch on fire. These words of caution are given because not heeding them could result in you getting involved in a complicated and frustrating problem that wastes a ton of time, maybe stalling your progress with your project while time ticks by. In the real world, that could cost you a heap of money (pun intended lol) or even make a client terminate a contract when the agreed upon deadlines are missed. So, yeah, the warning isn't because it's supposed to explode, start a fire or anything like that, it's all about your time/money and mental health, lol. ;)


    Ah, well ... like I said, it was a valiant first try at the "easy way", but I am certain that making a little shim assembly with a piece of custom IL assembly in it will do the trick, and I will build one asap to share for "academic" purposes. It's unfortunate that I couldn't do it in a "pure C#" way, but I will still finish the job as soon as I have some time for it.
     
  8. atcarter714

    atcarter714

    Joined:
    Jul 25, 2021
    Posts:
    66
    Update on this tomorrow ... I've solved the problem in several different ways and all that's left is benchmarking and comparisons and checking Unity compatibility. I've done an IL emit solution, a "pure" IL solution, a Roslyn hack solution to compile C# with my own rules and a .NET 6 "helper assembly" version.
     
    Nad_B likes this.
  9. atcarter714

    atcarter714

    Joined:
    Jul 25, 2021
    Posts:
    66
    I'm almost finished with this so I can distribute it, and I've run myself about half crazy here, lol. I wanted to demonstrate several different ways of doing something like this as an educational resource, and I'm trying to make sure everything works with all the possible build configurations. Also trying to do a copy + paste solution so users can simply copy a .cs file in the assets folder and it magically works on rebuild, and that has been more of a headache than I would have imagined. Emitting IL into a dynamic assembly or method is not generic-friendly, and this code doesn't have any time to stop and reflect (lol, nice unintended pun). Reflection will not let you get a generic delegate to a generic method and treat it like an ordinary generic. Nope, it forces you specify a concrete, known type argument, which really sucks. So that has forced me to be a little more creative to meet the challenge of a one .cs file "drag and drop" solution. In any case, the DLLs containing the solution work so I will try to wrap it up this week as time allows (I've got a job as a developer already so time is a scarce commodity lol).

    One of the solutions I've made for this is ridiculously easy to do and doesn't require hacking Roslyn, emitting assembly code and breaking protection/access rules in C# ... and it may have further-reaching implications about supporting even more of the newer .NET features, but I've found out some rather strange things as well ... this started off just as something to be amusing but it's become a really deep research and academic project, and I think I might make a video about this and talk about .NET versions, Mono and how Unity works. Hopefully people would be interested in it, but I found it fascinating lol.
     
  10. TheZombieKiller

    TheZombieKiller

    Joined:
    Feb 8, 2013
    Posts:
    266
    The underlying "_items" array is always the first field in all current implementations of List<T>, so "unsafely" getting the array is as simple as:
    Code (CSharp):
    1. var array = Unsafe.As<StrongBox<T[]>>(list).Value;
    That means you can implement AsSpan like this:
    Code (CSharp):
    1. using System.Runtime.CompilerServices;
    2.  
    3. namespace System.Runtime.InteropServices
    4. {
    5.     public static class CollectionsMarshal
    6.     {
    7.         public static Span<T> AsSpan<T>(List<T> list)
    8.         {
    9.             if (list == null)
    10.                 return default(Span<T>);
    11.  
    12.             return new Span<T>(Unsafe.As<StrongBox<T[]>(list).Value, 0, list.Count);
    13.         }
    14.     }
    15. }
    It is important to note that the above snippet has a dependency on System.Runtime.CompilerServices.Unsafe.dll.

    You can avoid this dependency with C# 11, by compiling a .NET Standard library with the following code:
    Code (CSharp):
    1. #pragma warning disable 8500
    2.  
    3. using System.Runtime.CompilerServices;
    4.  
    5. namespace System.Runtime.InteropServices;
    6.  
    7. public static class CollectionsMarshal
    8. {
    9.     public static unsafe Span<T> AsSpan<T>(List<T>? list)
    10.     {
    11.         if (list is null)
    12.             return default;
    13.  
    14.         return new(((StrongBox<T[]>*)&list)->Value, 0, list.Count);
    15.     }
    16. }
    You would then add the compiled .dll to your project, rather than a .cs source file.

    All of that said, I would not personally recommend doing any of this unless you are absolutely certain that the benefits outweigh the costs for your scenario.
     
    atcarter714 and stevphie123 like this.
  11. atcarter714

    atcarter714

    Joined:
    Jul 25, 2021
    Posts:
    66
    Thanks for the post and another cool idea! It's important to point out that the actual thing I'm doing this for isn't because AsSpan is all that important by itself (it's definitely a cool micro-optimization) or because I'm encouraging people to use this purely for the sake of using it, but I was already doing a huge deep-dive into .NET Core and Unity's runtime, Roslyn compiler-as-a-service stuff, reflection, runtime code-synthesis and modification, managed and native code injection and all kinds of evil and scary things lol. I've even been getting into the backend of the IL2CPP backend, LVVM, and how it works -- that's a subject which will take a bit longer to make a coherent article about, but the things you can do once you catch on to how that pipeline works are very exciting. The AsSpan subject itself was just a good example case to pick up, because it wasn't tightly embedded/coupled with .NET Core runtime and it was a small thing to attack. But I'm going to be writing about it and I might make a video as well if I have time. I've found a multitude of ways of achieving this same back-port of the feature, but you pointed out one I haven't thought of yet and I will definitely credit you for sharing it. But I hope people understand that it's for "academic" purposes and sport, not to encourage people to use things they don't understand, and it's about opening up a larger discussion about the internals of both .NET Core and Unity. And also largely about showing people that, as a programmer, you don't have to just accept things as they seem to be. :)

    I've found a couple novel ways of packaging the feature into PE binaries (e.g., DLL library), but I think the ideal thing for Unity users is the best "drag and drop" solution as a .cs file. There are some nuances to consuming external DLLs and plugins in Unity that will frustrate and discourage many users. But I'm going to talk about all of that as well, even an interesting C++/CLI way and F# way of doing this thing (amongst other things) that most Unity users probably haven't thought about. I think that makes another great point about problem-solving in the .NET ecosystem: Don't forget that managed C++/CLI and the fact that "mixed-mode" assemblies exist, and remember F# is around too and has unique features like inline assembly:

    F# will actually let you just write asm straight up, as shown below. And it's totally possible to emit and inject x86/64 or ARM instructions from C# code, too, it's just a quite a bit of work! That's something else I want to write about sometime. There are some interesting NuGet packages to aid you in breaking things with assembly language also, haha

    Code (FSharp):
    1. let getCycle = asm {
    2.     rdtsc
    3.     shl rdx 32uy
    4.     add rax rdx
    5.     ret
    6. }
    o_O

    There are a ton of ways, of varying levels of difficulty, to do something like this beyond perceived limitations of Unity or Mono/.NET or other tools and platforms, and I love being able to show people that. So I really appreciate that contribution to the discussion! I still haven't decided on the "best" way of all yet, but I'm strongly in favor of providing a self-contained .cs script implementation that's "drag and drop" without dependency. A few nanoseconds of performance difference is gonna decide how feasible that is, but, in any case, it still adds a lot more interesting things to talk and write about. ;)
     
    TheZombieKiller likes this.
  12. TheZombieKiller

    TheZombieKiller

    Joined:
    Feb 8, 2013
    Posts:
    266
    There isn't much benefit to using a .cs file for code that isn't going to change, which is the case for polyfills like this -- if anything, using .cs files can be worse because it can bloat compile times for no real benefit. That said, you can implement a 'fast' AsSpan in a single Unity-compatible .cs file without dependencies, but the implementation suffers for it (and it's not quite as fast as the two versions I posted before):
    Code (CSharp):
    1. using System.Collections.Generic;
    2. using System.Runtime.CompilerServices;
    3.  
    4. namespace System.Runtime.InteropServices
    5. {
    6.     public static class CollectionsMarshal
    7.     {
    8.         public static Span<T> AsSpan<T>(List<T> list)
    9.         {
    10.             if (list == null)
    11.                 return default(Span<T>);
    12.  
    13.             var box = new ListCastHelper { List = list }.StrongBox;
    14.             return new Span<T>((T[])box.Value, 0, list.Count);
    15.         }
    16.  
    17.         [StructLayout(LayoutKind.Explicit)]
    18.         private struct ListCastHelper
    19.         {
    20.             [FieldOffset(0)]
    21.             public StrongBox<Array> StrongBox;
    22.  
    23.             [FieldOffset(0)]
    24.             public object List;
    25.         }
    26.     }
    27. }
    F# itself doesn't have any built-in functionality for writing inline assembly, that sample is actually from a library that simulates it by (ab)using computation expressions, developed by WhiteBlackGoose with a little help from me and a few others in the C# Discord.

    I actually have a small sample project for manually emitting assembly with C# a bit more easily, if you're interested in learning more about that :)

    Just for anyone reading: it's important to note that these should be avoided for "new" code when possible, they're strongly tied to Windows and are essentially "soft deprecated". C++/CLI in particular also lacks support for a lot of newer .NET runtime features which can cause headaches as soon as you run into it.
     
    Invertex and atcarter714 like this.
  13. atcarter714

    atcarter714

    Joined:
    Jul 25, 2021
    Posts:
    66
    This is fair point, but the only reason I say that is purely for the ease-of-use, and having a way to support a thing (not just this) without introducing dependencies and things of that nature. That being said, the way I personally provide most of the extended functionality I've implemented to my Unity applications is through building my own DLL.

    Ah, I see! I've been skim-reading too fast again, it would seem, haha. For some reason I thought that was a recent addition to F#, which I found really surprising lol. That's what I get for having 64 different browser tabs open and jumping around like a psychopath. :D

    Thanks for the clarification on that though! I've been having
    //! TODO: Learn F#, you dummy!
    on my personal task list for a while now, and still haven't had a chance to really deep-dive into it, but I remain very interested in it and how I can apply it to different things and challenges I face.

    I've got a number of little techniques and implementations of my own, and I know of some NuGet packages that do really fun and interesting stuff, but I would never say "No" to checking out someone's code for doing something as deranged as this and violating the Geneva Convention, haha. Yes, please! o_O

    (5 mins later ...)
    Alright, I checked it, and that's pretty cool! I see that you've got references to Iced and TerraFX packages, two projects I also have on my radar. And I couldn't help noticing you're also into ClangSharp and other things pertaining to these areas that I take a great interest in. This is pretty cool, indeed, as I don't run into many people around here who mess with anything like this, haha. I actually haven't had a chance to take ClangSharp out for a test drive, but I have been working with CsWin32 and WinMD, as I'm doing a custom DirectX 11/12 interop library for my "hobby" engine project. It would probably be more appropriate to DM you, but I'd definitely like to ask about your experiences with ClangSharp and to discuss some more advanced interop scenarios, if you're up for it. :cool:

    In the past, I usually used VirtualAlloc through P/Invoke call to allocate some memory to inject binary instructions, and create a delegate (or delegate* these days -- the more recent delegate pointers feature in C# is quite nice). First time I ever did this sort of thing was when I was a teenager back in the XP or Vista days, and at first I didn't know what DEP (data execution prevention) was so you can imagine how that worked out for me, at first, when I mistakenly thought that I could just call any arbitrary thing in RAM, haha, I learned though ... VirtualAlloc is tied to Windows, however, and is not portable. What you've shared actually makes a nice example of a cross-platform implementation, though, and thanks for sharing it! Would like to know what you've been up to with TerraFX also ... o_O

    Yes, mixed-mode assemblies are not portable like a "regular" modern .NET assembly would be because the native parts still have the same inherent limitations any native code does elsewhere. (Note: There are also some changes to compiler switches and options to be aware of, so anyone who finds this in the future: be sure to check Microsoft's documentation). The same kind of "laws of universe" still apply to code you build with it regarding how both .NET and native things work, so if you put the two of them together in a mixed-mode assembly you will have to consider the limitations you inherit from both worlds. And you have to read the documentation for the do's and don't's of C++/CLR, of course ...

    Anyway, it's my understanding that it's essentially never going away because it's simply too useful to ever ditch it. However, Microsoft no longer treats it as its own .NET language, like it did in days gone by. They've removed the project templates for things like Winforms and WPF projects with C++ to reflect this and so as not to encourage people to use it for their primary language and mode of .NET development (nothing stops you from making your own, however, and I actually have some I use for really specific stuff, haha). Nowadays, it's treated like it's just an extended capability of the C++ compiler rather than as an actual .NET language of its own. It's complicated, tricky and problematic to use, and not a very pleasant development experience like C# is. However, it's still an extremely useful tool for building "glue" between native and managed code and handling advanced interop needs and other scenarios. What I took away from my readings on it is that it will remain just as such, for the foreseeable future, but you are advised not to use it for things other than the particular type of scenarios I just described, because it would definitely be a particularly unpleasant and problematic adventure to try to treat it like you treat C#.

    Am I incorrect or missing something there? And can you elaborate a bit more on which particular/specific .NET 7 features people have had issues with in C++ /CLR code?

    There's a whole iceberg I've explored concerning C++/CLR, C++/Cx, C++/WinRT, WinMD, C#/WinRT etc ... if anyone out there needs to do some heavy interop work, I'd definitely advise looking into those things to understand what options exist and are advised, and look into stuff like win32metadata and, one of my favorites, CsWin32 if you're doing Win32 or Windows API-specific interop ... there are a lot of fascinating ways to do things in the .NET world, and I've had great results with CsWin32 in one of my projects. (NOTE: Some things like C++/Cx are technically deprecated, but since interop can often involve legacy code I still included it. Just make sure you read things carefully before choosing what to use.)
     
    Last edited: Nov 28, 2022
  14. TheZombieKiller

    TheZombieKiller

    Joined:
    Feb 8, 2013
    Posts:
    266
    Sure, my DMs are always open :)
    I also recommend joining the C# Discord server, the #allow-unsafe-blocks channel is a veritable treasure trove of information on topics like this.

    Pretty much, yeah. It's comparable to .NET Framework in that sense: it's too big and important to entirely deprecate yet, but it's recommended that you avoid it when you can.

    ClangSharp mostly fills this gap with ClangSharpPInvokeGenerator. It can generate bindings for C++ classes and methods, though it's not really recommended to do so (it's usually a better idea to make C wrappers and generate bindings for those instead). You have to consider ABI differences, compiler version changes, etc. That said, there are definitely still cases where it's safe, I have one or two types in one of my own libraries that do it.

    Problems typically arise whenever the C++/CLI compiler encounters newer features (like ref structs, default interface methods, static abstracts, etc). Some features like generic attributes had to actually be delayed because just having one in an assembly (even if it was never used/inspected) was enough to crash the C++/CLI compiler.
     
    atcarter714 likes this.
  15. atcarter714

    atcarter714

    Joined:
    Jul 25, 2021
    Posts:
    66
    This is the benchmark I did last night with what I think is probably gonna be the fastest possible way to do it. I just used some IL assembly language during the build process to make a DLL. And the reason I say this is because there's no method calls or steps needed to steal a reference to the inner "_items" array of the List<T>, we literally just load it on the evaluation stack with one instruction and boom, make a Span<T> and return. The runtime will of course throw an exception and say "What, are you crazy?! That's not for you!" ... unless, of course, you have the magical attribute.

    All your array are belong to us!
    Code (CSharp):
    1. IL_0004: ldfld        !0/*T*/[] class [mscorlib]System.Collections.Generic.List`1<!!0/*T*/>::_items
    That one instruction in IL is all we need to load the reference to the array, then we just return a Span<T> in a couple more ops and the whole thing is done. I copied the really efficient way that .NET 7 does it.

     
    TheZombieKiller likes this.
  16. TheZombieKiller

    TheZombieKiller

    Joined:
    Feb 8, 2013
    Posts:
    266
    If you mean
    IgnoresAccessChecksToAttribute
    , note that this doesn't work in Unity (the version of Mono it uses is too old to recognize the attribute). However, you can use
    SecurityPermissionAttribute
    with
    SkipVerification = true
    to work around this.

    You can actually avoid writing IL and do this directly from C# too... if you compile against a custom .NET Standard library. I've published a sample demonstrating how to do that here.
     
    atcarter714 likes this.
  17. atcarter714

    atcarter714

    Joined:
    Jul 25, 2021
    Posts:
    66
    Nice! Very helpful information, and I wasn't even aware
    IgnoreAccessChecksToAttribute
    wasn't working in Unity, I was just about to move onto real-life Unity build tests to analyze the library performance and functionality, and that probably just saved me a significant amount of wasted time! Much appreciated!

    And yeah, I found several ways to produce the desired end result other than using IL, such as using the Roslyn SDK to compile my source files and make an assembly, there's even a NuGet package which made reference assembly images that allow you to break accessibility rules in a regular C# project. There are also "unsafe" methods like the one you demonstrated, a couple I found using actual pinned ref pointers and other scary stuff. But I finally felt like the simple IL approach using some extensions to MSBuild and the projects system made a really good way to go about this sort of feature development. It allows me to use IL when I want to like an actual language for development, and I found a good workflow for writing really optimized IL code (basically, let Roslyn show you how she would do it with max optimizations, then steal the ideas or even just copy the code and use it to fill in things fast). Gives you a lot of control over the assembly image you produce, and you can even do things you're technically not supposed to be able to do even if you compile C# in your service.

    The existing VSIX extension I found for IL support in .NET projects is only for 2019, unfortunately, and it needs a few more things to really make me happy. So I cloned the repo to see how it's all put together and I intend to build a new extension very soon which takes it quite a bit further and adds some major "quality of life" improvements. And one of the things I'm going to do is allow you to use your own custom Roslyn services to process specific files into different compilation types (like IL text/code file output, syntax tree, whatever) and then you can control your whole linking and output process to do pretty much whatever your heart desires ... all the things that are doable already but not very intuitive or pleasant to do. So you'll be able to use IL like a built-in language in projects with a bit better integration than the old 2019 extension offers, and then mix and match other languages and custom compiler services on an as-needed basis and of course you can also wire up all kinds of custom code generators and analyzers.

    If you have any good suggestions for this kind of extension and the tools, definitely let me know! The workflow, in general, for doing these kinds of unorthodox and "academic" things is pretty terrible, lol, there's a whole lot of clunking around between tools, banging out command-line inputs and manually doing steps that are very easy to mess up, and I want to change this, both for myself and others who like pushing these boundaries, breaking the ordinary rules and learning about all these nuances of the ecosystem.

    Thanks again for the insights and input, I'll definitely ping you in your inbox soon!
     
    TheZombieKiller likes this.
  18. atcarter714

    atcarter714

    Joined:
    Jul 25, 2021
    Posts:
    66
    I tested it out in Unity editor (2022.2.0.b14 beta) without the SecurityPermissionAttribute, just to see what would happen, and it actually worked as-is, just with IgnoresAccessChecksToAttribute ... seems to imply they support it now, but not sure what versions that does or does not apply to, and I'm gonna use the attribute to try to maximize backward compatibility with previous versions.



    By the way, @TheZombieKiller do you know what
    SecurityAction
    value to use for those older Unity versions? Just
    SecurityAction.Assert
    ?
     
  19. TheZombieKiller

    TheZombieKiller

    Joined:
    Feb 8, 2013
    Posts:
    266
    Unity definitely does not support it. Do you have AllowUnsafeBlocks enabled in the project file? The C# compiler will automatically add a SecurityPermissionAttribute with SkipVerification set to true in that case.

    SecurityAction.RequestMinimum
    .
     
    atcarter714 likes this.
  20. atcarter714

    atcarter714

    Joined:
    Jul 25, 2021
    Posts:
    66
    It works with both /unsafe enabled and disable in 2022.2 editor. Could they have possibly added support for it? Or is there more to it?

    And thanks for the extra info on the attribute and flags. I'm guessing that belongs on the assembly itself, right?
     
  21. TheZombieKiller

    TheZombieKiller

    Joined:
    Feb 8, 2013
    Posts:
    266
    IgnoresAccessChecksTo isn't supported in any version of Unity, not even the 2023 alpha builds. Try opening your .dll in something like ILSpy to verify if SecurityPermissionAttribute is there or not.

    Yup, check out AssemblyInfo.cs in the repository I linked earlier for an example.
     
    atcarter714 likes this.
  22. atcarter714

    atcarter714

    Joined:
    Jul 25, 2021
    Posts:
    66
    I put the binary and the code I used up on Github, and made a new project to share a collection of stuff I'm gonna add to. I've done so much work I didn't (or literally couldn't due to legal reasons like NDA, patents, etc) share that I am making it an effort to do some things I can share now, and I'll have a lot of stuff to add to this project. For now, I just wanted to share code and the DLL for Unity.

    https://github.com/atcarter714/UnityH4xx

    One problem I had is that the technique I used to build the C# and the IL code with an existing and outdated VS extension is not portable ... I literally cannot get that damn thing to work on any other machine except whichever one you start off on (it must be hard-coding absolute paths all over the place), and their MSBuild logic is too complicated and convoluted to waste time fighting with. I'm just gonna replace it with my own extension pack for VS 2022 soon and share that, time is too short to fight with old junk, lol. But you can always build the code manually using dotnet CLI tool or Powershell, it's not very hard, so I provided it there if anyone wants to. But you can just drag and drop the DLLs into your project in a "Plugins" folder (I'd put it in a subfolder in there so plugin DLLs don't mix). As soon as Unity rebuilds scripts you've got a new extension method for Lists and CollectionMarshal exists in the InteropServices namespace, nothing else needs to be done.

    I'll be adding a bunch of other stuff into that repo in the future and plan to publish some articles and videos about .NET and Unity soon.
     
    IgorBoyko likes this.
  23. ckocank

    ckocank

    Joined:
    Sep 29, 2020
    Posts:
    9
    Please do CollectionsMarshal.GetValueRefOrNullRef<TKey,TValue> Method
     
    atcarter714 likes this.
  24. atcarter714

    atcarter714

    Joined:
    Jul 25, 2021
    Posts:
    66
    I will look into that one soon. I've been really busy haha, launched a company and YouTube and Discord and a bunch of stuff, but I will revisit "Unity H4xX" project soon :)
     
  25. atcarter714

    atcarter714

    Joined:
    Jul 25, 2021
    Posts:
    66
    CrowbarSka likes this.
  26. atcarter714

    atcarter714

    Joined:
    Jul 25, 2021
    Posts:
    66
  27. atcarter714

    atcarter714

    Joined:
    Jul 25, 2021
    Posts:
    66
    I am really grateful for the people who have found me and our little Discord group from this and the Unity H4xx project I shared months ago. It seems people are ending up finding it here when searching for solutions to performance problems or wanting to do some kind of memory manipulation that isn't normally "allowed". Someone asked about how to get a pointer from the `Span<T>`, and the answer is: The same way it's done in .NET Core:

    Example Code with Unity H4xx

    Code (CSharp):
    1. using System.Collections.Generic;
    2. using System.Runtime.InteropServices;
    3. using System.Runtime.CompilerServices;
    4. using UnityEngine;
    5.  
    6. public class TestScript : MonoBehaviour
    7. {
    8.  
    9.     //! Create a list to test with
    10.     List<int> testList = new(){0,1,2,3,4,5,6,7};
    11.  
    12.     // Start is called before the first frame update
    13.     void Start( ) {
    14.         Debug.Log( "TestScript.Start():: Running Span<T> -> Pointer test!" );
    15.         var span = testList.AsSpan();
    16.      
    17.         //! --- "Unsafe" blocks allow pointers --------
    18.         //! Can also mark entire classes/methods "unsafe"
    19.         unsafe {
    20.             fixed ( int* ptr = span ) {
    21.                 for (int i = 0; i < span.Length; i++) {
    22.                     if( i is 0x03 ) overwriteMem( ptr + i ) ;
    23.  
    24.                     Debug.Log( $"ptr[ {i} ] = {ptr[ i ]} @ " +
    25.                                $"Address: { (ptr + i)->ToString("x8") }" ) ;
    26.                 }
    27.             }
    28.         }
    29.  
    30.         //! Even a local function can be "unsafe" ?!
    31.         [MethodImpl(0x100|0x200)]
    32.         static unsafe void overwriteMem( int* p ) => *p = 7 ;
    33.     }
    34. }
    This will print out the numbers we initialized in the `List<int>` we created. However, it will overwrite the memory at the offset of [ 3 ] with the value `7`! This proves that it actually reads/writes the underlying RAM and isn't referencing something or using a property or anything. By the same token, you're also taking great responsibility into your own hands not to screw that memory up! So be cautious, test things, read documentation and don't make assumptions. That being said, this is very, very powerful if used correctly and carefully. :)

    upload_2023-5-18_17-42-37.png

    Yes, you can add pointers with integers and do arithmetic to them, and you can use [ ] bracket indexing like an array! An array, after all, is nothing more than an abstraction around some memory being accessed by a pointer. :)

    You can find me here if you have more questions:
    https://discord.gg/JgqGhYPJ