bflattened / bflat

C# as you know it but with Go-inspired tooling (small, selfcontained, and native executables)
GNU Affero General Public License v3.0
3.63k stars 104 forks source link

Allow manual heap deallocation with zerolib #79

Open rozbf opened 1 year ago

rozbf commented 1 year ago

Currently, when using --stdlib:zero, all heap-allocated data are memory leaks since there is no GC.

This PR adds the ability to manually free heap-allocated objects using the GC.SuppressFinalize method. Why this method?

  1. It's a standard method from the dotnet API that takes an object.
  2. When using the dotnet runtime, calls to GC.SuppressFinalize are no-ops unless the type has a finalizer.
  3. When implementing the IDisposable interface, GC.SuppressFinalize is typically called at the end when all managed resources have been released. It makes sense to deallocate the object itself at that point.

Example:

using System;

class Work : IDisposable
{
    public string Name { get; set; } = nameof(Work);

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
    }
}

class Program
{
    public static void Main()
    {
        while(true)
        {
            using var x = new Work();
            Console.WriteLine(x.Name);
        }
    }
}

It is also possible to have nested objects if the IDisposable interface is implemented correctly. Arrays also work, but can't implement IDisposable, so they have to be freed by calling GC.SuppressFinalize directly.

I haven't tested the Linux and UEFI code. Feel free to edit this PR as needed.

MichalStrehovsky commented 1 year ago

Thanks!

I'll think about this a bit. I've been scratching my head on how to best approach this too. Repurposing GC.SuppressFinalize is definitely an out-of-the-box idea. I'm conflicted because on one hand it kind of is in the spirit of zerolib (no new API that doesn't exist in .NET), but on the other hand the meaning is different.

squidink7 commented 1 year ago

Perhaps this could involve the NativeMemory class (as without a GC, all zerolib memory is native memory), which has dedicated methods for alloc and free. Although the API ergonomics of those functions leave much to be desired..

rozbf commented 1 year ago

on one hand it kind of is in the spirit of zerolib (no new API that doesn't exist in .NET), but on the other hand the meaning is different

I don't think there is a solution that satisfies both. The closest thing C# has to manual memory management is the dispose pattern, which uses the GC.SuppressFinalize method to inform the garbage collector that the object has been disposed. Repurposing this method seems quite natural and together with the using keyword supported by the language, it produces something similar to std::unique_ptr in C++ (but without the ownership checks).

Btw, I also considered this approach:

int id = GC.GetGeneration(obj);
GC.Collect(id, GCCollectionMode.Optimized);

It is more explicit, but the implementation is complicated and it has a much higher overhead in .NET than GC.SuppressFinalize.

Perhaps this could involve the NativeMemory class

While it might be useful to provide access to the already implemented alloc/free functions via the NativeMemory class, it doesn't solve the problem of memory leaks when using managed reference types.

bartimaeusnek commented 1 year ago

How about adding a Method to object called Free, which can be called on anything, like ToString, Equals or GetHashCode?

CypherPotato commented 1 year ago

Why not just

unsafe static bool Free(void* ptr)
{
    ...
}

?

Since it ins't .NET, I see no point in creating an "GC" class for this. There is no GC here.

rozbf commented 1 year ago

Why not just

See here:

Public API surface that doesn't exist in .NET cannot be added (i.e. source code compilable against zerolib needs to be compilable against .NET).

trufae commented 1 year ago

Shouldn't this be void Free?

CypherPotato commented 1 year ago

Shouldn't this be void Free?

it could return an boolean indicating if the memory was freed or not.

tajOpti commented 11 months ago

Well if there are memory leaks why not implement a profiler that constantly tracks allocations and deallocations? I mean profilers are possible with native aot now for .NET

Dwedit commented 9 months ago

Freeing an object that's on the call stack turns your code into a minefield. Your this pointer now points to freed memory. You can't safely access any class fields or virtual methods at this point.

Also looking through the standard .NET framework, many functions use GC.SuppressFinalize on objects that aren't ready to actually be freed from memory. For example, the class System.IO.Stream. Close calls GC.SuppressFinalize. Dispose is implemented by calling the virtual function Close. So if you call Close, you can't call Dispose afterwards without a crash.

rozbf commented 9 months ago

Freeing an object that's on the call stack turns your code into a minefield. Your this pointer now points to freed memory. You can't safely access any class fields or virtual methods at this point.

This is no different from C/C++, which also has manual memory management. Originally, I was looking for a dotnet method that takes a ref object parameter, which could then set the reference to null, but it's kinda pointless since there may be other references to the deallocated object.

Also looking through the standard .NET framework, many functions use GC.SuppressFinalize on objects that aren't ready to actually be freed from memory. For example, the class System.IO.Stream. Close calls GC.SuppressFinalize. Dispose is implemented by calling the virtual function Close. So if you call Close, you can't call Dispose afterwards without a crash.

Yes, you should not call anything on the freed object. That's a responsibility the developer takes on when working with Zerolib. Zerolib will only work with specially tailored code that follows its constraints. The goal is that the same code should also work perfectly fine when compiled and run with dotnet. The reverse is not necessarily true - you can write code that will work fine with dotnet, but will crash with Zerolib.

ghost commented 7 months ago

Is it possible to use libc's free function on .NET object?

Tajbiul-Rawol commented 7 months ago

@iahung2 do you mean consuming windows API implementations? One question that I have idk If it is a practical question or not: But is it possible to reimplement these methods in C# by peeking the windows API implementations?

ghost commented 7 months ago

@iahung2 do you mean consuming windows API implementations? One question that I have idk If it is a practical question or not: But is it possible to reimplement these methods in C# by peeking the windows API implementations?

My question is simple. I have a class called MyClass. I created an object MyObject of type MyClass using new (heap allocation). As it's said on this thread, any heap allocation is a memory leak because there is no GC. I don't want to have a memory leak. So, I need a way to deallocate the memory. I want to know if something like free(MyObject); will work or not.

Tajbiul-Rawol commented 7 months ago

@iahung2 as far as I know, is you can't, allocating in heap isn't preferred and instead of reference types use structs as @MichalStrehovsky suggested to use stack allocation instead of heap, as heap allocation will eventually run out of memory due to no GC, you can try to see if free deallocates or not but this was my understanding from this statement.

https://twitter.com/MStrehovsky/status/1728378901188235301?t=NnuY-TJHNsztAasdfLQAxQ&s=19

ghost commented 7 months ago

@Tajbiul-Rawol My intuition tells me that free doesn't work. I only asked to be sure. @MichalStrehovsky You must do something. If there is no GC, you must add the ability to do manual memory management. Otherwise, your zerolib is only a toy, or the use cases of it will be very limited, for example, in resource-constrained environments like UEFI, where heap allocation isn't preferred. But for all other use cases, it's useless.

tajOpti commented 7 months ago

@iahung2 I agree. The tool feels very limited in what it can do, maybe because it is a brand new tool, but with no manual memory collection system for heap it is severely limiting. @MichalStrehovsky hope you change your decision and implement something to handle deallocations.

FrankRay78 commented 6 months ago

I'm going to give heap allocation for nostdlib a shot by overriding the .Net new() operator, as per my comment here: https://github.com/bflattened/bflat/issues/138#issuecomment-194946075

But if anyone wants to beat me to it, please feel free 😉

andrew-skybound commented 6 months ago

Here’s another idea: how about overriding GC.KeepAlive() to free an object? (Instead of GC.SuppressFinalize)

It might sound odd at first, but when you think about it, the purpose of KeepAlive is to ensure that the object stays alive until KeepAlive is called. Nobody calling that method should have the expectation that the object would live any longer, so in theory, it should be safe to release at that point.