diff --git a/.github/workflows/javadoc.yml b/.github/workflows/javadoc.yml new file mode 100644 index 0000000..1ea6d31 --- /dev/null +++ b/.github/workflows/javadoc.yml @@ -0,0 +1,50 @@ +name: Javadoc + +on: [ release, workflow_dispatch ] + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup Pages + uses: actions/configure-pages@v5 + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: | + 23-ea + distribution: 'temurin' + - name: Grant execute permission for gradlew + if: ${{ runner.os != 'Windows' }} + run: chmod +x gradlew + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + - name: Execute Gradle build + run: ./gradlew aggregateJavadoc + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: './build/docs/javadoc' + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/README.md b/README.md index 11d81b3..8539b27 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ Import as a Gradle dependency: ```groovy dependencies { - implementation("io.github.over-run:marshal:0.1.0-alpha.32-jdk23") + implementation("io.github.over-run:marshal:0.1.0-alpha.33-jdk23") } ``` diff --git a/build.gradle.kts b/build.gradle.kts index 62e87d0..2c2abbd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -36,6 +36,7 @@ val projDevelopers = arrayOf( val junitVersion: String by rootProject val memstackVersion: String by rootProject +val platformVersion: String by rootProject data class Organization( val name: String, @@ -92,6 +93,7 @@ allprojects { // add your dependencies compileOnly("org.jetbrains:annotations:24.1.0") testImplementation("org.junit.jupiter:junit-jupiter:$junitVersion") + testImplementation("io.github.over-run:platform:$platformVersion") api("io.github.over-run:memstack:$memstackVersion") } diff --git a/gradle.properties b/gradle.properties index ad8e9eb..5affdc9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,7 +12,7 @@ projGroupId=io.github.over-run projArtifactId=marshal # The project name should only contain lowercase letters, numbers and hyphen. projName=marshal -projVersion=0.1.0-alpha.32-jdk23 +projVersion=0.1.0-alpha.33-jdk23 projDesc=Marshaler of native libraries # Uncomment them if you want to publish to maven repository. projUrl=https://github.com/Over-Run/marshal @@ -36,3 +36,4 @@ jdkEarlyAccessDoc=jdk23 junitVersion=5.11.0 memstackVersion=0.2.0 +platformVersion=1.0.0 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 9355b41..0aaefbc 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/src/main/java/overrun/marshal/Downcall.java b/src/main/java/overrun/marshal/Downcall.java index c0d402c..fbf7a93 100644 --- a/src/main/java/overrun/marshal/Downcall.java +++ b/src/main/java/overrun/marshal/Downcall.java @@ -55,7 +55,9 @@ * which allows {@code Downcall} to define the hidden class. * You can obtain that lookup object with {@link MethodHandles#lookup()}. *

- * The generated class implements the target class. + * The generated class implements the target class, which is specified with + * {@link DowncallOption#targetClass(Class) DowncallOption::targetClass}. If no target class is specified, the caller + * class will be used. *

Methods

* The loader finds method from the target class and its superclasses. *

diff --git a/src/main/java/overrun/marshal/LayoutBuilder.java b/src/main/java/overrun/marshal/LayoutBuilder.java index a65f4e4..bd08a90 100644 --- a/src/main/java/overrun/marshal/LayoutBuilder.java +++ b/src/main/java/overrun/marshal/LayoutBuilder.java @@ -224,7 +224,7 @@ public Struct getThis() { @Override public StructLayout build() { long size = 0; - long align = 1; + long align = 0; final List finalLayouts = new ArrayList<>(layouts.size()); for (MemoryLayout layout : layouts) { final long alignment = layout.byteAlignment(); diff --git a/src/main/java/overrun/marshal/gen/CType.java b/src/main/java/overrun/marshal/gen/CType.java new file mode 100644 index 0000000..071eb37 --- /dev/null +++ b/src/main/java/overrun/marshal/gen/CType.java @@ -0,0 +1,51 @@ +/* + * MIT License + * + * Copyright (c) 2024 Overrun Organization + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + */ + +package overrun.marshal.gen; + +import overrun.marshal.gen.processor.DescriptorTransformer; + +import java.lang.annotation.*; +import java.lang.foreign.Linker; + +/** + * This marker annotation is for methods and parameters to mark the native type of them. + *

+ * If the target is marked as {@linkplain #canonical() canonical}, then {@link DescriptorTransformer} will use + * the {@linkplain Linker#canonicalLayouts() canonical layout} mapped from the linker of the current operating system. + *

Example

+ *
{@code
+ * @CType(value = "size_t", canonical = true)
+ * long strlen(@CType("const char*") String s);
+ * }
+ * + * @author squid233 + * @since 0.1.0 + */ +@Documented +@Target(ElementType.TYPE_USE) +@Retention(RetentionPolicy.RUNTIME) +public @interface CType { + /** + * {@return the native (C) type of the marked type} + */ + String value(); + + /** + * {@return {@code true} if {@link DescriptorTransformer} should use the canonical layout} + */ + boolean canonical() default false; +} diff --git a/src/main/java/overrun/marshal/gen/processor/DescriptorTransformer.java b/src/main/java/overrun/marshal/gen/processor/DescriptorTransformer.java index 9d92ee2..f88d446 100644 --- a/src/main/java/overrun/marshal/gen/processor/DescriptorTransformer.java +++ b/src/main/java/overrun/marshal/gen/processor/DescriptorTransformer.java @@ -16,16 +16,19 @@ package overrun.marshal.gen.processor; +import overrun.marshal.gen.CType; import overrun.marshal.gen.Sized; import overrun.marshal.struct.ByValue; import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.Linker; import java.lang.foreign.MemoryLayout; import java.lang.foreign.ValueLayout; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.ArrayList; import java.util.List; +import java.util.Map; /** * Transforms the method and parameters into a function descriptor. @@ -34,9 +37,19 @@ * @since 0.1.0 */ public final class DescriptorTransformer extends TypeTransformer { + private final Map canonicalLayouts = Linker.nativeLinker().canonicalLayouts(); + private DescriptorTransformer() { } + private MemoryLayout findCanonicalLayout(String typename) { + MemoryLayout layout = canonicalLayouts.get(typename); + if (layout == null) { + throw new IllegalArgumentException("Canonical layout not found for type: " + typename); + } + return layout; + } + /** * The context. * @@ -54,44 +67,57 @@ public record Context( @Override public FunctionDescriptor process(Context context) { Method method = context.method(); - ProcessorType returnType = ProcessorTypes.fromMethod(method); + CType cType = method.getDeclaredAnnotation(CType.class); List argLayouts = new ArrayList<>(); - Sized sized = method.getDeclaredAnnotation(Sized.class); - MemoryLayout returnLayout = switch (returnType) { - case ProcessorType.Allocator allocator -> allocator.downcallLayout(); - case ProcessorType.Array array -> sized != null ? - ValueLayout.ADDRESS.withTargetLayout(MemoryLayout.sequenceLayout(sized.value(), array.componentType().downcallLayout())) : - array.downcallLayout(); - case ProcessorType.BoolConvert boolConvert -> boolConvert.downcallLayout(); - case ProcessorType.Custom custom -> custom.downcallLayout(); - case ProcessorType.Str str -> str.downcallLayout(); - case ProcessorType.Struct struct -> { - if (method.getDeclaredAnnotation(ByValue.class) != null) { - yield struct.downcallLayout(); + MemoryLayout returnLayout; + if (cType != null && cType.canonical()) { + returnLayout = findCanonicalLayout(cType.value()); + } else { + ProcessorType returnType = ProcessorTypes.fromMethod(method); + Sized sized = method.getDeclaredAnnotation(Sized.class); + returnLayout = switch (returnType) { + case ProcessorType.Allocator allocator -> allocator.downcallLayout(); + case ProcessorType.Array array -> sized != null ? + ValueLayout.ADDRESS.withTargetLayout(MemoryLayout.sequenceLayout(sized.value(), array.componentType().downcallLayout())) : + array.downcallLayout(); + case ProcessorType.BoolConvert boolConvert -> boolConvert.downcallLayout(); + case ProcessorType.Custom custom -> custom.downcallLayout(); + case ProcessorType.Str str -> str.downcallLayout(); + case ProcessorType.Struct struct -> { + if (method.getDeclaredAnnotation(ByValue.class) != null) { + yield struct.downcallLayout(); + } + if (sized != null) { + yield ValueLayout.ADDRESS.withTargetLayout(MemoryLayout.sequenceLayout(sized.value(), struct.downcallLayout())); + } + yield ValueLayout.ADDRESS.withTargetLayout(struct.downcallLayout()); } - if (sized != null) { - yield ValueLayout.ADDRESS.withTargetLayout(MemoryLayout.sequenceLayout(sized.value(), struct.downcallLayout())); - } - yield ValueLayout.ADDRESS.withTargetLayout(struct.downcallLayout()); - } - case ProcessorType.Upcall upcall -> upcall.downcallLayout(); - case ProcessorType.Value value -> value == ProcessorType.Value.ADDRESS && sized != null ? - ValueLayout.ADDRESS.withTargetLayout(MemoryLayout.sequenceLayout(sized.value(), ValueLayout.JAVA_BYTE)) : - value.downcallLayout(); - case ProcessorType.Void _ -> null; - }; + case ProcessorType.Upcall upcall -> upcall.downcallLayout(); + case ProcessorType.Value value -> value == ProcessorType.Value.ADDRESS && sized != null ? + ValueLayout.ADDRESS.withTargetLayout(MemoryLayout.sequenceLayout(sized.value(), ValueLayout.JAVA_BYTE)) : + value.downcallLayout(); + case ProcessorType.Void _ -> null; + }; + } var parameters = context.parameters(); for (int i = context.descriptorSkipFirstParameter() ? 1 : 0, size = parameters.size(); i < size; i++) { Parameter parameter = parameters.get(i); - ProcessorType type = ProcessorTypes.fromParameter(parameter); - argLayouts.add(switch (type) { - case ProcessorType.Struct struct -> parameter.getDeclaredAnnotation(ByValue.class) != null ? - struct.downcallLayout() : - ValueLayout.ADDRESS; - default -> type.downcallLayout(); - }); + CType parameterCType = parameter.getDeclaredAnnotation(CType.class); + MemoryLayout layout; + if (parameterCType != null && parameterCType.canonical()) { + layout = findCanonicalLayout(parameterCType.value()); + } else { + ProcessorType type = ProcessorTypes.fromParameter(parameter); + layout = switch (type) { + case ProcessorType.Struct struct -> parameter.getDeclaredAnnotation(ByValue.class) != null ? + struct.downcallLayout() : + ValueLayout.ADDRESS; + default -> type.downcallLayout(); + }; + } + argLayouts.add(layout); } return returnLayout == null ? diff --git a/src/test/java/overrun/marshal/test/downcall/CTypeTest.java b/src/test/java/overrun/marshal/test/downcall/CTypeTest.java new file mode 100644 index 0000000..f89f9e2 --- /dev/null +++ b/src/test/java/overrun/marshal/test/downcall/CTypeTest.java @@ -0,0 +1,98 @@ +/* + * MIT License + * + * Copyright (c) 2024 Overrun Organization + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + */ + +package overrun.marshal.test.downcall; + +import io.github.overrun.platform.Platform; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import overrun.marshal.Downcall; +import overrun.marshal.DowncallOption; +import overrun.marshal.gen.CType; + +import java.lang.foreign.*; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author squid233 + * @since 0.1.0 + */ +public class CTypeTest { + private static Functions instance; + + interface Functions { + @CType(value = "long", canonical = true) + long returnLong(); + + long acceptLong(@CType(value = "long", canonical = true) long value); + } + + static long returnLong() { + return 42L; + } + + static int returnInt() { + return 41; + } + + static long acceptLong(long value) { + return value * 2L; + } + + static long acceptInt(int value) { + return value * 3L; + } + + @BeforeAll + static void beforeAll() throws NoSuchMethodException, IllegalAccessException { + Linker linker = Linker.nativeLinker(); + MethodHandles.Lookup handles = MethodHandles.lookup(); + Arena arena = Arena.ofAuto(); + + MemorySegment _returnLong = linker.upcallStub(handles.findStatic(CTypeTest.class, "returnLong", MethodType.methodType(long.class)), FunctionDescriptor.of(ValueLayout.JAVA_LONG), arena); + MemorySegment _returnInt = linker.upcallStub(handles.findStatic(CTypeTest.class, "returnInt", MethodType.methodType(int.class)), FunctionDescriptor.of(ValueLayout.JAVA_INT), arena); + MemorySegment _acceptLong = linker.upcallStub(handles.findStatic(CTypeTest.class, "acceptLong", MethodType.methodType(long.class, long.class)), FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG), arena); + MemorySegment _acceptInt = linker.upcallStub(handles.findStatic(CTypeTest.class, "acceptInt", MethodType.methodType(long.class, int.class)), FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_INT), arena); + SymbolLookup lookup = name -> switch (name) { + case "returnLong" -> Optional.of(Platform.current() instanceof Platform.Windows ? _returnInt : _returnLong); + case "acceptLong" -> Optional.of(Platform.current() instanceof Platform.Windows ? _acceptInt : _acceptLong); + default -> Optional.empty(); + }; + instance = Downcall.load(handles, lookup, DowncallOption.targetClass(Functions.class)); + } + + @Test + void testReturnLong() { + if (Platform.current() instanceof Platform.Windows) { + assertEquals(41L, instance.returnLong()); + } else { + assertEquals(42L, instance.returnLong()); + } + } + + @Test + void testAcceptLong() { + if (Platform.current() instanceof Platform.Windows) { + assertEquals(126L, instance.acceptLong(42L)); + } else { + assertEquals(84L, instance.acceptLong(42L)); + } + } +}