Skip to content

Commit

Permalink
jextract: Indirect returns and more value type support (#211)
Browse files Browse the repository at this point in the history
* ignore .index-build directory

* Implement SwiftValue cleanup

* SourceGen: Handle init() of value types with indirect return

* Followups for handling value inits, various helper funcs

* SwiftArena is now a segment allocator

* Add $destroyed flag to wrapper types to prevent use-after-free bugs

* Use varHandle instead of offset for reading data off VWT

* testing: skip empty lines in output matching

* gitignore vscode build outputs

* test fixups

* more tests for protecting access to destroyed objects

* remove commented out code

* fix formatting
  • Loading branch information
ktoso authored Jan 28, 2025
1 parent cb4fa3d commit 831397d
Show file tree
Hide file tree
Showing 31 changed files with 580 additions and 185 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ DerivedData/
*.class
bin/
BuildLogic/out/
.index-build
.build-vscode

# Ignore gradle build artifacts
.gradle
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@

public struct MySwiftStruct {

public var number: Int
private var cap: Int
private var len: Int

public init(number: Int) {
self.number = number
public init(cap: Int, len: Int) {
self.cap = cap
self.len = len
}

public func voidMethod() {
Expand All @@ -38,6 +40,20 @@ public struct MySwiftStruct {
return 12
}

public func getCapacity() -> Int {
self.cap
}

public func getLength() -> Int {
self.len
}

public mutating func increaseCap(by value: Int) -> Int {
precondition(value > 0)
self.cap += value
return self.cap
}

public func makeRandomIntMethod() -> Int {
return Int.random(in: 1..<256)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,13 @@
import org.swift.swiftkit.SwiftKit;
import org.swift.swiftkit.SwiftValueWitnessTable;

import java.util.Arrays;

public class HelloJava2Swift {

public static void main(String[] args) {
boolean traceDowncalls = Boolean.getBoolean("jextract.trace.downcalls");
System.out.println("Property: jextract.trace.downcalls = " + traceDowncalls);

System.out.print("Property: java.library.path = " +SwiftKit.getJavaLibraryPath());
System.out.print("Property: java.library.path = " + SwiftKit.getJavaLibraryPath());

examples();
}
Expand All @@ -44,23 +42,23 @@ static void examples() {

// Example of using an arena; MyClass.deinit is run at end of scope
try (var arena = SwiftArena.ofConfined()) {
MySwiftClass obj = new MySwiftClass(arena, 2222, 7777);

// just checking retains/releases work
SwiftKit.retain(obj.$memorySegment());
SwiftKit.release(obj.$memorySegment());
MySwiftClass obj = new MySwiftClass(arena, 2222, 7777);

obj.voidMethod();
obj.takeIntMethod(42);
// just checking retains/releases work
SwiftKit.retain(obj.$memorySegment());
SwiftKit.release(obj.$memorySegment());

MySwiftStruct swiftValue = new MySwiftStruct(12);
obj.voidMethod();
obj.takeIntMethod(42);

MySwiftStruct swiftValue = new MySwiftStruct(arena, 2222, 1111);
}

System.out.println("DONE.");
}

public static native long jniWriteString(String str);

public static native long jniGetInt();

}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ public class MySwiftClassTest {
void checkPaths(Throwable throwable) {
var paths = SwiftKit.getJavaLibraryPath().split(":");
for (var path : paths) {
System.out.println("CHECKING PATH: " + path);
Stream.of(new File(path).listFiles())
.filter(file -> !file.isDirectory())
.forEach((file) -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,24 @@
//
//===----------------------------------------------------------------------===//

package com.example.swift;
package org.swift.swiftkit;

import org.junit.jupiter.api.Disabled;
import com.example.swift.MySwiftStruct;
import org.junit.jupiter.api.Test;
import org.swift.swiftkit.SwiftArena;
import org.swift.swiftkit.SwiftKit;

import java.io.File;
import java.util.stream.Stream;

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

public class MySwiftStructTest {

@Test
void test_MySwiftClass_voidMethod() {
void create_struct() {
try (var arena = SwiftArena.ofConfined()) {
MySwiftStruct o = new MySwiftStruct(12);
// o.voidMethod();
long cap = 12;
long len = 34;
var struct = new MySwiftStruct(arena, cap, len);

assertEquals(cap, struct.getCapacity());
assertEquals(len, struct.getLength());
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package org.swift.swiftkit;

import com.example.swift.MySwiftClass;
import com.example.swift.MySwiftStruct;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledIf;
Expand Down Expand Up @@ -47,8 +48,44 @@ public void arena_releaseClassOnClose_class_ok() {
release(obj.$memorySegment());
assertEquals(1, retainCount(obj.$memorySegment()));
}
}

// FIXME: The destroy witness table call hangs on x86_64 platforms during the destroy witness table call
// See: https://github.com/swiftlang/swift-java/issues/97
@Test
public void arena_markAsDestroyed_preventUseAfterFree_class() {
MySwiftClass unsafelyEscapedOutsideArenaScope = null;

try (var arena = SwiftArena.ofConfined()) {
var obj = new MySwiftClass(arena,1, 2);
unsafelyEscapedOutsideArenaScope = obj;
}

try {
unsafelyEscapedOutsideArenaScope.echoIntMethod(1);
fail("Expected exception to be thrown! Object was suposed to be dead.");
} catch (IllegalStateException ex) {
return;
}
}

// FIXME: The destroy witness table call hangs on x86_64 platforms during the destroy witness table call
// See: https://github.com/swiftlang/swift-java/issues/97
@Test
public void arena_markAsDestroyed_preventUseAfterFree_struct() {
MySwiftStruct unsafelyEscapedOutsideArenaScope = null;

// TODO: should we zero out the $memorySegment perhaps?
try (var arena = SwiftArena.ofConfined()) {
var s = new MySwiftStruct(arena,1, 2);
unsafelyEscapedOutsideArenaScope = s;
}

try {
unsafelyEscapedOutsideArenaScope.echoIntMethod(1);
fail("Expected exception to be thrown! Object was suposed to be dead.");
} catch (IllegalStateException ex) {
return;
}
}

@Test
Expand Down
26 changes: 16 additions & 10 deletions Sources/JExtractSwift/Convenience/SwiftSyntax+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,22 @@ extension ImplicitlyUnwrappedOptionalTypeSyntax {
}
}

extension TypeSyntax {
fileprivate var isActorSystem: Bool {
self.trimmedDescription == "ActorSystem"
extension SyntaxProtocol {

var asNominalTypeKind: NominalTypeKind {
if isClass {
.class
} else if isActor {
.actor
} else if isStruct {
.struct
} else if isEnum {
.enum
} else {
fatalError("Unknown nominal kind: \(self)")
}
}
}

extension DeclSyntaxProtocol {
var isClass: Bool {
return self.is(ClassDeclSyntax.self)
}
Expand All @@ -79,11 +88,8 @@ extension DeclSyntaxProtocol {
extension DeclModifierSyntax {
var isAccessControl: Bool {
switch self.name.tokenKind {
case .keyword(.private): fallthrough
case .keyword(.fileprivate): fallthrough
case .keyword(.internal): fallthrough
case .keyword(.package): fallthrough
case .keyword(.public):
case .keyword(.private), .keyword(.fileprivate), .keyword(.internal), .keyword(.package),
.keyword(.public):
return true
default:
return false
Expand Down
62 changes: 50 additions & 12 deletions Sources/JExtractSwift/ImportedDecls.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,19 @@ public struct ImportedNominalType: ImportedDecl {
TranslatedType(
cCompatibleConvention: .direct,
originalSwiftType: "\(raw: swiftTypeName)",
originalSwiftTypeKind: self.kind,
cCompatibleSwiftType: "UnsafeRawPointer",
cCompatibleJavaMemoryLayout: .heapObject,
javaType: javaType
)
}

public var isReferenceType: Bool {
switch self.kind {
case .class, .actor:
return true
case .enum, .struct:
return false
true
default:
false
}
}

Expand All @@ -68,11 +69,40 @@ public struct ImportedNominalType: ImportedDecl {
}
}

// TODO: replace this with `SwiftNominalTypeDeclaration.Kind`
public enum NominalTypeKind {
case `actor`
case `class`
case `enum`
case `struct`
case `void` // TODO: NOT NOMINAL, BUT...
case function // TODO: NOT NOMINAL, BUT...
case primitive // TODO: NOT NOMINAL, BUT...

var isReferenceType: Bool {
switch self {
case .actor, .class: true
case .enum, .struct: false
case .void, .function, .primitive: false
}
}

var isValueType: Bool {
switch self {
case .actor, .class: false
case .enum, .struct: true
case .void, .function, .primitive: false
}
}

var isVoid: Bool {
switch self {
case .actor, .class: false
case .enum, .struct: false
case .void: true
case .function, .primitive: false
}
}
}

public struct ImportedParam {
Expand All @@ -99,7 +129,7 @@ public struct ImportedParam {
var effectiveName: String? {
firstName ?? secondName
}

var effectiveValueName: String {
secondName ?? firstName ?? "_"
}
Expand Down Expand Up @@ -199,13 +229,13 @@ public struct ImportedFunc: ImportedDecl, CustomStringConvertible {
case nil, .wrapper:
break

case .pointer:
case .pointer where !isInit:
let selfParam: FunctionParameterSyntax = "self$: $swift_pointer"
params.append(
ImportedParam(syntax: selfParam, type: parent)
)

case .memorySegment:
case .memorySegment where !isInit:
let selfParam: FunctionParameterSyntax = "self$: $java_lang_foreign_MemorySegment"
var parentForSelf = parent
parentForSelf.javaType = .javaForeignMemorySegment
Expand All @@ -215,6 +245,9 @@ public struct ImportedFunc: ImportedDecl, CustomStringConvertible {

case .swiftThunkSelf:
break

default:
break
}

// TODO: add any metadata for generics and other things we may need to add here
Expand All @@ -233,6 +266,11 @@ public struct ImportedFunc: ImportedDecl, CustomStringConvertible {

public var isInit: Bool = false

public var isIndirectReturn: Bool {
returnType.isValueType ||
(isInit && (parent?.isValueType ?? false))
}

public init(
module: String,
decl: any DeclSyntaxProtocol,
Expand Down Expand Up @@ -269,9 +307,9 @@ extension ImportedFunc: Hashable {
self.swiftDecl.id.hash(into: &hasher)
}

public static func ==(lhs: ImportedFunc, rhs: ImportedFunc) -> Swift.Bool {
lhs.parent?.originalSwiftType.id == rhs.parent?.originalSwiftType.id &&
lhs.swiftDecl.id == rhs.swiftDecl.id
public static func == (lhs: ImportedFunc, rhs: ImportedFunc) -> Swift.Bool {
lhs.parent?.originalSwiftType.id == rhs.parent?.originalSwiftType.id
&& lhs.swiftDecl.id == rhs.swiftDecl.id
}
}

Expand Down Expand Up @@ -394,8 +432,8 @@ public struct ImportedVariable: ImportedDecl, CustomStringConvertible {
)

case nil,
.wrapper,
.swiftThunkSelf:
.wrapper,
.swiftThunkSelf:
break
}
}
Expand Down
Loading

0 comments on commit 831397d

Please sign in to comment.