Silk.NET icon indicating copy to clipboard operation
Silk.NET copied to clipboard

Consider adding ref property overloads in structs for those who have a crippling fear of unsafe

Open Perksey opened this issue 5 years ago • 27 comments

Perksey avatar Jul 06 '20 20:07 Perksey

I beg you use in/out over ref

HurricanKai avatar Jul 06 '20 20:07 HurricanKai

For properties this isn't an option

Perksey avatar Jul 06 '20 20:07 Perksey

I'm sorry, but if properties in the title refer to the language construct, what is the current way? Are pointer properties the current way of doing this? (I've never seed a C# property in Silk so far)

HurricanKai avatar Jul 06 '20 20:07 HurricanKai

For example:

public struct InstanceCreateInfo
{
    public ApplicationInfo* PApplicationInfo;
    public ref ApplicationInfo PApplicationInfoRef => ...
}

However, now that I think about it, it may be better to generate alternative structs using this ref technique like ImGui.NET does instead of this.

Perksey avatar Jul 06 '20 20:07 Perksey

I'd argue both should be a property with a shared underlying field, also, then a ref property seems very reasonable, my apologies

HurricanKai avatar Jul 06 '20 20:07 HurricanKai

I'm pretty sure there's no good way to do this until dotnet/csharplang#1147 is in. Could compromise for Span overloads instead?

cc @JakenVeina

Perksey avatar Aug 28 '20 14:08 Perksey

We could store a pointer and convert to a ref?

HurricanKai avatar Aug 28 '20 14:08 HurricanKai

Ref properties can't have setters.

Perksey avatar Aug 28 '20 14:08 Perksey

https://docs.microsoft.com/de-de/dotnet/api/system.runtime.compilerservices.unsafe.asref?view=netcore-3.1#System_Runtime_CompilerServices_Unsafe_AsRef__1_System_Void__

HurricanKai avatar Aug 28 '20 14:08 HurricanKai

The problem isn't that we can't convert pointers to refs, it's that the property overloads will only work one-way. For example, you won't be able to:

applicationInfo.PNextRef = ref _nextThing;

Because the C# language forbids ref properties having setters. However, this would work:

ref applicationInfo.PNextRef

But if you haven't assigned the pointer value already, this property would return Unsafe.NullRef

Perksey avatar Aug 28 '20 14:08 Perksey

Methods, maybe?

HurricanKai avatar Aug 28 '20 14:08 HurricanKai

Could do methods, yeah.

Perksey avatar Aug 28 '20 14:08 Perksey

Oh wait no because there's no guarantee that the ref will be valid without it being within a fixed block. Unsafe.AsPointer isn't safe here.

Perksey avatar Aug 28 '20 14:08 Perksey

To me, the concept of "unsafe" in this context means having to build the struct tree that the native APIs call for, because it has to be done via pointers. A "safe" mechanism should be doing this work for you, not just exposing the same work to be done, but without pointers.

E.G. if an outer struct requires a field that points to an inner struct, the outer struct should allocate both, and handle the pointer logic for connecting them together, and simply expose a ref of the inner struct, for the caller to manipulate. I suppose this would probably necessitate that this logic is done in a wrapper struct that manages BOTH different native structs.

I know most of the APIs are actually auto-generated, here, so I don't know how feasible this idea is. If there's an upcoming language mechanism that would solve this problem, you're free to wait for that as well. But to me, that' the kinda "safe" solution that I'd go for.

JakenVeina avatar Aug 28 '20 14:08 JakenVeina

I can do just about anything with the generated APIs, so if you could propose what your idea would be in terms of the existing files (perhaps reuse the ApplicationInfo.gen.cs example) then that'd be great, I'm a lot better at reading code than I am English.

Perksey avatar Aug 28 '20 14:08 Perksey

I would 100% agree with what @JakenVeina said. A fully save wrapper that does all the initialization work for you is ideal, when you want save wrappers. Currently we are just looking into adding some save-ish overloads, but we could be looking into doing more then that.

Can ref properties have init things? That would somewhat solve the problem too.

HurricanKai avatar Aug 28 '20 15:08 HurricanKai

Ref properties, unfortunately, are strictly get-only. If we had ref fields, it would be easy to tackle as we'd be able to just use a union. If either of you have ideas please let me know as I'm really struggling to picture what Jake said in terms of code.

Perksey avatar Aug 28 '20 15:08 Perksey

If I understand what he said correctly, this would mean a fully save wrapper. Ie, the constructor initializes all resources, handles updates, handle lifetime of underlying native resources, etc. etc. With some performance loss we could probaby generate this too, but it would be a good amount of work.

HurricanKai avatar Aug 28 '20 15:08 HurricanKai

If I had code I could probably fairly easily generate it, but I don't even know what the safe wrapper would look like atm.

Perksey avatar Aug 28 '20 15:08 Perksey

Consider a current struct like:

public struct SomeNativeResource
{
    public byte* PApplicationName; // string
    public byte* PWindowTitle; // string
}

a new type like

public struct SomeNativeStructButManaged : IDisposable // idk maybe indicated by a namespace?
{
    public string ApplicationName { get { ... } set { ... } }
    public string WindowTitle { get { ... } set { ... } }

    public SomeNativeStructButManaged(string applicationName, string windowTitle) { ... allocate native strings and assign to some backing field 
    

    public void Dispose() // dispose pattern, but I can't be bothered to type that out in the web browser...
    {
        ... dispose of backing native resources ...
    }
}

I mean that doesn't solve the ref problem, but I think that's what @JakenVeina ment? Am I correct here? This would of course also work for other native resources. A difficult task. yeah...

HurricanKai avatar Aug 28 '20 15:08 HurricanKai

Yeah. It's really just the ref problem I have no idea how to solve, but yeah that looks like something that I could do fairly easily (would require SilkTouch work, though)

Perksey avatar Aug 28 '20 15:08 Perksey

I do think that's mostly a thing SilkTouch would handle? (with some BuildTools attribute backing) Would move this into 3.0 if we choose this route. A span compromise would be nice imo :)

HurricanKai avatar Aug 28 '20 15:08 HurricanKai

For something like that, I don't see that needing to be disposable.

Lemme see if I can re-create a concrete example...

Right now Silk.NET has...

public struct InstanceCreateInfo
{
        public StructureType SType;
        public void* PNext;
        public uint Flags;
        public ApplicationInfo* PApplicationInfo;
        public uint EnabledLayerCount;
        public byte** PpEnabledLayerNames;
        public uint EnabledExtensionCount;
        public byte** PpEnabledExtensionNames;
}

public struct ApplicationInfo
    {
        public StructureType SType;
        public void* PNext;
        public byte* PApplicationName;
        public uint ApplicationVersion;
        public byte* PEngineName;
        public uint EngineVersion;
        public uint ApiVersion;
    }

A "safe wrapper" for this, in my mind (ignoring the strings and extension pointers), would look something like.....

public ref struct SafeInstanceCreateInfo
{
    public uint Flags
    {
        get => _instanceCreateInfo.Flags;
        set => _instanceCreateInfo.Flags = value;
    }
        
    public uint EnabledLayerCount
    {
        get => _instanceCreateInfo.EnabledLayerCount;
        set => _instanceCreateInfo.EnabledLayerCount = value;
    }
        
    public uint EnabledExtensionCount
    {
        get => _instanceCreateInfo.EnabledExtensionCount;
        set => _instanceCreateInfo.EnabledExtensionCount = value;
    }

    ...

    public SafeApplicationInfo ApplicationInfo
        => _applicationInfo;

    internal InstanceCreateInfo _instanceCreateInfo;
    internal SafeApplicationInfo _applicationInfo;
}

public ref struct SafeApplicationInfo
{
    public uint ApplicationVersion
    {
        get => _applicationInfo.ApplicationVersion;
        set => _applicationInfo.ApplicationVersion = value;
    }

    public uint EngineVersion
    {
        get => _applicationInfo.EngineVersion;
        set => _applicationInfo.EngineVersion = value;
    }

    public uint ApiVersion
    {
        get => _applicationInfo.ApiVersion;
        set => _applicationInfo.ApiVersion = value;
    }

    ...

    internal ApplicationInfo _applicationInfo;
}

I'm sure I've screwed up some of the technicals here, but that'd be the general idea.

Then, the CreateInstance() API method would accept a ref parameter of this struct, do a few quick finalization operations on the structs (which are already fully allocated) and pass them off to the native calls, as such...

safeCreateInstanceInfo._createInstanceInfo.SType = ...;
safeCreateInstanceInfo._createInstanceInfo.PApplicationInfo = &safeCreateInstanceInfo.ApplicationInfo._applicationInfo;
safeCreateInstanceInfo.ApplicationInfo._applicationInfo.SType = ...;

JakenVeina avatar Aug 28 '20 17:08 JakenVeina

Discussed in working group meeting

  • Getter method on structs with one return type overload
  • Wither method on structs with multiple parameter type overloads
  • https://github.com/mellinoe/ImGui.NET/blob/master/src/ImGui.NET/Generated/ImGuiIO.gen.cs#L106 except with getters and setters instead of ref properties so that people don't go wrong with the ref's lifetime

Perksey avatar Dec 11 '20 22:12 Perksey

The currently approved model was making my cry so we need to take a step back and re-evaluate it. What I'm thinking is:

  • a Ptr<T> type so those really afraid of pointers can pretend they don't exist
  • similar to functions, we add property overloads with the S suffix with spans, strings, string arrays, and IntPtrs.

Postponing to 2.X

Perksey avatar Dec 31 '20 20:12 Perksey

We should consider overloading existing ctors similar to how we overload methods already.

HurricanKai avatar Dec 31 '20 20:12 HurricanKai

Provisionally moving this into 3.0.

Perksey avatar Dec 18 '23 23:12 Perksey