Skip to content
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
30 changes: 23 additions & 7 deletions Sources/SkipSyntax/Kotlin/KotlinBridgeTransformer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -223,12 +223,11 @@ struct CDeclFunction {
} else {
var file = translator.syntaxTree.source.file
file.extension = ""
// The JNI symbol side (`cdeclTypeName`) is escaped via `cdeclEscaped` at
// the return below. The Swift-identifier side (`typeName`) must be escaped
// here too, since a file name like `Foo+Bar.swift` yields `Foo+BarKt`, and
// the raw `+` produces an invalid Swift function name (issue #63).
// The JNI symbol (`cdeclTypeName`) uses `cdeclEscaped`; the Swift `@_cdecl`
// function name needs Swift-identifier escaping instead — e.g. `Foo+Bar.swift`
// must not leak `+` or `.` into the generated func name (issue #63).
cdeclTypeName = file.name + "Kt"
typeName = cdeclTypeName.cdeclEscaped
typeName = cdeclTypeName.swiftIdentifierEscaped
}
return (cdeclPrefix + cdeclTypeName.cdeclEscaped + "_" + name.cdeclEscaped, typeName + "_" + name)
}
Expand Down Expand Up @@ -286,13 +285,14 @@ extension String {
return Self.backtickEscapingIdentifiers.contains(self) ? "`\(self)`" : self
}

/// Escape special characters for use in a `@_cdecl` declaration.
/// Escape special characters for use in the JNI symbol of a `@_cdecl` declaration.
///
/// As documented at https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/design.html#resolving_native_method_names
/// - `_` → `_1`
/// - `.` and `/` → `_` (package/class separator)
/// - `/` → `_` (package/class separator)
/// - `;` → `_2`
/// - `[` → `_3`
/// - `.` → `.` (preserved; callers replace the separator as needed)
/// - Non-ASCII → `_0XXXX` (UTF-16 hex)
var cdeclEscaped: String {
self.compactMap { ch -> String in
Expand All @@ -314,6 +314,22 @@ extension String {
}.joined()
}

/// Escape characters that are invalid in a Swift identifier, leaving ASCII
/// alphanumerics and `_` untouched. Unlike `cdeclEscaped` (JNI symbol), this
/// keeps `_` as-is and escapes `.`, since a `.` is invalid in a Swift `@_cdecl`
/// function name (issue #63).
var swiftIdentifierEscaped: String {
self.compactMap { ch -> String in
if ch == "_" || (ch.isASCII && (ch.isLetter || ch.isNumber)) {
return String(ch)
} else {
// Escape every UTF-16 code unit, so characters outside the BMP
// (encoded as a surrogate pair) are fully represented, not truncated.
return ch.utf16.map { "_0\(String(format: "%04x", $0))" }.joined()
}
}.joined()
}

/// Return this property name as the equivalent Java getter.
var getterName: String {
guard !isEmpty else {
Expand Down
46 changes: 40 additions & 6 deletions Tests/SkipSyntaxTests/FileNameEscapeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,28 @@
// Licensed under the GNU Affero General Public License v3.0
// SPDX-License-Identifier: AGPL-3.0-only

import SkipSyntax
@testable import SkipSyntax
import XCTest

final class FileNameEscapeTests: XCTestCase {

func testSwiftIdentifierEscaping() {
// Valid Swift identifier characters (ASCII alphanumerics and `_`) pass through.
XCTAssertEqual("Model_ExtensionsKt".swiftIdentifierEscaped, "Model_ExtensionsKt")
// Invalid characters are escaped as `_0XXXX` per UTF-16 code unit.
XCTAssertEqual("Model+BridgingKt".swiftIdentifierEscaped, "Model_0002bBridgingKt")
XCTAssertEqual("Model.generatedKt".swiftIdentifierEscaped, "Model_0002egeneratedKt")
// A character outside the BMP is a UTF-16 surrogate pair; both units escape.
XCTAssertEqual("A🎉Kt".swiftIdentifierEscaped, "A_0d83c_0df89Kt")
}

private var transformers: [KotlinTransformer] {
return builtinKotlinTransformers() + [KotlinBridgeTransformer()]
}

// A source file whose name contains characters that are invalid in a Swift
// identifier (e.g. the "+" in `Model+Bridging.swift`) is used to derive both
// the JNI symbol (already escaped) and the generated `@_cdecl` Swift function
// name. The Swift-identifier side must be escaped too, or the generated
// bridging code fails to compile (issue #63).
// A file name character that is invalid in a Swift identifier (the "+") must be
// escaped in the generated `@_cdecl` function name, or the bridge fails to
// compile (issue #63). Both the JNI symbol and the Swift name escape it here.
func testBridgeFunctionNameEscapesInvalidFileNameCharacters() async throws {
try await check(swiftBridge: """
public var i = 1
Expand All @@ -37,4 +46,29 @@ final class FileNameEscapeTests: XCTestCase {
}
""", transformers: transformers)
}

// An underscore is valid in a Swift identifier, so the Swift `@_cdecl` function
// name must keep it as-is; only the JNI symbol mangles it to `_1` (issue #63).
func testBridgeFunctionNamePreservesUnderscoreInFileName() async throws {
try await check(swiftBridge: """
public var i = 1
""", swiftBridgeFileName: "Model_Extensions.swift", kotlin: """
var i: Int
get() = Swift_i()
set(newValue) {
Swift_i_set(newValue)
}
private external fun Swift_i(): Int
private external fun Swift_i_set(value: Int)
""", swiftBridgeSupport: """
@_cdecl("Java_Model_1ExtensionsKt_Swift_1i")
public func Model_ExtensionsKt_Swift_i(_ Java_env: JNIEnvPointer, _ Java_target: JavaObjectPointer) -> Int32 {
return Int32(i)
}
@_cdecl("Java_Model_1ExtensionsKt_Swift_1i_1set")
public func Model_ExtensionsKt_Swift_i_set(_ Java_env: JNIEnvPointer, _ Java_target: JavaObjectPointer, _ value: Int32) {
i = Int(value)
}
""", transformers: transformers)
}
}
Loading