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
36 changes: 33 additions & 3 deletions Sources/SwiftJavaMacros/ImplementsJavaMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 " : ""
Expand Down
190 changes: 190 additions & 0 deletions Tests/SwiftJavaMacrosTests/JavaImplementationMacroTests.swift
Original file line number Diff line number Diff line change
@@ -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<JNIEnv?>!, 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<JNIEnv?>!, 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<JNIEnv?>!, 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<JNIEnv?>!, 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<JNIEnv?>!, 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<JNIEnv?>!, thisObj: jobject) {
let obj = Processor(javaThis: thisObj, environment: environment!)
return obj.process_data()
}
""",
macros: Self.javaImplementationMacros
)
}
}
Loading