diff --git a/Sources/SwiftJavaMacros/ImplementsJavaMacro.swift b/Sources/SwiftJavaMacros/ImplementsJavaMacro.swift index b3057ae4..91cd3211 100644 --- a/Sources/SwiftJavaMacros/ImplementsJavaMacro.swift +++ b/Sources/SwiftJavaMacros/ImplementsJavaMacro.swift @@ -16,10 +16,39 @@ import SwiftSyntax import SwiftSyntaxBuilder import SwiftSyntaxMacros -enum JavaImplementationMacro {} +package enum JavaImplementationMacro {} + +// JNI identifier escaping per the JNI specification: +// https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/design.html#resolving_native_method_names +extension String { + /// Returns the string with characters escaped according to JNI symbol naming rules. + /// - `_` → `_1` + /// - `.` and `/` → `_` (package/class separator) + /// - `;` → `_2` + /// - `[` → `_3` + /// - Non-ASCII → `_0XXXX` (UTF-16 hex) + var escapedJNIIdentifier: String { + self.compactMap { ch -> String in + switch ch { + case "_": return "_1" + case "/": return "_" + case ";": return "_2" + case "[": return "_3" + default: + if ch.isASCII && (ch.isLetter || ch.isNumber) { + return String(ch) + } else if let utf16 = ch.utf16.first { + return "_0\(String(format: "%04x", utf16))" + } else { + fatalError("Invalid JNI character: \(ch)") + } + } + }.joined() + } +} extension JavaImplementationMacro: PeerMacro { - static func expansion( + package static func expansion( of node: AttributeSyntax, providingPeersOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext @@ -112,7 +141,8 @@ extension JavaImplementationMacro: PeerMacro { } ?? "" let swiftName = memberFunc.name.text - let cName = "Java_" + className.replacingOccurrences(of: ".", with: "_") + "_" + swiftName + let escapedClassName = className.split(separator: ".").map { String($0).escapedJNIIdentifier }.joined(separator: "_") + let cName = "Java_" + escapedClassName + "_" + swiftName.escapedJNIIdentifier let innerBody: CodeBlockItemListSyntax let isThrowing = memberFunc.signature.effectSpecifiers?.throwsClause != nil let tryClause: String = isThrowing ? "try " : "" diff --git a/Tests/SwiftJavaMacrosTests/JavaImplementationMacroTests.swift b/Tests/SwiftJavaMacrosTests/JavaImplementationMacroTests.swift new file mode 100644 index 00000000..aaea2568 --- /dev/null +++ b/Tests/SwiftJavaMacrosTests/JavaImplementationMacroTests.swift @@ -0,0 +1,190 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 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 +// +//===----------------------------------------------------------------------===// + +import SwiftJavaMacros +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import XCTest + +class JavaImplementationMacroTests: XCTestCase { + static let javaImplementationMacros: [String: any Macro.Type] = [ + "JavaImplementation": JavaImplementationMacro.self, + "JavaMethod": JavaMethodMacro.self, + ] + + func testJNIIdentifierEscaping() throws { + assertMacroExpansion( + """ + @JavaImplementation("org.swift.example.Hello_World") + extension HelloWorld { + @JavaMethod + func test_method() -> Int32 { + return 42 + } + } + """, + expandedSource: """ + + extension HelloWorld { + func test_method() -> Int32 { + return 42 + } + } + + @_cdecl("Java_org_swift_example_Hello_1World_test_1method") + func __macro_local_11test_methodfMu_(environment: UnsafeMutablePointer!, thisObj: jobject) -> Int32.JNIType { + let obj = HelloWorld(javaThis: thisObj, environment: environment!) + return obj.test_method() + .getJNIValue(in: environment) + } + """, + macros: Self.javaImplementationMacros + ) + } + + func testJNIIdentifierEscapingWithDots() throws { + assertMacroExpansion( + """ + @JavaImplementation("com.example.test.MyClass") + extension MyClass { + @JavaMethod + func simpleMethod() -> Int32 { + return 1 + } + } + """, + expandedSource: """ + + extension MyClass { + func simpleMethod() -> Int32 { + return 1 + } + } + + @_cdecl("Java_com_example_test_MyClass_simpleMethod") + func __macro_local_12simpleMethodfMu_(environment: UnsafeMutablePointer!, thisObj: jobject) -> Int32.JNIType { + let obj = MyClass(javaThis: thisObj, environment: environment!) + return obj.simpleMethod() + .getJNIValue(in: environment) + } + """, + macros: Self.javaImplementationMacros + ) + } + + func testJNIIdentifierEscapingStaticMethod() throws { + assertMacroExpansion( + """ + @JavaImplementation("org.example.Utils") + extension Utils { + @JavaMethod + static func static_helper(environment: JNIEnvironment) -> String { + return "hello" + } + } + """, + expandedSource: """ + + extension Utils { + static func static_helper(environment: JNIEnvironment) -> String { + return "hello" + } + } + + @_cdecl("Java_org_example_Utils_static_1helper") + func __macro_local_13static_helperfMu_(environment: UnsafeMutablePointer!, thisClass: jclass) -> String.JNIType { + return Utils.static_helper(environment: environment) + .getJNIValue(in: environment) + } + """, + macros: Self.javaImplementationMacros + ) + } + + func testJNIIdentifierEscapingMultipleMethods() throws { + assertMacroExpansion( + """ + @JavaImplementation("test.Class_With_Underscores") + extension ClassWithUnderscores { + @JavaMethod + func method_one() -> Int32 { + return 1 + } + + @JavaMethod + func method_two() -> Int32 { + return 2 + } + } + """, + expandedSource: """ + + extension ClassWithUnderscores { + func method_one() -> Int32 { + return 1 + } + func method_two() -> Int32 { + return 2 + } + } + + @_cdecl("Java_test_Class_1With_1Underscores_method_1one") + func __macro_local_10method_onefMu_(environment: UnsafeMutablePointer!, thisObj: jobject) -> Int32.JNIType { + let obj = ClassWithUnderscores(javaThis: thisObj, environment: environment!) + return obj.method_one() + .getJNIValue(in: environment) + } + + @_cdecl("Java_test_Class_1With_1Underscores_method_1two") + func __macro_local_10method_twofMu_(environment: UnsafeMutablePointer!, thisObj: jobject) -> Int32.JNIType { + let obj = ClassWithUnderscores(javaThis: thisObj, environment: environment!) + return obj.method_two() + .getJNIValue(in: environment) + } + """, + macros: Self.javaImplementationMacros + ) + } + + func testJNIIdentifierEscapingVoidReturn() throws { + assertMacroExpansion( + """ + @JavaImplementation("org.example.Processor") + extension Processor { + @JavaMethod + func process_data() { + // do nothing + } + } + """, + expandedSource: """ + + extension Processor { + func process_data() { + // do nothing + } + } + + @_cdecl("Java_org_example_Processor_process_1data") + func __macro_local_12process_datafMu_(environment: UnsafeMutablePointer!, thisObj: jobject) { + let obj = Processor(javaThis: thisObj, environment: environment!) + return obj.process_data() + } + """, + macros: Self.javaImplementationMacros + ) + } +}