Skip to content

[JExtract/FFM] Translate 'some DataProtocol' parameters to 'Data' #303

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

Merged
merged 2 commits into from
Jul 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ public func withBuffer(body: (UnsafeRawBufferPointer) -> Void) {
body(globalBuffer)
}

public func globalReceiveSomeDataProtocol(data: some DataProtocol) -> Int {
p(Array(data).description)
return data.count
}

// ==== Internal helpers

func p(_ msg: String, file: String = #fileID, line: UInt = #line, function: String = #function) {
Expand Down
3 changes: 2 additions & 1 deletion Samples/SwiftKitSampleApp/ci-validate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
set -x
set -e

./gradlew run
./gradlew run
./gradlew test
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@
import org.swift.swiftkit.ffm.AllocatingSwiftArena;
import org.swift.swiftkit.ffm.SwiftRuntime;

import java.lang.foreign.MemoryLayout;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;

public class HelloJava2Swift {

public static void main(String[] args) {
Expand Down Expand Up @@ -95,6 +91,12 @@ static void examples() {
});
}

try (var arena = AllocatingSwiftArena.ofConfined()) {
var bytes = arena.allocateFrom("hello");
var dat = Data.init(bytes, bytes.byteSize(), arena);
MySwiftLibrary.globalReceiveSomeDataProtocol(dat);
Copy link
Member Author

Choose a reason for hiding this comment

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

@ktoso I'd be happy to make this and Data thing test cases, but where exactly I should write? Any PR testing runs Samples:SwiftKitSampleApp:test?

Copy link
Collaborator

Choose a reason for hiding this comment

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

So every example has a ci-validate.sh and you can add a ./gradle test in there in Samples/SwiftKitSampleApp/ci-validate.sh and add a test in Samples/SwiftKitSampleApp/src/test/java/com/example/swift :)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh boy I just realized we have tests there but we've not been running them huh. so yeah please add a gradle test there :)

Copy link
Member Author

Choose a reason for hiding this comment

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

Done!

}


System.out.println("DONE.");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift.org project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift.org project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

package com.example.swift;

import org.junit.jupiter.api.Test;
import org.swift.swiftkit.ffm.AllocatingSwiftArena;

import static org.junit.jupiter.api.Assertions.*;

public class DataImportTest {
@Test
void test_Data_receiveAndReturn() {
try (var arena = AllocatingSwiftArena.ofConfined()) {
var origBytes = arena.allocateFrom("foobar");
var origDat = Data.init(origBytes, origBytes.byteSize(), arena);
assertEquals(7, origDat.getCount());

var retDat = MySwiftLibrary.globalReceiveReturnData(origDat, arena);
assertEquals(7, retDat.getCount());
retDat.withUnsafeBytes((retBytes) -> {
assertEquals(7, retBytes.byteSize());
var str = retBytes.getString(0);
assertEquals("foobar", str);
});
}
}

@Test
void test_DataProtocol_receive() {
try (var arena = AllocatingSwiftArena.ofConfined()) {
var bytes = arena.allocateFrom("hello");
var dat = Data.init(bytes, bytes.byteSize(), arena);
var result = MySwiftLibrary.globalReceiveSomeDataProtocol(dat);
assertEquals(6, result);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ extension CType {
init(cdeclType: SwiftType) throws {
switch cdeclType {
case .nominal(let nominalType):
if let knownType = nominalType.nominalTypeDecl.knownStandardLibraryType {
if let knownType = nominalType.nominalTypeDecl.knownTypeKind {
if let primitiveCType = knownType.primitiveCType {
self = primitiveCType
return
Expand Down Expand Up @@ -68,7 +68,7 @@ extension CType {
case .optional(let wrapped) where wrapped.isPointer:
try self.init(cdeclType: wrapped)

case .metatype, .optional, .tuple:
case .metatype, .optional, .tuple, .opaque, .existential:
throw CDeclToCLoweringError.invalidCDeclType(cdeclType)
}
}
Expand Down Expand Up @@ -125,7 +125,7 @@ extension SwiftKnownTypeDeclKind {
.qualified(const: true, volatile: false, type: .void)
)
case .void: .void
case .unsafePointer, .unsafeMutablePointer, .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer, .unsafeBufferPointer, .unsafeMutableBufferPointer, .string, .data:
case .unsafePointer, .unsafeMutablePointer, .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer, .unsafeBufferPointer, .unsafeMutableBufferPointer, .string, .data, .dataProtocol:
nil
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ extension FFMSwift2JavaGenerator {
struct CdeclLowering {
var knownTypes: SwiftKnownTypes

init(knownTypes: SwiftKnownTypes) {
self.knownTypes = knownTypes
}

init(symbolTable: SwiftSymbolTable) {
self.knownTypes = SwiftKnownTypes(symbolTable: symbolTable)
}
Expand Down Expand Up @@ -165,7 +169,7 @@ struct CdeclLowering {
)

case .nominal(let nominal):
if let knownType = nominal.nominalTypeDecl.knownStandardLibraryType {
if let knownType = nominal.nominalTypeDecl.knownTypeKind {
if convention == .inout {
// FIXME: Support non-trivial 'inout' for builtin types.
throw LoweringError.inoutNotSupported(type)
Expand Down Expand Up @@ -320,6 +324,18 @@ struct CdeclLowering {
conversion: conversion
)

case .opaque(let proto), .existential(let proto):
// If the protocol has a known representative implementation, e.g. `String` for `StringProtocol`
// Translate it as the concrete type.
// NOTE: This is a temporary workaround until we add support for generics.
if
let knownProtocol = proto.asNominalTypeDeclaration?.knownTypeKind,
let concreteTy = knownTypes.representativeType(of: knownProtocol)
{
return try lowerParameter(concreteTy, convention: convention, parameterName: parameterName)
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍

throw LoweringError.unhandledType(type)

case .optional:
throw LoweringError.unhandledType(type)
}
Expand Down Expand Up @@ -386,7 +402,7 @@ struct CdeclLowering {

switch type {
case .nominal(let nominal):
if let knownType = nominal.nominalTypeDecl.knownStandardLibraryType {
if let knownType = nominal.nominalTypeDecl.knownTypeKind {
switch knownType {
case .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer:
// pointer buffers are lowered to (raw-pointer, count) pair.
Expand Down Expand Up @@ -421,7 +437,7 @@ struct CdeclLowering {
// Custom types are not supported yet.
throw LoweringError.unhandledType(type)

case .function, .metatype, .optional, .tuple:
case .function, .metatype, .optional, .tuple, .existential, .opaque:
// TODO: Implement
throw LoweringError.unhandledType(type)
}
Expand Down Expand Up @@ -454,7 +470,7 @@ struct CdeclLowering {

case .nominal(let nominal):
// Types from the Swift standard library that we know about.
if let knownType = nominal.nominalTypeDecl.knownStandardLibraryType {
if let knownType = nominal.nominalTypeDecl.knownTypeKind {
switch knownType {
case .unsafePointer, .unsafeMutablePointer:
// Typed pointers are lowered to corresponding raw forms.
Expand Down Expand Up @@ -575,7 +591,7 @@ struct CdeclLowering {
conversion: .tupleExplode(conversions, name: outParameterName)
)

case .function(_), .optional(_):
case .function, .optional, .existential, .opaque:
throw LoweringError.unhandledType(type)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +113,16 @@ extension FFMSwift2JavaGenerator {
}

struct JavaTranslation {
var symbolTable: SwiftSymbolTable
var knownTypes: SwiftKnownTypes

init(symbolTable: SwiftSymbolTable) {
self.knownTypes = SwiftKnownTypes(symbolTable: symbolTable)
}

func translate(
_ decl: ImportedFunc
) throws -> TranslatedFunctionDecl {
let lowering = CdeclLowering(symbolTable: symbolTable)
let lowering = CdeclLowering(knownTypes: knownTypes)
let loweredSignature = try lowering.lowerFunctionSignature(decl.functionSignature)

// Name.
Expand Down Expand Up @@ -208,7 +212,7 @@ extension FFMSwift2JavaGenerator {

switch type {
case .nominal(let nominal):
if let knownType = nominal.nominalTypeDecl.knownStandardLibraryType {
if let knownType = nominal.nominalTypeDecl.knownTypeKind {
switch knownType {
case .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer:
return TranslatedParameter(
Expand Down Expand Up @@ -248,11 +252,12 @@ extension FFMSwift2JavaGenerator {
// 'self'
let selfParameter: TranslatedParameter?
if case .instance(let swiftSelf) = swiftSignature.selfParameter {
selfParameter = try self.translate(
swiftParam: swiftSelf,
selfParameter = try self.translateParameter(
type: swiftSelf.type,
convention: swiftSelf.convention,
parameterName: swiftSelf.parameterName ?? "self",
loweredParam: loweredFunctionSignature.selfParameter!,
methodName: methodName,
parameterName: swiftSelf.parameterName ?? "self"
methodName: methodName
)
} else {
selfParameter = nil
Expand All @@ -263,11 +268,12 @@ extension FFMSwift2JavaGenerator {
.map { (idx, swiftParam) in
let loweredParam = loweredFunctionSignature.parameters[idx]
let parameterName = swiftParam.parameterName ?? "_\(idx)"
return try self.translate(
swiftParam: swiftParam,
return try self.translateParameter(
type: swiftParam.type,
convention: swiftParam.convention,
parameterName: parameterName,
loweredParam: loweredParam,
methodName: methodName,
parameterName: parameterName
methodName: methodName
)
}

Expand All @@ -285,13 +291,13 @@ extension FFMSwift2JavaGenerator {
}

/// Translate a Swift API parameter to the user-facing Java API parameter.
func translate(
swiftParam: SwiftParameter,
func translateParameter(
type swiftType: SwiftType,
convention: SwiftParameterConvention,
parameterName: String,
loweredParam: LoweredParameter,
methodName: String,
parameterName: String
methodName: String
) throws -> TranslatedParameter {
let swiftType = swiftParam.type

// If there is a 1:1 mapping between this Swift type and a C type, that can
// be expressed as a Java primitive type.
Expand Down Expand Up @@ -319,8 +325,8 @@ extension FFMSwift2JavaGenerator {
)

case .nominal(let swiftNominalType):
if let knownType = swiftNominalType.nominalTypeDecl.knownStandardLibraryType {
if swiftParam.convention == .inout {
if let knownType = swiftNominalType.nominalTypeDecl.knownTypeKind {
if convention == .inout {
// FIXME: Support non-trivial 'inout' for builtin types.
throw JavaTranslationError.inoutNotSupported(swiftType)
}
Expand Down Expand Up @@ -388,6 +394,26 @@ extension FFMSwift2JavaGenerator {
conversion: .call(.placeholder, function: "\(methodName).$toUpcallStub", withArena: true)
)

case .existential(let proto), .opaque(let proto):
// If the protocol has a known representative implementation, e.g. `String` for `StringProtocol`
// Translate it as the concrete type.
// NOTE: This is a temporary workaround until we add support for generics.
if
let knownProtocol = proto.asNominalTypeDeclaration?.knownTypeKind,
let concreteTy = knownTypes.representativeType(of: knownProtocol)
{
return try translateParameter(
type: concreteTy,
convention: convention,
parameterName: parameterName,
loweredParam: loweredParam,
methodName: methodName
)
}

// Otherwise, not supported yet.
throw JavaTranslationError.unhandledType(swiftType)

case .optional:
throw JavaTranslationError.unhandledType(swiftType)
}
Expand Down Expand Up @@ -422,7 +448,7 @@ extension FFMSwift2JavaGenerator {
)

case .nominal(let swiftNominalType):
if let knownType = swiftNominalType.nominalTypeDecl.knownStandardLibraryType {
if let knownType = swiftNominalType.nominalTypeDecl.knownTypeKind {
switch knownType {
case .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer:
return TranslatedResult(
Expand Down Expand Up @@ -476,7 +502,7 @@ extension FFMSwift2JavaGenerator {
// TODO: Implement.
throw JavaTranslationError.unhandledType(swiftType)

case .optional, .function:
case .optional, .function, .existential, .opaque:
throw JavaTranslationError.unhandledType(swiftType)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ extension JNISwift2JavaGenerator {
func translate(swiftType: SwiftType) -> JavaType {
switch swiftType {
case .nominal(let nominalType):
if let knownType = nominalType.nominalTypeDecl.knownStandardLibraryType {
if let knownType = nominalType.nominalTypeDecl.knownTypeKind {
guard let javaType = translate(knownType: knownType) else {
fatalError("unsupported known type: \(knownType)")
}
Expand All @@ -78,7 +78,7 @@ extension JNISwift2JavaGenerator {
case .tuple([]):
return .void

case .metatype, .optional, .tuple, .function:
case .metatype, .optional, .tuple, .function, .existential, .opaque:
fatalError("unsupported type: \(self)")
}
}
Expand All @@ -99,10 +99,8 @@ extension JNISwift2JavaGenerator {
.unsafeRawPointer, .unsafeMutableRawPointer,
.unsafePointer, .unsafeMutablePointer,
.unsafeRawBufferPointer, .unsafeMutableRawBufferPointer,
.unsafeBufferPointer, .unsafeMutableBufferPointer:
.unsafeBufferPointer, .unsafeMutableBufferPointer, .data, .dataProtocol:
nil
case .data:
fatalError("unimplemented")
}
}
}
Expand Down
12 changes: 8 additions & 4 deletions Sources/JExtractSwiftLib/Swift2JavaTranslator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ extension Swift2JavaTranslator {
// If any API uses 'Foundation.Data', import 'Data' as if it's declared in
// this module.
if let dataDecl = self.symbolTable[.data] {
if self.isUsing(dataDecl) {
let dataProtocolDecl = self.symbolTable[.dataProtocol]!
if self.isUsing(where: { $0 == dataDecl || $0 == dataProtocolDecl }) {
visitor.visit(nominalDecl: dataDecl.syntax!.asNominal!, in: nil)
}
}
Expand All @@ -117,12 +118,13 @@ extension Swift2JavaTranslator {
)
}

/// Check if any of the imported decls uses the specified nominal declaration.
func isUsing(_ decl: SwiftNominalTypeDeclaration) -> Bool {
/// Check if any of the imported decls uses a nominal declaration that satisfies
/// the given predicate.
func isUsing(where predicate: (SwiftNominalTypeDeclaration) -> Bool) -> Bool {
func check(_ type: SwiftType) -> Bool {
switch type {
case .nominal(let nominal):
return nominal.nominalTypeDecl == decl
return predicate(nominal.nominalTypeDecl)
case .optional(let ty):
return check(ty)
case .tuple(let tuple):
Expand All @@ -131,6 +133,8 @@ extension Swift2JavaTranslator {
return check(fn.resultType) || fn.parameters.contains(where: { check($0.type) })
case .metatype(let ty):
return check(ty)
case .existential(let ty), .opaque(let ty):
return check(ty)
}
}

Expand Down
Loading