-
Notifications
You must be signed in to change notification settings - Fork 206
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
Changes from 6 commits
dafc364
971d6a4
52a698e
59ddccb
0e4e915
79ea572
6471146
0f6521f
d066f9b
277c41f
a6bd3b0
c6f42a2
66735db
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, sounds good. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 = PInvoke_Create(utf8BytesPtr, utf8Bytes.Length, 1); | ||
} | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Gets the length of string. | ||
/// </summary> | ||
public int Length => (int)PInvoke_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 = PInvoke_GetUtf8ContiguousArray(_payload); | ||
PInvoke_WithUnsafeBytes(bytes => | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How much do you care about performance? This will allocate a marshalled delegate on every call that is quite expensive operation. It would be a lot more efficient to use function pointers and use the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, this is a good point. We should implement the correct layout and memory management for the closure context. Also, we should be able to reduce the number of calls using a thin wrapper. Created a tracking issue: #2990 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doing this properly using function pointers is like 10-line edit on the new code added in this PR... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, let's fix that in this PR. |
||
{ | ||
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, should the callback signature have the closure There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes.
|
||
|
||
[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 PInvoke_Create(byte* str, long len, byte flag); | ||
|
||
[UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvSwift) })] | ||
[DllImport(KnownLibraries.SwiftCore, EntryPoint = "$sSS5countSivg")] | ||
public static extern long PInvoke_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 PInvoke_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 PInvoke_WithUnsafeBytes(CallbackDelegate callback, IntPtr context, IntPtr contiguousArray, TypeMetadata elementType, TypeMetadata resultType); | ||
} |
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()); | ||
} | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, sounds good.