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

Review projections of Swift generics in C# for CryptoKit dev template #2580

Open
Tracked by #95633
kotlarmilos opened this issue May 8, 2024 · 2 comments
Open
Tracked by #95633
Assignees
Labels
area-SwiftBindings Swift bindings for .NET

Comments

@kotlarmilos
Copy link
Member

Description

This proposal discusses the projection of Swift generics in C#. The primary goal is to validate the design of projections and integrate CryptoKit APIs for direct invocation from the dotnet/runtime repository. To limit the scope, this discussion specifically focuses on generics use-cases for the CryptoKit dev template. Protocols are considered as constraints within the Swift runtime, which should not affect functionality and they are not covered in this proposal.

Generics in Swift functions

In Swift, when a generic type is used as a parameter, its memory layout might not be known until runtime. To manage this, the generic parameter is represented by an opaque pointer. For object allocation, the runtime utilizes init functions from the value witness table using a metadata pointer. This metadata pointer is included as an implicit argument at the end of the function's signature, enabling the runtime to correctly allocate and manage generic types. When returning a generic type, the runtime utilizes indirect return result mechanism.

Here is a code snippet that illustrates how generics are handled in functions with the corresponding assembly code for arm64:

func returnData<T>(data: T) -> T {
    return data
}

As the function returns a generic type, the register x8 is expected to contain the return buffer where the value should be stored. The first parameter x0 is an opaque pointer; the second parameter x1 is the metadata pointer of the type.

00000000000079b0	sub	sp, sp, #0x30
00000000000079b4	stp	x29, x30, [sp, #0x20]
00000000000079b8	add	x29, sp, #0x20
00000000000079bc	str	x8, [sp]

This block shuffles regs for an object allocation. At the end, the x0 is the return buffer, x1 is the opaque pointer, and x2 is the metadata pointer.

00000000000079c0	mov	x2, x0
00000000000079c4	ldr	x0, [sp]
00000000000079c8	str	x2, [sp, #0x8]
00000000000079cc	mov	x2, x1
00000000000079d0	ldr	x1, [sp, #0x8]
00000000000079d4	str	xzr, [sp, #0x10]
00000000000079d8	mov	x8, x2
00000000000079dc	stur	x8, [x29, #-0x8]
00000000000079e0	mov	x8, x1

Next, x8 loads a value witness table pointer from -1 offset of the metadata pointer, and then loads the init function initializeBufferWithCopyOfBuffer. This function is invoked with the x0 return buffer , x1 opaque pointer, and x2 metadata pointer.

00000000000079e4	str	x8, [sp, #0x10]
00000000000079e8	ldur	x8, [x2, #-0x8]
00000000000079ec	ldr	x8, [x8, #0x10]
00000000000079f0	blr	x8
00000000000079f4	ldp	x29, x30, [sp, #0x20]
00000000000079f8	add	sp, sp, #0x30
00000000000079fc	ret

CryptoKit bindings for encryption and decryption

To directly call into CryptoKit APIs, the tooling should surface methods for encryption and decryption with Foundation.Data type:

static func seal<Plaintext, AuthenticatedData>(
    _ message: Plaintext,
    using key: SymmetricKey,
    nonce: ChaChaPoly.Nonce? = nil,
    authenticating authenticatedData: AuthenticatedData
) throws -> ChaChaPoly.SealedBox where Plaintext : DataProtocol, AuthenticatedData : DataProtocol
static func open<AuthenticatedData>(
    _ sealedBox: ChaChaPoly.SealedBox,
    using key: SymmetricKey,
    authenticating authenticatedData: AuthenticatedData
) throws -> Data where AuthenticatedData : DataProtocol

Following the generics implementation described earlier, below are proposed CryptoKit APIs in C#:

[UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvSwift) })]
[DllImport("/System/Library/Frameworks/CryptoKit.framework/CryptoKit")]
public unsafe static extern SealedBox PIfunc_seal(void* messagePtr, SymmetricKey key, Nonce nonce, void* authenticatedData, void* messageMetadata, void* aadMetadata);

public unsafe static SealedBox seal<Plaintext, AuthenticatedData>(Plaintext message, SymmetricKey key, Nonce nonce, AuthenticatedData authenticatedData)
{
    void* messagePtr = Unsafe.AsPointer(ref message);
    void* authenticatedDataPtr = Unsafe.AsPointer(ref authenticatedData);
    void* messageMetadata = Swift.Runtime.GetMetadata(message);
    void* aadMetadata = Swift.Runtime.GetMetadata(authenticatedData);
    return PIfunc_seal(messagePtr, key, nonce, authenticatedDataPtr, messageMetadata, aadMetadata);
}
[UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvSwift) })]
[DllImport("/System/Library/Frameworks/CryptoKit.framework/CryptoKit")]
public unsafe static extern Data PIfunc_open(SealedBox sealedBox, SymmetricKey key, void* authenticatedData, void* aadMetadata);

public unsafe static Data open<AuthenticatedData>(SealedBox sealedBox, SymmetricKey key, AuthenticatedData authenticatedData)
{
    void* authenticatedDataPtr = Unsafe.AsPointer(ref authenticatedData);
    void* aadMetadata = Swift.Runtime.GetMetadata(authenticatedData);
    return PIfunc_seal(sealedBox, key, authenticatedDataPtr, aadMetadata);
}

/cc: @jkoritzinsky @AaronRobinsonMSFT @stephen-hawley @rolfbjarne @jkotas

@kotlarmilos kotlarmilos added the area-SwiftBindings Swift bindings for .NET label May 8, 2024
@kotlarmilos kotlarmilos self-assigned this May 8, 2024
@jkotas
Copy link
Member

jkotas commented May 8, 2024

public unsafe static extern SealedBox PIfunc_seal

Should this be internal?

void* messagePtr = Unsafe.AsPointer(ref message);

These can be just void* messagePtr = &message; (may need to disable warning at the top of the file).

void* messageMetadata = Swift.Runtime.GetMetadata(message);

Do we need to guarantee that message is not collected by the GC after we extract messageMetadata from it? (e.g. via GC.KeepAlive after the end of the call)

@kotlarmilos
Copy link
Member Author

Do we need to guarantee that message is not collected by the GC after we extract messageMetadata from it? (e.g. via GC.KeepAlive after the end of the call)

Good point. Yes, we need to preserve it since messagePtr is unmanaged reference.

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