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

Conversation

kotlarmilos
Copy link
Member

Description

This PR projects Swift.String used in products(for:).

In Swift, String is defined as frozen struct String with an internal payload of Swift.Data. The projection is a thin C# wrapper around Swift.Data, with methods to convert between UTF-8 and UTF-16 encodings.

Out of Scope

Fixes #2831

@kotlarmilos kotlarmilos added the area-SwiftBindings Swift bindings for .NET label Feb 6, 2025
@kotlarmilos kotlarmilos self-assigned this Feb 6, 2025
@kotlarmilos kotlarmilos changed the title [Swift language feature] Implement empty Swift.String in C# [Swift language feature] Implement Swift.String wrapper in C# Feb 6, 2025
@vitek-karas
Copy link
Member

I think we should have a discussion if we want to project string (which is what this PR does) or if we want to always marshal string. Personal preference would be to marshal string whenever possible (params, return value), but I guess we need the projection just like we will likely need boxes for other primitive types (mainly due to protocol conformance).
Either way we should have this decision discussed and written down somewhere.

@kotlarmilos
Copy link
Member Author

I think we should have a discussion if we want to project string

Let's collect input on this PR.

Personal preference would be to marshal string whenever possible (params, return value)

Could you provide more details?

I guess we need the projection just like we will likely need boxes for other primitive types (mainly due to protocol conformance).

Yes + it is lowered as (i64, ptr).

@vitek-karas
Copy link
Member

More details on the personal preference:
Let's say we have a swift code like this:

Label("Your name please:")  // Assuming Label init takes a string parameter, which it doesn't, but it's a good example

When projected to C# I would want to be able to write:

new Label("Your name please:");

Basically I should not be forced to write things like:

new Label(new SwiftString("Your name please:"));

But we might be able to achieve this with implicit casts.

@kotlarmilos
Copy link
Member Author

When projected to C# I would want to be able to write:

new Label("Your name please:");

Yes, this is a UX decision.

The preferred Swift encoding is UTF-8: https://www.swift.org/blog/utf8-string/. Currently, we access it through ContiguousArray and use a closure to access the array’s contiguous storage.

@@ -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.

/// <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.

string? str = null;

var arr = PInvoke_GetUtf8ContiguousArray(_payload);
PInvoke_WithUnsafeBytes(bytes =>
Copy link
Member

Choose a reason for hiding this comment

The 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 context argument to pass the local context.

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, 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

Copy link
Member

Choose a reason for hiding this comment

The 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...

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok, let's fix that in this PR.

}
}

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.

Copy link
Member

@jkotas jkotas left a comment

Choose a reason for hiding this comment

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

This is a better way to do it without an extra copy

src/Swift.Runtime/src/Swift/SwiftString.cs Outdated Show resolved Hide resolved
src/Swift.Runtime/src/Swift/SwiftString.cs Outdated Show resolved Hide resolved
@kotlarmilos
Copy link
Member Author

kotlarmilos commented Feb 12, 2025

This is a better way to do it without an extra copy

Changed ToStringCallbackContext to a ref type to allow the callback to take an unmanaged pointer, as it contains managed fields.

Copy link
Member

@vitek-karas vitek-karas left a comment

Choose a reason for hiding this comment

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

Thanks!

@kotlarmilos kotlarmilos merged commit 4364aa8 into dotnet:feature/swift-bindings Feb 12, 2025
6 checks passed
@kotlarmilos
Copy link
Member Author

@jkotas Thanks for help! If you have any additional feedback regarding the function pointers, please let me know and we can address them in a follow-up.

@jkotas
Copy link
Member

jkotas commented Feb 12, 2025

Changed ToStringCallbackContext to a ref type to allow the callback to take an unmanaged pointer, as it contains managed fields.

That's just an annoying C# warning. It is fine to disable it. We have it disabled globally in dotnet/runtime. We use the pattern that I have suggested in many places. One example from many: https://github.com/dotnet/runtime/blob/c6fd0543b67aec813b16fc9458afd2ec2716178b/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.Nls.cs#L370-L403

There is nothing wrong with taking an address of a struct on stack that contains object references. You just need to be careful about what you are doing - like you need to be careful everywhere else in unsafe code. GCHandle and the reference type instead of struct are just a bunch of unnecessary overhead.

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

Successfully merging this pull request may close these issues.

5 participants