dotnet / runtimelab

This repo is for experimentation and exploring new ideas that may or may not make it into the main dotnet/runtime repo.
MIT License
1.42k stars 198 forks source link

[Swift bindings] Design of Swift runtime features #2705

Open kotlarmilos opened 1 week ago

kotlarmilos commented 1 week ago

[!NOTE]
This is a draft issue that tracks feedback from the https://github.com/dotnet/runtimelab/pull/2704. Ii will be updated with more details soon.

Overview

This issue tracks the the existing design proposals of Swift runtime features required for the interop.

Metadata

The existing implementation from the CryptoKit example:

public static unsafe void* GetMetadata<T>(ISwiftObject obj) {
    return obj.Metadata;
}

Proposal based on measurements:

public static bool TryGetMetadata (Type t, out Metadata metadata)
{
    // one catch - t must not be an unbound generic.
    if (typeof(ISwiftObject).IsAssignableFrom(t)) {
        metadata = GetMetadataFromISwiftObject(t);
        return true;
    }
    // over time we add the cases we need for tuples, nint, nuint, nfloat, etc.
    // I'd like to see this ultimately be a ladder of `else if` constructs for each major type ordered by
    // most common and/or cheapest predicates. For example, identifying certain scalars should be
    // a matter of looking at `t.GetTypeCode()`
    return false;
}

static Metadata GetMetadataFromISwiftObject(Type t) // t is guaranteed to be ISwiftObject compatible 
{
    if (typeof(ISwiftObject).IsAssignableFrom(t))
    {
        var metadataProperty = t.GetProperty("Metadata", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public);
        if (metadataProperty != null)
        {
            var metadataValue = metadataProperty.GetValue(null);
            if (metadataValue is IntPtr ptr)
            {
                return ptr.ToPointer();
            }
        }
    }
    // no try/get pattern needed, this is a private API 
    throw new ArgumentException($"Type {t.Name} does not implement ISwiftObject.");
}

Consider creating a new type for the metadata - a struct containing either an IntPtr, a NativeHandle, or a void*. There will be sufficient functions that will have metadata as arguments that we are likely to have signature conflicts.

Protocol conformance descriptor

The existing implementation from the CryptoKit example:

public static unsafe void* GetConformanceDescriptor(string symbol)
  {
      IntPtr handle = IntPtr.Zero;
      try
      {
          handle = NativeLibrary.Load(Foundation.Path);
          void* conformanceDescriptor = NativeLibrary.GetExport(handle, symbol).ToPointer();
          return conformanceDescriptor;
      }
      catch (Exception ex)
      {
          throw new InvalidOperationException($"Failed to get conformance descriptor for symbol: {symbol}", ex);
      }
      finally
      {
          if (handle != IntPtr.Zero)
          {
              NativeLibrary.Free(handle);
          }
      }
  }
stephen-hawley commented 1 week ago

UnsafeMutablePointer

We will need a binding for UnsafeMutablePointer<T> but we need to be careful that it follows the Swift conventions. An UMP can be in any of the following states:

This is important because T could be a Swift type that is reference counted. Here is an example of code generated by BTfS which follows all of the states:

public func next() -> Optional<T0>
    {   
        let vt: SwiftIteratorProtocol_xam_vtable = getSwiftIteratorProtocol_xam_vtable(T0.self)!;

        // creates an UMP which is now allocated (but not initialized)
        let retval = UnsafeMutablePointer<Swift.Optional<T0>>.allocate(capacity: 1);

        // C# code gets called which will set the payload of the UMP which will initialize it, and in the process will bump any
        // reference counts. You can't simply blit the type into the allocated space. It is best to use the types value witness table
        // to copy it UNLESS the type is frozen and is blittable.
        vt.func0!(retval, toIntPtr(value: self));

        // Move uses the value witness table to pull the contents of the pointer out into a variable and in the process changes
        // the state from initialized to deinitialized
        let actualRetval = retval.move();

        // deallocate should only be called on a pointer that in the allocated or deinitialized states.
        retval.deallocate();
        return actualRetval;
    }

Therefore we should cleave as close as possible to the API of UMP to make it harder to use it incorrectly and we should document it usage thoroughly. Apple documents the states briefly here