Skip to content

Commit 763ff47

Browse files
authored
wrap-java: Basic classfile parser to get CLASS retained annotations (#589)
* move InputStream to SwiftJava for now since we need it in order to getResourceAsStream * wrap-java: Basic classfile parser to get CLASS retained annotations Implement basic class file parsing, only enough to get annotations Thankfully the classfile format is well known and documented. We avoid the usual "Java way" of doing this work which would be to pull in the asm.jar dependency, but instead choose to do the parsing ourselfes. The format is well specified and stable, so we can be pretty confident in it. I based it on the JDK22 revision of the spec: https://docs.oracle.com/javase/specs/jvms/se22/html/jvms-4.html We only forus on the "RuntimeInvisibleAannotations" attribute because runtime visible ones we're able to get from plain reflection calls (which the Deprecated test cases showcase already in previous PR): https://docs.oracle.com/javase/specs/jvms/se22/html/jvms-4.html#jvms-4.7.17 This is necessary to support Android's RequiredApi annotations which are CLASS retained (or how kotlin confusingly calls it BINARY which mislead me and I thouhgt they're retained but yeah makes sense). This way we're able to emit Android availability annotations for the whole Android SDK. * format
1 parent 9c5e8e7 commit 763ff47

14 files changed

Lines changed: 1120 additions & 147 deletions

Sources/JavaStdlib/JavaIO/swift-java.config

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
{
22
"classes" : {
3-
"java.io.InputStream" : "InputStream",
43
"java.io.BufferedInputStream" : "BufferedInputStream",
54
"java.io.InputStreamReader" : "InputStreamReader",
65

File renamed without changes.

Sources/SwiftJava/generated/JavaClass.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,9 @@ open class JavaClass<T: AnyJavaObject>: JavaObject {
137137

138138
@JavaMethod
139139
open func getNestMembers() -> [JavaClass<JavaObject>?]
140+
141+
@JavaMethod
142+
open func getResourceAsStream(_ arg0: String) -> InputStream!
140143
}
141144
extension JavaClass {
142145
@JavaStaticMethod

Sources/SwiftJava/generated/JavaClassLoader.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ open class JavaClassLoader: JavaObject {
5959

6060
@JavaMethod
6161
open func clearAssertionStatus()
62+
63+
@JavaMethod
64+
open func getResourceAsStream(_ arg0: String) -> InputStream!
65+
66+
@JavaMethod
67+
open func getResource(_ arg0: String) -> JavaObject!
6268
}
6369
extension JavaClass<JavaClassLoader> {
6470
@JavaStaticMethod

Sources/SwiftJava/swift-java.config

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@
4747
"java.io.OutputStream": "OutputStream",
4848
"java.io.Writer": "Writer",
4949
"java.io.PrintWriter": "PrintWriter",
50-
"java.io.StringWriter": "StringWriter"
50+
"java.io.StringWriter": "StringWriter",
51+
"java.io.InputStream": "InputStream"
5152

5253
}
5354
}
Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2026 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift.org project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
/// Minimal parser for JVM `.class` files (JVM Spec §4).
16+
///
17+
/// We only handle the bare minimum subset that swift-java is interested in, and skip everything else.
18+
struct JavaClassFileReader {
19+
private var bytes: [UInt8]
20+
private var offset: Int = 0
21+
22+
/// Utf8 constant pool entries, indexed by CP index (1-based).
23+
private var utf8Constants: [Int: String] = [:]
24+
/// Integer constant pool entries, indexed by CP index (1-based).
25+
private var integerConstants: [Int: Int32] = [:]
26+
27+
/// The parsed RuntimeInvisibleAnnotations from the class file.
28+
private(set) var runtimeInvisibleAnnotations = JavaRuntimeInvisibleAnnotations()
29+
}
30+
31+
extension JavaClassFileReader {
32+
33+
/// Parse a `.class` file and return the invisible annotations found.
34+
static func parseRuntimeInvisibleAnnotations(_ bytes: [UInt8]) -> JavaRuntimeInvisibleAnnotations {
35+
var reader = JavaClassFileReader(bytes: bytes)
36+
reader.parseClassFile()
37+
return reader.runtimeInvisibleAnnotations
38+
}
39+
}
40+
41+
// ===== ----------------------------------------------------------------------
42+
// MARK: Low-level reading/skipping
43+
44+
extension JavaClassFileReader {
45+
private mutating func readU1() -> UInt8 {
46+
let value = bytes[offset]
47+
offset += 1
48+
return value
49+
}
50+
51+
private mutating func readU2() -> UInt16 {
52+
let hi = UInt16(readU1())
53+
let lo = UInt16(readU1())
54+
return (hi << 8) | lo
55+
}
56+
57+
private mutating func readU4() -> UInt32 {
58+
let hi = UInt32(readU2())
59+
let lo = UInt32(readU2())
60+
return (hi << 16) | lo
61+
}
62+
63+
private mutating func skip(_ count: Int) {
64+
offset += count
65+
}
66+
}
67+
68+
// ===== ----------------------------------------------------------------------
69+
// MARK: Parsing entities
70+
71+
extension JavaClassFileReader {
72+
73+
/// Parse the class file version numbers (§4.1).
74+
/// Returns `(major, minor)` version.
75+
private mutating func parseClassFileVersion() -> (major: UInt16, minor: UInt16) {
76+
let minor = readU2()
77+
let major = readU2()
78+
return (major, minor)
79+
}
80+
81+
private mutating func parseClassFile() {
82+
// Magic number
83+
let magic = readU4()
84+
guard magic == 0xCAFE_BABE else { return }
85+
86+
// Version
87+
_ = parseClassFileVersion()
88+
89+
// Constant pool
90+
let cpCount = Int(readU2())
91+
parseConstantPool(count: cpCount)
92+
93+
// Access flags, this_class, super_class
94+
_ = readU2() // access_flags
95+
_ = readU2() // this_class
96+
_ = readU2() // super_class
97+
98+
// Interfaces
99+
let interfacesCount = Int(readU2())
100+
skip(interfacesCount * 2)
101+
102+
// Fields
103+
let fieldsCount = Int(readU2())
104+
for _ in 0..<fieldsCount {
105+
let (name, annotations) = parseMemberInfo()
106+
if !annotations.isEmpty {
107+
runtimeInvisibleAnnotations.fieldAnnotations[name] = annotations
108+
}
109+
}
110+
111+
// Methods
112+
let methodsCount = Int(readU2())
113+
for _ in 0..<methodsCount {
114+
let (nameAndDescriptor, annotations) = parseMemberInfo(includeDescriptor: true)
115+
if !annotations.isEmpty {
116+
runtimeInvisibleAnnotations.methodAnnotations[nameAndDescriptor] = annotations
117+
}
118+
}
119+
120+
// Class-level attributes
121+
runtimeInvisibleAnnotations.classAnnotations = parseAttributes()
122+
}
123+
124+
// MARK: - Constant pool
125+
126+
/// JVM class file constant pool tags (JVM Spec §4.4).
127+
enum JavaConstantPoolTag: UInt8 {
128+
case utf8 = 1
129+
case integer = 3
130+
case float = 4
131+
case long = 5
132+
case double = 6
133+
case `class` = 7
134+
case string = 8
135+
case fieldref = 9
136+
case methodref = 10
137+
case interfaceMethodref = 11
138+
case nameAndType = 12
139+
case methodHandle = 15
140+
case methodType = 16
141+
case dynamic = 17
142+
case invokeDynamic = 18
143+
case module = 19
144+
case package = 20
145+
}
146+
147+
private mutating func parseConstantPool(count: Int) {
148+
var index = 1
149+
while index < count {
150+
guard let tag = JavaConstantPoolTag(rawValue: readU1()) else {
151+
// Unknown tag — give up parsing
152+
return
153+
}
154+
switch tag {
155+
case .utf8:
156+
let length = Int(readU2())
157+
let slice = Array(bytes[offset..<offset + length])
158+
utf8Constants[index] = String(decoding: slice, as: UTF8.self)
159+
skip(length)
160+
161+
case .integer:
162+
let rawBits = readU4()
163+
integerConstants[index] = Int32(bitPattern: rawBits)
164+
165+
case .float:
166+
skip(4)
167+
168+
case .long: // takes 2 slots
169+
skip(8)
170+
index += 1
171+
172+
case .double: // takes 2 slots
173+
skip(8)
174+
index += 1
175+
176+
case .class:
177+
skip(2)
178+
179+
case .string:
180+
skip(2)
181+
182+
case .fieldref, .methodref, .interfaceMethodref:
183+
skip(4)
184+
185+
case .nameAndType:
186+
skip(4)
187+
188+
case .methodHandle:
189+
skip(3)
190+
191+
case .methodType:
192+
skip(2)
193+
194+
case .dynamic, .invokeDynamic:
195+
skip(4)
196+
197+
case .module, .package:
198+
skip(2)
199+
}
200+
index += 1
201+
}
202+
}
203+
204+
/// Parse a field_info or method_info structure. Returns the member's
205+
/// identifying key (field name, or "name:descriptor" for methods) and
206+
/// any `RuntimeInvisibleAnnotations` found on it.
207+
private mutating func parseMemberInfo(
208+
includeDescriptor: Bool = false
209+
) -> (String, [JavaRuntimeInvisibleAnnotation]) {
210+
_ = readU2() // access_flags
211+
let nameIndex = Int(readU2())
212+
let descriptorIndex = Int(readU2())
213+
let name = utf8Constants[nameIndex] ?? ""
214+
let key: String
215+
if includeDescriptor {
216+
let descriptor = utf8Constants[descriptorIndex] ?? ""
217+
key = "\(name):\(descriptor)"
218+
} else {
219+
key = name
220+
}
221+
let annotations = parseAttributes()
222+
return (key, annotations)
223+
}
224+
225+
/// Parse an attributes table and return any annotations found in
226+
/// `RuntimeInvisibleAnnotations` attributes.
227+
private mutating func parseAttributes() -> [JavaRuntimeInvisibleAnnotation] {
228+
let attributesCount = Int(readU2())
229+
var collected: [JavaRuntimeInvisibleAnnotation] = []
230+
231+
for _ in 0..<attributesCount {
232+
let attrNameIndex = Int(readU2())
233+
let attrLength = Int(readU4())
234+
let attrName = utf8Constants[attrNameIndex] ?? ""
235+
236+
if attrName == "RuntimeInvisibleAnnotations" {
237+
collected.append(contentsOf: parseAnnotationsAttribute())
238+
} else {
239+
// Skip this attribute's content
240+
skip(attrLength)
241+
}
242+
}
243+
244+
return collected
245+
}
246+
247+
/// Parse the body of a `RuntimeInvisibleAnnotations` attribute (§4.7.17).
248+
private mutating func parseAnnotationsAttribute() -> [JavaRuntimeInvisibleAnnotation] {
249+
let numAnnotations = Int(readU2())
250+
var annotations: [JavaRuntimeInvisibleAnnotation] = []
251+
for _ in 0..<numAnnotations {
252+
annotations.append(parseAnnotation())
253+
}
254+
return annotations
255+
}
256+
257+
/// Parse a single `annotation` structure.
258+
private mutating func parseAnnotation() -> JavaRuntimeInvisibleAnnotation {
259+
let typeIndex = Int(readU2())
260+
let typeDescriptor = utf8Constants[typeIndex] ?? ""
261+
let numPairs = Int(readU2())
262+
var elements: [String: Int32] = [:]
263+
264+
for _ in 0..<numPairs {
265+
let nameIndex = Int(readU2())
266+
let elementName = utf8Constants[nameIndex] ?? ""
267+
let value = parseElementValue()
268+
if let intValue = value {
269+
elements[elementName] = intValue
270+
}
271+
}
272+
273+
return JavaRuntimeInvisibleAnnotation(typeDescriptor: typeDescriptor, elements: elements)
274+
}
275+
276+
/// Annotation element_value tags (JVM Spec §4.7.16.1).
277+
enum ElementValueTag: UInt8 {
278+
case byte = 0x42 // B
279+
case char = 0x43 // C
280+
case double = 0x44 // D
281+
case float = 0x46 // F
282+
case int = 0x49 // I
283+
case long = 0x4A // J
284+
case short = 0x53 // S
285+
case boolean = 0x5A // Z
286+
case string = 0x73 // s
287+
case enumConstant = 0x65 // e
288+
case classInfo = 0x63 // c
289+
case annotation = 0x40 // @
290+
case array = 0x5B // [
291+
}
292+
293+
/// Parse an `element_value` structure (§4.7.16.1).
294+
/// Returns the integer value if the tag is `I`, otherwise returns nil
295+
/// (and skips the appropriate number of bytes).
296+
private mutating func parseElementValue() -> Int32? {
297+
guard let tag = ElementValueTag(rawValue: readU1()) else {
298+
return nil
299+
}
300+
switch tag {
301+
case .byte, .char, .double, .float,
302+
.long, .short, .boolean, .string:
303+
_ = readU2() // const_value_index
304+
return nil
305+
306+
case .int:
307+
let constIndex = Int(readU2())
308+
return integerConstants[constIndex]
309+
310+
case .enumConstant:
311+
_ = readU2() // type_name_index
312+
_ = readU2() // const_name_index
313+
return nil
314+
315+
case .classInfo:
316+
_ = readU2() // class_info_index
317+
return nil
318+
319+
case .annotation:
320+
_ = parseAnnotation()
321+
return nil
322+
323+
case .array:
324+
let numValues = Int(readU2())
325+
for _ in 0..<numValues {
326+
_ = parseElementValue()
327+
}
328+
return nil
329+
}
330+
}
331+
}

0 commit comments

Comments
 (0)