Skip to content

[JExtract] Import any C compatible closures #253

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 10, 2025

Conversation

rintaro
Copy link
Member

@rintaro rintaro commented Jun 6, 2025

Generalized closure parameter import, part 1.

Start with C-compatible closures which don't need any conversions.

For this Swift API:

public func callMeMore(
  callback: (UnsafeRawPointer, Float) -> Int,
  fn: () -> ()
)

The binding descriptor class has additional nested classes that describes the lowered functional interface and the method handle for each closure parameter.

/**
 * {@snippet lang=c :
 * void swiftjava___FakeModule_callMeMore_callback_fn(ptrdiff_t (*callback)(const void *, float), void (*fn)(void))
 * }
 */
private static class swiftjava___FakeModule_callMeMore_callback_fn {
  private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid(
    /* callback: */SwiftValueLayout.SWIFT_POINTER,
    /* fn: */SwiftValueLayout.SWIFT_POINTER
  );
  private static final MemorySegment ADDR =
    __FakeModule.findOrThrow("swiftjava___FakeModule_callMeMore_callback_fn");
  private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC);
  public static void call(java.lang.foreign.MemorySegment callback, java.lang.foreign.MemorySegment fn) {
    try {
      if (SwiftKit.TRACE_DOWNCALLS) {
        SwiftKit.traceDowncall(callback, fn);
      }
      HANDLE.invokeExact(callback, fn);
    } catch (Throwable ex$) {
      throw new AssertionError("should not reach here", ex$);
    }
  }
  /**
   * {snippet lang=c :
   * ptrdiff_t (*)(const void *, float)
   * }
   */
  private static class $callback {
    public interface Function {
      long apply(java.lang.foreign.MemorySegment _0, float _1);
    }
    private static final FunctionDescriptor DESC = FunctionDescriptor.of(
      /* -> */SwiftValueLayout.SWIFT_INT,
      /* _0: */SwiftValueLayout.SWIFT_POINTER,
      /* _1: */SwiftValueLayout.SWIFT_FLOAT
    );
    private static final MethodHandle HANDLE = SwiftKit.upcallHandle(Function.class, "apply", DESC);
    private static MemorySegment toUpcallStub(Function fi, Arena arena) {
      return Linker.nativeLinker().upcallStub(HANDLE.bindTo(fi), DESC, arena);
    }
  }
  /**
   * {snippet lang=c :
   * void (*)(void)
   * }
   */
  private static class $fn {
    public interface Function {
      void apply();
    }
    private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid();
    private static final MethodHandle HANDLE = SwiftKit.upcallHandle(Function.class, "apply", DESC);
    private static MemorySegment toUpcallStub(Function fi, Arena arena) {
      return Linker.nativeLinker().upcallStub(HANDLE.bindTo(fi), DESC, arena);
    }
  }
}

Additional nested class containing the "wrapper" functional interfaces:

public static class callMeMore {
  @FunctionalInterface
  public interface callback extends swiftjava___FakeModule_callMeMore_callback_fn.$callback.Function {}
  private static MemorySegment $toUpcallStub(callback fi, Arena arena) {
    return swiftjava___FakeModule_callMeMore_callback_fn.$callback.toUpcallStub(fi, arena);
  }
  @FunctionalInterface
  public interface fn extends swiftjava___FakeModule_callMeMore_callback_fn.$fn.Function {}
  private static MemorySegment $toUpcallStub(fn fi, Arena arena) {
    return swiftjava___FakeModule_callMeMore_callback_fn.$fn.toUpcallStub(fi, arena);
  }
}

The wrapper method converts the "wrapper" lambda to the lowered lambda, and allocate the upcall stub by calling .toUpcallStub(arena$)

/**
 * Downcall to Swift:
 * {@snippet lang=swift :
 * public func callMeMore(callback: (UnsafeRawPointer, Float) -> Int, fn: () -> ())
 * }
 */
public static void callMeMore(callMeMore.callback callback, callMeMore.fn fn) {
  try(var arena$ = Arena.ofConfined()) {
    swiftjava___FakeModule_callMeMore_callback_fn.call(callMeMore.$toUpcallStub(callback, arena$), callMeMore.$toUpcallStub(fn, arena$));
  }
}

@rintaro rintaro changed the title [WIP][JExtract] Import any C compatible closure [WIP][JExtract] Import any C compatible closures Jun 6, 2025
@rintaro rintaro force-pushed the jextract-closure-c-compat branch 3 times, most recently from 919e3fd to 83ffb88 Compare June 7, 2025 00:41
printer.print(
"""
public interface \(functionType.name) extends \(cdeclDescritor).Function {
default MemorySegment toUpcallStub(Arena arena) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it's better to make a static func outside the interface, so it won't leak toUpcallStub method?

private static func $toUpcallStub(\(functionType.name) fi, Arena arena) { ... }

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, that'll be nicer. In order keep the interface without default impls so users aren't confused what those funcs are about.

Most importantly we want only one non-default method, but it'll be nice to have those not have any defaulted ones either

/// return Linker.nativeLinker().upcallStub(HANDLE.bindTo(fi), DESC, arena);
/// }
/// }
/// ```
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's pretty good structure, I like it 👍

_ cType: CType
) {
guard case .pointer(.function(let cResultType, let cParameterTypes, variadic: false)) = cType else {
preconditionFailure("must be a C function pointer type")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Include the name and cType in the crash message?

) { printer in
printer.print(
"""
public interface Function {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public interface Function {
@FunctionalInterface
public interface Function {

This isn't "required" in order for an interface to be a functional interface, but it's a good pattern.

What this does is that if we accidentally added one more non-default method to the interface then the java compiler would then error about it. That's important because we want callers to use the () -> {} lambda syntax rather than spell out the explicit types

printer.print(
"""
public interface \(functionType.name) extends \(cdeclDescritor).Function {
default MemorySegment toUpcallStub(Arena arena) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, that'll be nicer. In order keep the interface without default impls so users aren't confused what those funcs are about.

Most importantly we want only one non-default method, but it'll be nice to have those not have any defaulted ones either

)
} else {
// Otherwise, the lambda must be wrapped with the lowered function instance.
assertionFailure("should be unreachable at this point")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If always unreachable, should we make this fatal error and remove the code below it? It's not like we'll be testing if that code is correct after all right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer to keep the code below for now. It illustrates how things would work for non-C-compat closures, and it's also something I plan to implement pretty soon.


/// Function interfaces (i.e. lambda types) required for the Java method.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Function interfaces (i.e. lambda types) required for the Java method.
/// Function interfaces (i.e. functional interface types) required for the Java method.

Nitpick but there's no real lambda types in java, just functional interface types which happen to get implemented with a lambda expression

var parameters: [TranslatedParameter]
var result: TranslatedResult

var isTrivial: Bool {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is trivial the right word? or are those c-convention compatible or something like that?

)
}

/// Translate Swift closure type to Java function interface.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Translate Swift closure type to Java function interface.
/// Translate Swift closure type to Java functional interface.

minor wording nitpick

@ktoso
Copy link
Collaborator

ktoso commented Jun 9, 2025

Sorry about the conflicts 🙏 No more big "move everything" incoming now anymore I think after the big command refactor.

@rintaro rintaro force-pushed the jextract-closure-c-compat branch 2 times, most recently from 4c8ddf2 to c40cf47 Compare June 9, 2025 19:47
@rintaro
Copy link
Member Author

rintaro commented Jun 9, 2025

(holding off merging until #249 is merged)

@ktoso
Copy link
Collaborator

ktoso commented Jun 10, 2025

#249 is merged so this needs a rebase and can be merged at will! 👍

Generalized closure parameter import, part 1. Start with C-compatible
closures which don't need any conversions.
@rintaro rintaro force-pushed the jextract-closure-c-compat branch from c40cf47 to 7860cb8 Compare June 10, 2025 12:18
@rintaro rintaro marked this pull request as ready for review June 10, 2025 12:26
@rintaro rintaro changed the title [WIP][JExtract] Import any C compatible closures [JExtract] Import any C compatible closures Jun 10, 2025
@rintaro rintaro merged commit 60d0b00 into swiftlang:main Jun 10, 2025
17 checks passed
@rintaro rintaro deleted the jextract-closure-c-compat branch June 13, 2025 01:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants