Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Swift bindings] Design of Swift runtime features #2705

Open
Tracked by #95633
kotlarmilos opened this issue Oct 8, 2024 · 1 comment
Open
Tracked by #95633

[Swift bindings] Design of Swift runtime features #2705

kotlarmilos opened this issue Oct 8, 2024 · 1 comment
Assignees
Labels
area-SwiftBindings Swift bindings for .NET

Comments

@kotlarmilos
Copy link
Member

Note

This is a draft issue that tracks feedback from the #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
Copy link

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:

  • allocated
  • initialized
  • deinitialized
  • deallocated

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-SwiftBindings Swift bindings for .NET
Projects
None yet
Development

No branches or pull requests

2 participants