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 language feature] Implement Swift.String wrapper in C# #2983

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,19 @@ private static unsafe int SumSet(SwiftSet<SwiftIntMock> set)
[DllImport("Structs/libStructsTests.dylib", EntryPoint = "$s12StructsTests8sumArray5arrays5Int32VSayAEG_tF")]
private static extern int PInvoke_SumSet(Variant array);

[Fact]
public void TestString()
{
SwiftString swiftString = StructsTests.getString();
int result = StructsTests.verifyString(swiftString);
Assert.Equal(42, result);

string str = swiftString.ToString();
Assert.Equal("Hello world!", str);

swiftString = new SwiftString(str);
result = StructsTests.verifyString(swiftString);
Assert.Equal(42, result);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -257,3 +257,18 @@ public func sumArray(array: Array<Int32>) -> Int32
{
return array.reduce(0, +)
}

public func getString() -> String
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand this is here as it requires some swift code. But I dont think the label 'struct tests' describes it very well. Maybe we should create a new test file?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, sounds good.

{
return "Hello world!"
}

public func verifyString(str: String) -> Int32
{
if str == "Hello world!" {
return 42
}

return -1
}

3 changes: 3 additions & 0 deletions src/Swift.Runtime/src/Swift/FoundationDatabase.xml
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,8 @@
<entity managedNameSpace="Swift" managedTypeName="UnsafeMutableBufferPointer&lt;System.Byte&gt;">
<typedeclaration kind="struct" name="UnsafeMutableBufferPointer&lt;Swift.UInt8&gt;" module="Swift" mangledName="sSr" frozen="true" blittable="true"/>
</entity>
<entity managedNameSpace="Swift" managedTypeName="SwiftString">
<typedeclaration kind="struct" name="String" module="Swift" mangledName="sSS" frozen="true" blittable="true"/>
</entity>
</entities>
</swifttypedatabase>
3 changes: 3 additions & 0 deletions src/Swift.Runtime/src/Swift/SwiftArray.cs
Original file line number Diff line number Diff line change
Expand Up @@ -272,9 +272,11 @@ public unsafe Element this[int index]

internal static class SwiftArrayPInvokes
{
[UnmanagedCallConv(CallConvs = [typeof(CallConvSwift)])]
[DllImport(KnownLibraries.SwiftCore, EntryPoint = "$sSaMa")]
public static extern TypeMetadata PInvoke_getMetadata(TypeMetadataRequest request, TypeMetadata typeMetadata);

[UnmanagedCallConv(CallConvs = [typeof(CallConvSwift)])]
[DllImport(KnownLibraries.SwiftCore, EntryPoint = "$sS2ayxGycfC")]
public static extern ArrayBuffer Init(TypeMetadata typeMetadata);

Expand All @@ -286,6 +288,7 @@ internal static class SwiftArrayPInvokes
[DllImport(KnownLibraries.SwiftCore, EntryPoint = "$sSayxSicis")]
public static unsafe extern void Set(SwiftHandle value, nint index, TypeMetadata elementMetadata, SwiftSelf self);

[UnmanagedCallConv(CallConvs = [typeof(CallConvSwift)])]
[DllImport(KnownLibraries.SwiftCore, EntryPoint = "$sSa5countSivg")]
public static extern nint Count(ArrayBuffer handle, TypeMetadata elementMetadata);

Expand Down
164 changes: 164 additions & 0 deletions src/Swift.Runtime/src/Swift/SwiftString.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Swift;
using System.Text;
using Swift.Runtime;
using Swift.Runtime.InteropServices;

namespace Swift;

/// <summary>
/// Represents Foundation.Data type.
/// </summary>
public struct Data
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: This is used outside of string extensively right? I think we encountered pointers using that when projecting crypto kit - maybe it deserves it own file

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, sounds good.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved and created a tracking issue: #2992.

{
private long _flags;
private IntPtr _object;
}

/// <summary>
/// Represents a Swift string with Foundation.Data payload.
/// </summary>
public struct SwiftString : ISwiftObject
{
static nuint _payloadSize = SwiftObjectHelper<SwiftString>.GetTypeMetadata().Size;

private Data _payload;
kotlarmilos marked this conversation as resolved.
Show resolved Hide resolved

private static Dictionary<Type, string> _protocolConformanceSymbols;

static SwiftString()
{
_protocolConformanceSymbols = new Dictionary<Type, string> { };
}

public static nuint PayloadSize => _payloadSize;
vitek-karas marked this conversation as resolved.
Show resolved Hide resolved

public Data Payload => _payload;

static TypeMetadata ISwiftObject.GetTypeMetadata()
{
return TypeMetadata.Cache.GetOrAdd(typeof(SwiftString), _ => PInvoke_getMetadata());
}

static ISwiftObject ISwiftObject.NewFromPayload(SwiftHandle handle)
{
return new SwiftString(handle);
}

IntPtr ISwiftObject.MarshalToSwift(IntPtr swiftDest)
{
var metadata = SwiftObjectHelper<SwiftString>.GetTypeMetadata();
unsafe
{
fixed (void* _payloadPtr = &_payload)
{
metadata.ValueWitnessTable->InitializeWithCopy((void*)swiftDest, (void*)_payloadPtr, metadata);
}
}
return swiftDest;
}

/// <summary>
/// Gets the protocol conformance descriptor for the given type.
/// </summary>
/// <typeparam name="TProtocol"></typeparam>
/// <returns></returns>
static ProtocolConformanceDescriptor ISwiftObject.GetProtocolConformanceDescriptor<TProtocol>()
where TProtocol : class
{
if (!_protocolConformanceSymbols.TryGetValue(typeof(TProtocol), out var symbolName))
{
throw new SwiftRuntimeException($"Attempted to retrieve protocol conformance descriptor for type SwiftString and protocol {typeof(TProtocol).Name}, but no conformance was found.");
}
return ProtocolConformanceDescriptor.LoadFromSymbol("/usr/lib/swift/libswiftCore.dylib", symbolName);
}

/// <summary>
/// Constructs a new SwiftString from the given handle.
/// </summary>
unsafe SwiftString(SwiftHandle handle)
{
_payload = *(Data*)handle;
}

/// <summary>
/// Constructs a new SwiftString from the C# string.
/// </summary>
public SwiftString(string str)
{
byte[] utf8Bytes = Encoding.UTF8.GetBytes(str);
unsafe
{
fixed (byte* utf8BytesPtr = utf8Bytes)
{
_payload = Create(utf8BytesPtr, utf8Bytes.Length, 1);
}
}
}

/// <summary>
/// Gets the length of string.
/// </summary>
public int Length => (int)GetLength(_payload);

/// <summary>
/// Converts the SwiftString to a C# string.
/// </summary>
public override string ToString()
{
unsafe
{
var elementType = TypeMetadata.GetTypeMetadataOrThrow<byte>();
var resultType = TypeMetadata.GetTypeMetadataOrThrow<long>();

var len = Length;
if (len <= 0)
return string.Empty;

string? str = null;

var arr = GetUtf8ContiguousArray(_payload);
WithUnsafeBytes(bytes =>
{
unsafe
{
str = Encoding.UTF8.GetString((byte*)bytes, len);
return IntPtr.Zero;
}
}, IntPtr.Zero, arr, elementType, resultType);

return str!;
}
}

public unsafe delegate IntPtr CallbackDelegate(IntPtr param);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the callback have default calling convention or Swift calling convention? (Related to my other comment about function pointers.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, should the callback signature have the closure context as the argument?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.

ToString has been refactored to use function pointers. The length is passed through the closure context to the callback via the self register. A utf8 buffer is allocated, and the content of the contiguous array is copied into it. The buffer is then marshalled using PtrToStringUTF8, and the native buffer is deallocated.


[UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvSwift) })]
[DllImport(KnownLibraries.SwiftCore, EntryPoint = "$sSSMa")]
public static extern TypeMetadata PInvoke_getMetadata();

[UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvSwift) })]
[DllImport(KnownLibraries.SwiftCore, CharSet = CharSet.Unicode, EntryPoint = "$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC")]
public static unsafe extern Data Create(byte* str, long len, byte flag);
kotlarmilos marked this conversation as resolved.
Show resolved Hide resolved

[UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvSwift) })]
[DllImport(KnownLibraries.SwiftCore, EntryPoint = "$sSS5countSivg")]
public static extern long GetLength(Data str);

// https://developer.apple.com/documentation/swift/string/utf8cstring
[UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvSwift) })]
[DllImport(KnownLibraries.SwiftCore, EntryPoint = "$sSS11utf8CStrings15ContiguousArrayVys4Int8VGvg")]
public static unsafe extern IntPtr GetUtf8ContiguousArray(Data str);

// https://developer.apple.com/documentation/swift/contiguousarray/withunsafebytes(_:)
[UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvSwift) })]
[DllImport(KnownLibraries.SwiftCore, EntryPoint = "$ss15ContiguousArrayV15withUnsafeBytesyqd__qd__SWKXEKlF")]
public static extern unsafe IntPtr WithUnsafeBytes(CallbackDelegate callback, IntPtr context, IntPtr contiguousArray, TypeMetadata elementType, TypeMetadata resultType);
kotlarmilos marked this conversation as resolved.
Show resolved Hide resolved
}
3 changes: 2 additions & 1 deletion src/Swift.Runtime/tests/LibraryTests/SwiftArrayTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ public unsafe void ArrayDispose()
Arc.Release(payload);
}

private static void PrimitiveArrayTest<T>(T value1, T value2, T overwriteValue) where T : unmanaged
private static void PrimitiveArrayTest<T>(T value1, T value2, T overwriteValue)
{
var metadata = TypeMetadata.GetTypeMetadataOrThrow<SwiftArray<T>>();
Assert.True(metadata.Size > 0);
Expand Down Expand Up @@ -181,4 +181,5 @@ private static void PrimitiveArrayTest<T>(T value1, T value2, T overwriteValue)
[Fact] public void ArrayTestFloat() => PrimitiveArrayTest<float>(4.2f, 1.7f, 10.0f);
[Fact] public void ArrayTestDouble() => PrimitiveArrayTest<double>(4.2, 1.7, 10.0);
[Fact] public void ArrayTestBool() => PrimitiveArrayTest<bool>(true, false, true);
[Fact] public void ArrayTestString() => PrimitiveArrayTest<SwiftString>(new SwiftString("Hello"), new SwiftString("World"), new SwiftString("String"));
}
48 changes: 48 additions & 0 deletions src/Swift.Runtime/tests/LibraryTests/SwiftStringTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Reflection;
using System.Runtime.InteropServices;
using Swift;
using Swift.Runtime;
using Xunit;

namespace BindingsGeneration.Tests;

public class SwiftStringTests : IClassFixture<SwiftStringTests.TestFixture>
{
private readonly TestFixture _fixture;

public SwiftStringTests(TestFixture fixture)
{
_fixture = fixture;
}

public class TestFixture
{
static TestFixture()
{
}

private static void InitializeResources()
{
}
}

[Fact]
static void SmokeTest()
{
var metadata = TypeMetadata.GetTypeMetadataOrThrow<SwiftString>();
// sizeof(Data)
Assert.Equal((nuint)16, metadata.Size);

var str = new SwiftString();
Assert.Equal(0, str.Length);

string text = "Hello world!";
str = new SwiftString(text);

Assert.Equal(text.Length, str.Length);
Assert.Equal(text, str.ToString());
}
}