diff --git a/.agents/languages/java.md b/.agents/languages/java.md
index 582e032248..a68fd7bd97 100644
--- a/.agents/languages/java.md
+++ b/.agents/languages/java.md
@@ -22,6 +22,9 @@ Load this file when changing anything under `java/` or when Java drives a cross-
- Do not add normal-JVM process-global caches keyed by user classes, generated classes, serializer classes, classloaders, or class-bound method handles. Prefer per-runtime state, immutable shared metadata, or build-time-only template data.
- Concrete serializers may opt into sharing only after auditing retained fields. Treat serializers retaining `TypeResolver`, `RefResolver`, mutable scratch buffers, runtime state, or classloader-sensitive state as non-shareable unless that state is externalized.
- Resolver and serializer hot paths should keep the fast-path/null-slow-path shape obvious. Hoist repeated buffer or cache-state access into locals for multi-step operations and keep rebuild/restoration logic cold.
+- Hot-path feature gates that are runtime constants must be `static final` fields read directly in
+ the branch. Do not hide them behind helper methods such as `jdkInternalFieldAccess()`, because
+ that obscures branch folding and can leave avoidable call/inlining work in hot serializers.
- In Java codec hot paths, avoid `Preconditions.checkArgument` for attacker-controlled primitive
validation. Use direct primitive branches and throw on the cold error path to preserve inlining and
avoid varargs/helper overhead.
@@ -46,12 +49,210 @@ Load this file when changing anything under `java/` or when Java drives a cross-
`MemoryOps` call, keep JVM bulk loads/stores local with raw Unsafe operations instead of routing
through branchful `_unsafeGet*` or `_unsafePut*` helpers. Add or preserve source comments that
explain this inlining invariant when editing these methods.
+- Unsafe object-offset arithmetic must stay `long` before calls such as `getLong(Object, long)` or
+ `putLong(Object, long)`. An all-`int` expression can compile under JDK8 to a bytecode descriptor
+ that fails with `NoSuchMethodError` on JDK9+.
- In primitive-array swap-endian readers, do not loop through `MemoryBuffer._unsafeGet*` helpers.
Copy the payload through the typed payload API, then swap destination values locally so the path
stays stream-safe and avoids Android-dispatch helper drift.
- Keep GraalVM feature code as a thin metadata/registration layer. Build time should publish metadata needed for runtime reconstruction, not retain concrete generated or user serializer instances in the image heap.
- If changes touch GraalVM bootstrap, serializer retention, native-image metadata, or `ObjectStreamSerializer` GraalVM behavior, verify the native-image build and run the produced binary; a plain Java compile is insufficient.
- Put latest-JDK or virtual-thread tests in the latest-JDK test modules with the matching compiler/profile floor, and centralize runtime-version probing in existing compatibility utilities.
+- For JDK25+ zero-Unsafe work, preserve serializer-family selection by type and configuration. Do not switch a type from `ObjectStreamSerializer` or another Fory serializer family to `JavaSerializer`, a JDK stream fallback, or any broad `java.* Serializable` fallback by JDK version or no-arg-constructor shape.
+- JDK25+ zero-Unsafe runtime support must distinguish launch shape. When Fory is on the module
+ path, use `--add-opens=java.base/java.lang.invoke=org.apache.fory.core`; when Fory is on the
+ classpath, use `--add-opens=java.base/java.lang.invoke=ALL-UNNAMED`. Missing this open is an
+ invalid access configuration, not a reason to open per-package JDK internals or switch
+ serializer/object-creation families. JPMS tests that validate named-module access should keep the
+ `org.apache.fory.core` target.
+- Do not probe JDK25+ trusted-lookup availability and turn `_JDKAccess` field-access booleans false when the required `java.base/java.lang.invoke` open is missing. Keep those access flags true on JDK25+ and let the owning trusted-lookup path raise the configuration error.
+- Keep JDK25+ unsafe-removal implementation invariants in agent/design docs and tests, not user guides. User guides should document user actions such as `--sun-misc-unsafe-memory-access=deny` and `java.base/java.lang.invoke` opens; do not expose internal serializer names, owner-model rationale, or avoided fallback strategies there.
+- JDK25+ user docs must not require application module package opens for Fory private-field access.
+ The only required platform open is `java.base/java.lang.invoke`, targeted to `ALL-UNNAMED` for
+ classpath runs or `org.apache.fory.core` for module-path runs; application module package opens
+ are not part of this design.
+- JDK25+ final-field user docs must not tell ordinary classes to implement
+ `java.io.Serializable`. Fory supports ordinary non-Serializable classes; mention
+ `Serializable` only for JDK serialization hook examples or `java.*` serializability checks.
+- JDK25+ final-field user docs must not include fallback advice such as switching unsupported
+ classes to records, no-arg constructors, or custom serializers. Keep user docs focused on the
+ supported runtime setup and normal class model.
+- Do not create a separate JDK25+ support user-guide page for the Java runtime setup unless the
+ user explicitly asks for one. Keep the `java.base/java.lang.invoke` open in install-facing docs
+ such as the Java/Kotlin/Scala install sections and README.
+- JDK25+ zero-Unsafe final-field writes must use a true target-class trusted lookup from the original `IMPL_LOOKUP`, not `IMPL_LOOKUP.in(type)`. JDK26+ normal Fory final-field restoration must pass with `--illegal-final-field-mutation=deny` and must not require `--enable-final-field-mutation`.
+- For JDK25+ object creation, do not use `sun.reflect.ReflectionFactory`, `jdk.unsupported`, or an
+ Unsafe-backed object instantiator. Normal JVM no-constructor construction must use the
+ `ObjectInstantiators` ReflectionFactory bypass path with `Object` as the template constructor, so
+ normal Fory field restoration does not inherit ObjectStream-only first-non-Serializable-superclass
+ constructor validation. ObjectStream-compatible serializers own the separate
+ `ParentNoArgCtrInstantiator` path and must keep Java serialization parent-constructor rules. The
+ JDK25+ ReflectionFactory path uses trusted-lookup access to `jdk.internal.reflect.ReflectionFactory`
+ in `java.base` and must not require `--add-opens=java.base/jdk.internal.reflect=...`; the only
+ JDK25+ platform open remains `java.base/java.lang.invoke=org.apache.fory.core`. GraalVM JDK25+
+ native-image ordinary serializers may use an `ObjectStreamClass.newInstance` MethodHandle only
+ for the exact Serializable case where the serialization constructor class is `Object`; that
+ preserves normal empty-instance semantics because no user superclass constructor can run. For
+ Serializable classes whose first non-Serializable superclass is not `Object`, fail explicitly
+ instead of importing ObjectStream parent-constructor rules into normal Fory object creation.
+ ObjectStream-compatible serializers are the broader stream-specific path: direct ReflectionFactory
+ serialization constructors can produce `Object` there, so they use a cached `ObjectStreamClass`
+ and a private `ObjectStreamClass.newInstance` MethodHandle from `_JDKAccess._trustedLookup`. If
+ the instantiator is retained in the native-image heap, the
+ MethodHandle owner may be initialized at build time but the per-type `ObjectStreamClass` descriptor
+ must be cached only at image runtime. `ForyGraalVMFeature` must register the matching serialization
+ constructor target class for each Serializable hierarchy member so runtime-lazy
+ `ObjectStreamClass.lookupAny` can build descriptors for JDK classes such as `TreeMap` and
+ `TreeSet`. Classes unsupported by ReflectionFactory itself require an accessible no-arg
+ constructor, a record canonical constructor path, or a custom serializer.
+- `UnsafeObjectInstantiator` is the JDK8-24 Unsafe owner only. It must be a top-level instantiator
+ with a Java25 multi-release stub that contains no Unsafe, ObjectStream, ReflectionFactory, or
+ constructor-bypass implementation.
+- Keep the Java25 `_Lookup` overlay unless a future refactor can merge it without exposing Unsafe to the JDK25 class graph. Root `_Lookup` uses Unsafe for the JDK8-24 trusted-lookup fast path, while Java25 `_Lookup` uses the required `java.lang.invoke` open. `DefineClass` is root-owned; when Java25+ generated serializers need hidden nestmate class definition, it must use cached method handles and reflective `Lookup.ClassOption.NESTMATE` loading so Java 8 through Java 14 can still load the root class safely.
+- Treat `ByteArrayOutputStream` and `ByteArrayInputStream` as ordinary streams on every JDK. Do
+ not restore private-buffer wrapping for JDK8-24 performance, because that reintroduces
+ `java.base/java.io` private-field ownership and module-open requirements.
+- `ObjectStreamSerializer` must use its stream-specific object-instantiator path and must not use
+ `TypeResolver.getObjectInstantiator`. ObjectStream reconstruction creates the object before stream
+ fields are read. This path must not invoke Serializable class constructors, including no-arg
+ constructors; the JDK8-24 ReflectionFactory template constructor must be the first
+ non-Serializable superclass no-arg constructor, and private no-arg constructors must be rejected
+ instead of made accessible. Package-private no-arg constructors are valid only for same-package
+ Serializable subclasses.
+- Runtime object-instantiator caches are `SharedRegistry` state backed by `ConcurrentHashMap`.
+ Keep ObjectStream-specific instantiators separate from normal object instantiators.
+- Generated Fory object serializers must initialize object-instantiator fields through
+ `TypeResolver.getObjectInstantiator(Class)`, so generated code respects runtime-scoped object
+ instantiators.
+ Do not emit generated calls to
+ `ObjectInstantiators.getObjectInstantiator(TypeResolver, Class)` or bypass the runtime-scoped owner; format
+ builders without a Fory runtime context may use the base `ObjectInstantiators.getObjectInstantiator(Class)`
+ construction default.
+- Root codegen and builder classes that still need Unsafe on JDK8-24 must route symbolic Unsafe
+ access through a helper with a Java 25 replacement. Do not leave `_JDKAccess.unsafe()` or
+ `sun.misc.Unsafe` references in JDK25-visible classes outside matching `java25` replacements.
+- `ObjectCodecBuilder` primitive generated-code paths must keep one primitive field traversal and
+ dispatch owner. Select direct Unsafe `(base, absoluteAddress)` versus indexed `MemoryBuffer`
+ `(buffer, intIndex)` access at codegen setup time; do not duplicate the primitive switch or emit a
+ per-field JDK-version branch. JDK25+ generated serializers must not reference `sun.misc.Unsafe`.
+- `_UnsafeUtils` is the JDK8-24 Unsafe owner only. It must have a Java25 replacement with no
+ `sun.misc.Unsafe` fields, methods, constructors, imports, or descriptors, and MR-JAR plus
+ benchmark checks must require that replacement so JDK25+ never links `jdk.unsupported` through
+ this utility.
+- Multi-release replacement is class-file exact. Replacing `Foo.class` does not replace
+ `Foo$Bar.class`; root nested classes that contain Unsafe descriptors must either be eliminated,
+ moved under an exact replaceable owner, or have their direct Unsafe operations moved to the
+ already-overlaid top-level class. JDK25 package checks must enumerate packaged Fory class files
+ and inspect forbidden constants so nested leaks are caught without rejecting shaded third-party
+ dependencies.
+- Java 25 multi-release classes never run on Android. Do not keep `AndroidSupport.IS_ANDROID`
+ branches or imports under `src/main/java25`; Android compatibility belongs to the root sources.
+- String zero-copy construction is serializer-owned behavior. Keep private `String` constructor
+ lambda factories in `StringSerializer`, keep `PlatformStringUtils`
+ focused on field and array access, keep serialization hook discovery in serializer-owned code,
+ and keep `_JDKAccess` limited to JDK lookup, module, function factory, and access-flag
+ primitives.
+- JDK25+ serialization hook access must use the required trusted lookup from
+ `java.base/java.lang.invoke=org.apache.fory.core`. Keep `sun.reflect.ReflectionFactory` as a
+ JDK8-24 hook optimization only, and do not add per-type reflective escapes for hook invocation.
+- JDK25+ `PlatformStringUtils` getter methods sit behind `StringSerializer` static-final access
+ gates. Do not add per-call access checks in those getters; missing module opens should fail at
+ trusted-lookup initialization or cold setup, not inside string hot paths.
+- `FieldAccessor` owns field-accessor dispatch. `RecordFieldAccessors` owns record field access,
+ and `InstanceFieldAccessors` owns non-record instance field access. Do not reintroduce a
+ `FieldAccessorFactory` layer. `InstanceFieldAccessors` is public only so generated serializers
+ can name its concrete nested accessor type; treat it as internal owner code, not user API.
+- Android non-record reflection field access belongs inside the root `InstanceFieldAccessors`
+ owner. Do not keep a standalone `ReflectionFieldAccessor`; Java25 never needs that path, and
+ record reflection fallback remains record-owned in `RecordFieldAccessors`. Keep `sun.misc.Unsafe`
+ fields and descriptors below the JVM-only nested instance accessor so Android can load the owner
+ and take the reflection branch.
+- JDK25+ `InstanceFieldAccessors` owns instance field access only. Do not add static-field handling
+ or per-write reflection fallback there, and do not expose public `FieldAccessor` static-field
+ factories. Static special cases such as Scala `MODULE$` belong to the owning serializer and should
+ use a cached `_JDKAccess._trustedLookup(...).findStaticGetter(...)` handle at that call site.
+- JDK25+ final-instance field mutation should use the same trusted-lookup `VarHandle` owner path as
+ non-final field mutation. Do not add a final-field-only `MethodHandle` setter path or ordinary
+ reflective `Field.set*` fallback.
+- JDK25+ `InstanceFieldAccessors` should use one final trusted-lookup `VarHandle` instance accessor
+ with dense access-kind switches, not public primitive/object accessor classes or a JDK25
+ `GeneratedAccessor` or hidden-class accessor generation. Do not wrap `VarHandle.get/set` in
+ hot-path try/catch blocks and do not call `FieldAccessor.checkObj`; VarHandle validates null and
+ receiver type itself. Root Unsafe offset access may keep a debug-only `assert` receiver check
+ because Unsafe does not validate the target object; do not add production receiver checks.
+- JDK25+ generated serializers should store field accessors as concrete
+ `InstanceFieldAccessors.InstanceAccessor` static final fields, initialized once through
+ `FieldAccessor.createAccessor(...)` and a static-init cast. This keeps platform dispatch out of
+ generated read/write hot paths and avoids `FieldAccessor` virtual dispatch on final/private field
+ get/set calls.
+- `DefineClass#defineHiddenNestmate` belongs in the root `DefineClass` owner. Do not add a Java25
+ overlay only to call `Lookup#defineHiddenClass` directly, and do not move it to `java9` because
+ `Lookup#defineClass` defines normal package classes, not hidden nestmates. Root code must avoid
+ direct `Lookup.ClassOption` linkage and cache the method-handle/option-array setup off the hot
+ path.
+- Hidden generated serializers are Java25+ only. Do not broaden serializer hidden-class definition
+ to Java15-24, because those runtimes still use the unsafe-backed field/object path. Keep
+ `AccessorHelper` as the source-generated same-package helper; do not turn it into a bytecode
+ hidden-field owner unless a separate Java25-only design explicitly requires that.
+- JDK25 hidden generated serializers must not emit private split helper methods. Janino lowers
+ private instance helpers to static bridge methods whose receiver parameter uses the original
+ binary class name, which fails hidden-class verification. Use non-private final split helpers on
+ that path.
+- Runtime codegen must not emit Janino source that names bootstrap JDK implementation classes in
+ concealed or non-source-public packages. Generated source in the unnamed module cannot access
+ those classes even when Fory's trusted field-access path can read/write their fields; use
+ `ObjectSerializer` for those object-copy/field paths.
+- Keep JDK26 `--illegal-final-field-mutation=deny` scoped to the JPMS runtime tests that prove
+ Fory's field-access path. Do not put it in global Maven `JDK_JAVA_OPTIONS`, because build tools
+ such as Lombok may perform their own reflective final-field access during compilation.
+- JDK25+ collection serializers can restore JDK private/final collection fields through the
+ required trusted lookup. Do not add JDK25-only unsupported branches for `Collections.newSetFromMap`
+ or similar JDK wrappers when the normal JDK field-access owner can preserve the existing payload.
+- Keep all private `Collections$SetFromMap` field access in one `SetFromMapAccess` owner helper.
+ The non-codegen payload branch is the backing-map payload path used to preserve backing-map
+ object/reference semantics, not a legacy fallback.
+- Do not add a new built-in class to the auto-assigned `registerInternalSerializer` sequence unless
+ it has an explicit stable internal type id. Use `ClassResolver#getSerializerClass` for native-only
+ serializer selection when the class was not previously internally registered; shifting built-in
+ type ids breaks Java native-mode binary compatibility.
+- When a native-only serializer is selected for a class that was already routed through a different
+ serializer family, do not shift built-in type ids. Preserve the existing payload only if it was
+ semantically valid; otherwise use the owning semantic serializer path and cover the behavior with
+ focused tests.
+- When `ClassResolver#getSerializerClass` selects a serializer that is not internally registered,
+ add that serializer class to `GraalvmSupport`'s default serializer metadata so native-image can
+ construct it without shifting Java native-mode type ids.
+- Do not add or restore constructor-binding APIs such as `@ForyConstructor` or
+ `BaseFory.registerConstructor(...)`. Java parameter names, `-parameters`, and
+ `@ConstructorProperties` are not a Fory object-creation contract. Runtime serializers for
+ ordinary classes must create an empty instance through `TypeResolver.getObjectInstantiator(Class)` and
+ set fields; records and source-generated Kotlin serializers are the constructor-owned paths.
+- Source-generated constructor serializers must own their constructor metadata at generation time
+ and call constructors directly. They must not depend on runtime `ObjectInstantiator` constructor-field
+ metadata or varargs constructor calls.
+- Java annotation-processor static serializers do not own ordinary-class constructor metadata.
+ Reject ordinary non-record final fields instead of generating descriptor-based final-field
+ mutation; records and Kotlin KSP primary-constructor serializers are the constructor-owned paths.
+- Generated JVM copy code may direct-copy immutable scalar values, but Java `Collection`/`Map`
+ subclasses must be copied through `CopyContext.copyObject(...)` so collection/map serializers own
+ concrete type, comparator, wrapper, and reference behavior.
+- When a `Throwable` is created without running `Throwable` constructors, restore the private
+ `cause` and `suppressedExceptions` sentinels directly before exposing the object. Do not call
+ `initCause` or `addSuppressed` on a constructor-bypassed `Throwable` whose sentinels are still
+ absent.
+- For JDK25+ CI, do not run core runtime tests from raw Maven reactor `target/classes`. Those
+ classes bypass `META-INF/versions/25` and exercise the JDK8-24 root implementation. Classpath
+ Surefire tests must use a test-only classes directory overlaid with Java16 and Java25 replacement
+ classes and without `module-info.class`, so the run stays unnamed while exercising the
+ zero-Unsafe classes. JPMS tests still own named-module coverage where `org.apache.fory.core` is
+ the real access target.
+- After shading Janino into `fory-core`, refresh the JPMS module descriptor package table from the
+ final jar before install. Otherwise named-module codegen cannot load concealed shaded Janino
+ packages even though the classes are present in the jar.
+- GraalVM native-image tests that run Fory on the classpath may target
+ `java.base/java.lang.invoke=ALL-UNNAMED`; that is the classpath launch shape, not a named-module
+ proof. Keep named-module zero-Unsafe verification in JPMS tests unless a native-image path itself
+ runs Fory as `org.apache.fory.core`.
## Key Modules
@@ -101,4 +302,4 @@ mvn -T16 test -Dtest=org.apache.fory.TestClass#testMethod
- Set `ENABLE_FORY_GENERATED_CLASS_UNIQUE_ID=false` when you need stable generated class names.
- When debugging Java tests or runtime behavior, set `FORY_LOG_LEVEL=INFO` unless a narrower
level is required.
-- In IntelliJ IDEA, use a JDK 11+ project SDK and disable `--release` if it blocks `sun.misc.Unsafe` access.
+- In IntelliJ IDEA, use the same JDK and module flags as the Maven profile you are debugging.
diff --git a/.agents/languages/kotlin.md b/.agents/languages/kotlin.md
index 2855fc6367..2df307a96e 100644
--- a/.agents/languages/kotlin.md
+++ b/.agents/languages/kotlin.md
@@ -6,6 +6,14 @@ Load this file when changing `kotlin/`.
- Run Kotlin Maven commands from within `kotlin/`.
- Kotlin serializers build on the Java implementation. If Java changed and the updated Java artifacts are not installed yet, run `cd ../java && mvn -T16 install -DskipTests` first.
+- KSP `@ForyStruct` serializers that use a primary constructor map constructor parameters to
+ same-named source properties at generation time and call the constructor directly. Do not restore
+ `@ForyConstructor`, runtime constructor registration, or Kotlin `javaParameters` dependencies;
+ mutable no-argument structs should use `var` properties with `@ForyField`.
+- Preserve serializer-family selection for Kotlin standard-library types already registered by
+ Fory. Do not auto-install a new serializer for an existing type-registered Kotlin class unless the
+ wire format matches the previous serializer family and old-payload/new-runtime compatibility is
+ tested.
## Commands
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 03b7b2b5a8..d9c794f51c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -169,7 +169,7 @@ jobs:
MY_VAR: "PATH"
strategy:
matrix:
- java-version: ${{ fromJSON(needs.changes.outputs.java_code == 'true' && '["8","11","17","21","25"]' || '["8"]') }}
+ java-version: ${{ fromJSON(needs.changes.outputs.java_code == 'true' && '["8","11","17","21","25","26"]' || '["8"]') }}
steps:
- uses: actions/checkout@v5
- name: Set up JDK ${{ matrix.java-version }}
diff --git a/.github/workflows/release-java-snapshot.yaml b/.github/workflows/release-java-snapshot.yaml
index 631975aac4..af869e9ebf 100644
--- a/.github/workflows/release-java-snapshot.yaml
+++ b/.github/workflows/release-java-snapshot.yaml
@@ -32,15 +32,15 @@ jobs:
- name: Set up Maven Central Repository
uses: actions/setup-java@v4
with:
- java-version: "11"
- distribution: "adopt"
+ java-version: "25"
+ distribution: "temurin"
architecture: x64
cache: maven
server-id: apache.snapshots.https
server-username: NEXUS_USERNAME
server-password: NEXUS_PASSWORD
- name: Publish Fory Java Snapshot
- run: python ./ci/run_ci.py java --version 11 --release
+ run: python ./ci/run_ci.py java --version 25 --release
env:
NEXUS_USERNAME: ${{ secrets.NEXUS_USER }}
NEXUS_PASSWORD: ${{ secrets.NEXUS_PW }}
diff --git a/README.md b/README.md
index a2f8502541..d5031eb543 100644
--- a/README.md
+++ b/README.md
@@ -142,6 +142,19 @@ Gradle:
implementation "org.apache.fory:fory-core:1.1.0"
```
+On JDK25+, open `java.lang.invoke` to Fory. Use `ALL-UNNAMED` when Fory is on
+the classpath:
+
+```bash
+--add-opens=java.base/java.lang.invoke=ALL-UNNAMED
+```
+
+Use the Fory core module name when Fory is on the module path:
+
+```bash
+--add-opens=java.base/java.lang.invoke=org.apache.fory.core
+```
+
**Scala**
sbt:
diff --git a/benchmarks/java/pom.xml b/benchmarks/java/pom.xml
index c25cf4cdc6..91a46a715d 100644
--- a/benchmarks/java/pom.xml
+++ b/benchmarks/java/pom.xml
@@ -212,13 +212,6 @@
-
-
- org.apache.fory
- fory-simd
- ${project.version}
-
- jmh
@@ -236,6 +229,136 @@
+
+ jdk25-benchmark-mrjar-check
+
+ [25,)
+
+
+
+
+ org.apache.maven.plugins
+ maven-antrun-plugin
+
+
+ verify-benchmark-mrjar
+ package
+
+ run
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -266,6 +389,7 @@
+ trueorg.apache.fory.benchmark
@@ -287,6 +411,10 @@
org.openjdk.jmh.Main
+
+ true
+ org.apache.fory.benchmark
+
@@ -298,6 +426,18 @@
META-INF/*.RSA
+
+ org.apache.logging.log4j:*
+
+ META-INF/versions/**
+
+
+
+ com.fasterxml.jackson.core:jackson-core
+
+ META-INF/versions/**
+
+
diff --git a/benchmarks/java/src/main/java/org/apache/fory/benchmark/CompressStringSuite.java b/benchmarks/java/src/main/java/org/apache/fory/benchmark/CompressStringSuite.java
index 0fcdb6b4ed..73054a6438 100644
--- a/benchmarks/java/src/main/java/org/apache/fory/benchmark/CompressStringSuite.java
+++ b/benchmarks/java/src/main/java/org/apache/fory/benchmark/CompressStringSuite.java
@@ -21,6 +21,7 @@
import java.nio.ByteBuffer;
import org.apache.fory.memory.MemoryBuffer;
+import org.apache.fory.serializer.StringEncodingUtils;
import org.apache.fory.util.StringUtils;
import org.openjdk.jmh.Main;
import org.openjdk.jmh.annotations.Benchmark;
@@ -99,7 +100,7 @@ public Object latinScalarCheck() {
@Benchmark
public Object latinSuperWordCheck() {
- return StringUtils.isLatin(latinStrChars);
+ return StringEncodingUtils.isLatin(latinStrChars);
}
public static void main(String[] args) throws Exception {
diff --git a/benchmarks/java/src/main/java/org/apache/fory/benchmark/Identity2IdMap.java b/benchmarks/java/src/main/java/org/apache/fory/benchmark/Identity2IdMap.java
index 946e98a103..8e5167e031 100644
--- a/benchmarks/java/src/main/java/org/apache/fory/benchmark/Identity2IdMap.java
+++ b/benchmarks/java/src/main/java/org/apache/fory/benchmark/Identity2IdMap.java
@@ -18,7 +18,6 @@
import java.util.ArrayList;
import java.util.List;
-import org.apache.fory.platform.UnsafeOps;
// Derived from
// https://github.com/RuedigerMoeller/fast-serialization/blob/e8da5591daa09452791dcd992ea4f83b20937be7/src/main/java/org/nustaq/serialization/util/FSTIdentity2IdMap.java.
@@ -405,20 +404,10 @@ public static void clear(int[] arr, int len) {
int count = 0;
final int emptyArrayLength = EMPTY_INT_ARRAY.length;
while (len - count > emptyArrayLength) {
- UnsafeOps.copyMemory(
- EMPTY_INT_ARRAY,
- UnsafeOps.INT_ARRAY_OFFSET,
- arr,
- UnsafeOps.INT_ARRAY_OFFSET + count,
- emptyArrayLength);
+ System.arraycopy(EMPTY_INT_ARRAY, 0, arr, count, emptyArrayLength);
count += emptyArrayLength;
}
- UnsafeOps.copyMemory(
- EMPTY_INT_ARRAY,
- UnsafeOps.INT_ARRAY_OFFSET,
- arr,
- UnsafeOps.INT_ARRAY_OFFSET + count,
- len - count);
+ System.arraycopy(EMPTY_INT_ARRAY, 0, arr, count, len - count);
}
public static void clear(Object[] arr, int len) {
diff --git a/benchmarks/java/src/main/java/org/apache/fory/benchmark/Jdk25MrJarCheck.java b/benchmarks/java/src/main/java/org/apache/fory/benchmark/Jdk25MrJarCheck.java
new file mode 100644
index 0000000000..8bdc70b66b
--- /dev/null
+++ b/benchmarks/java/src/main/java/org/apache/fory/benchmark/Jdk25MrJarCheck.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fory.benchmark;
+
+import org.apache.fory.memory.MemoryBuffer;
+
+/** Runtime smoke check that JDK25 benchmark runs load the multi-release Fory classes. */
+public final class Jdk25MrJarCheck {
+ private Jdk25MrJarCheck() {}
+
+ public static void main(String[] args) {
+ verifyVersionedClass(MemoryBuffer.class);
+ verifyMissing("org.apache.fory.platform.UnsafeOps");
+ Class> jdkAccess = verifyRootClass("org.apache.fory.platform.internal._JDKAccess");
+ Class> unsafeUtils = verifyVersionedClass("org.apache.fory.platform.internal._UnsafeUtils");
+ verifyVersionedClass("org.apache.fory.reflect.InstanceFieldAccessors");
+ verifyVersionedClass("org.apache.fory.serializer.PlatformStringUtils");
+ verifyNoUnsafeDescriptors(jdkAccess);
+ verifyNoUnsafeDescriptors(unsafeUtils);
+ }
+
+ private static void verifyMissing(String className) {
+ try {
+ Class.forName(className);
+ throw new IllegalStateException("JDK25 benchmark jar must not contain " + className);
+ } catch (ClassNotFoundException expected) {
+ // expected
+ }
+ }
+
+ private static Class> verifyClass(String className) {
+ try {
+ Class> cls = Class.forName(className);
+ return cls;
+ } catch (ClassNotFoundException e) {
+ throw new IllegalStateException("JDK25 benchmark jar is missing " + className, e);
+ }
+ }
+
+ private static Class> verifyVersionedClass(String className) {
+ Class> cls = verifyClass(className);
+ verifyVersionedClass(cls);
+ return cls;
+ }
+
+ private static void verifyVersionedClass(Class> cls) {
+ String resourceName = cls.getSimpleName() + ".class";
+ String resource = String.valueOf(cls.getResource(resourceName));
+ if (!resource.contains("benchmarks.jar!") || !resource.contains("!/META-INF/versions/25/")) {
+ throw new IllegalStateException("JDK25 benchmark jar loaded root class for " + cls);
+ }
+ }
+
+ private static Class> verifyRootClass(String className) {
+ Class> cls = verifyClass(className);
+ String resourceName = cls.getSimpleName() + ".class";
+ String resource = String.valueOf(cls.getResource(resourceName));
+ if (!resource.contains("benchmarks.jar!") || resource.contains("!/META-INF/versions/25/")) {
+ throw new IllegalStateException("JDK25 benchmark jar loaded versioned class for " + cls);
+ }
+ return cls;
+ }
+
+ private static void verifyNoUnsafeDescriptors(Class> cls) {
+ for (java.lang.reflect.Field field : cls.getDeclaredFields()) {
+ if (isUnsafeType(field.getType())) {
+ throw new IllegalStateException("JDK25 benchmark jar loaded Unsafe field in " + cls);
+ }
+ }
+ for (java.lang.reflect.Method method : cls.getDeclaredMethods()) {
+ if (isUnsafeType(method.getReturnType()) || hasUnsafeType(method.getParameterTypes())) {
+ throw new IllegalStateException("JDK25 benchmark jar loaded Unsafe method in " + cls);
+ }
+ }
+ for (java.lang.reflect.Constructor> constructor : cls.getDeclaredConstructors()) {
+ if (hasUnsafeType(constructor.getParameterTypes())) {
+ throw new IllegalStateException("JDK25 benchmark jar loaded Unsafe constructor in " + cls);
+ }
+ }
+ }
+
+ private static boolean hasUnsafeType(Class>[] types) {
+ for (Class> type : types) {
+ if (isUnsafeType(type)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean isUnsafeType(Class> type) {
+ return type.getName().equals("sun.misc.Unsafe");
+ }
+}
diff --git a/benchmarks/java/src/main/java/org/apache/fory/benchmark/Benchmark.java b/benchmarks/java/src/main/java/org/apache/fory/benchmark/JmhBenchmarkMain.java
similarity index 97%
rename from benchmarks/java/src/main/java/org/apache/fory/benchmark/Benchmark.java
rename to benchmarks/java/src/main/java/org/apache/fory/benchmark/JmhBenchmarkMain.java
index d51f433eb9..27a69f087d 100644
--- a/benchmarks/java/src/main/java/org/apache/fory/benchmark/Benchmark.java
+++ b/benchmarks/java/src/main/java/org/apache/fory/benchmark/JmhBenchmarkMain.java
@@ -21,7 +21,7 @@
import org.openjdk.jmh.Main;
-public class Benchmark {
+public class JmhBenchmarkMain {
// run from cmd:
// cd .. && mvn -T10 install -DskipTests -Dcheckstyle.skip -Dlicense.skip -Dmaven.javadoc.skip
// mvn exec:java -Dexec.args="-f 0 -wi 1 -i 1 -t 1 -w 1s -r 1s -rf csv"
diff --git a/benchmarks/java/src/main/java/org/apache/fory/benchmark/MemorySuite.java b/benchmarks/java/src/main/java/org/apache/fory/benchmark/MemorySuite.java
index a51441b1f3..eea85fddad 100644
--- a/benchmarks/java/src/main/java/org/apache/fory/benchmark/MemorySuite.java
+++ b/benchmarks/java/src/main/java/org/apache/fory/benchmark/MemorySuite.java
@@ -23,7 +23,6 @@
import java.util.Random;
import org.apache.fory.memory.MemoryBuffer;
import org.apache.fory.memory.MemoryUtils;
-import org.apache.fory.platform.UnsafeOps;
import org.apache.fory.util.StringUtils;
import org.openjdk.jmh.Main;
import org.openjdk.jmh.annotations.BenchmarkMode;
@@ -136,17 +135,6 @@ public Object systemArrayCopy(MemoryState state) {
return target;
}
- @org.openjdk.jmh.annotations.Benchmark
- public Object unsafeCopy(MemoryState state) {
- UnsafeOps.UNSAFE.copyMemory(
- state.bytes,
- UnsafeOps.BYTE_ARRAY_OFFSET,
- target,
- UnsafeOps.BYTE_ARRAY_OFFSET,
- state.bytes.length);
- return target;
- }
-
// @org.openjdk.jmh.annotations.Benchmark
public Object arrayAssignCopy(MemoryState state) {
byte[] bytes = state.bytes;
diff --git a/benchmarks/java/src/main/java/org/apache/fory/benchmark/NewJava11StringSuite.java b/benchmarks/java/src/main/java/org/apache/fory/benchmark/NewJava11StringSuite.java
index 38a380a6dd..4ed79ad02b 100644
--- a/benchmarks/java/src/main/java/org/apache/fory/benchmark/NewJava11StringSuite.java
+++ b/benchmarks/java/src/main/java/org/apache/fory/benchmark/NewJava11StringSuite.java
@@ -19,36 +19,26 @@
package org.apache.fory.benchmark;
+import java.nio.charset.StandardCharsets;
import org.apache.fory.Fory;
import org.apache.fory.memory.MemoryBuffer;
import org.apache.fory.platform.JdkVersion;
-import org.apache.fory.platform.UnsafeOps;
-import org.apache.fory.reflect.ReflectionUtils;
import org.apache.fory.serializer.StringSerializer;
import org.apache.fory.util.Preconditions;
import org.apache.fory.util.StringUtils;
import org.openjdk.jmh.Main;
public class NewJava11StringSuite {
-
static String str = StringUtils.random(10);
- static byte[] strBytes;
- static byte coder;
+ static byte[] strBytes = str.getBytes(StandardCharsets.ISO_8859_1);
+ static byte coder = 0;
static {
if (JdkVersion.MAJOR_VERSION > 8) {
- strBytes =
- (byte[]) UnsafeOps.getObject(str, ReflectionUtils.getFieldOffset(String.class, "value"));
- coder = UnsafeOps.getByte(str, ReflectionUtils.getFieldOffset(String.class, "coder"));
+ Preconditions.checkArgument(new String(strBytes, StandardCharsets.ISO_8859_1).equals(str));
}
}
- private static final long STRING_VALUE_FIELD_OFFSET =
- ReflectionUtils.getFieldOffset(String.class, "value");
- private static final long STRING_CODER_FIELD_OFFSET =
- ReflectionUtils.getFieldOffset(String.class, "coder");
-
- private static String stubStr = new String(new char[] {Character.MAX_VALUE, Character.MIN_VALUE});
private static Fory fory =
Fory.builder().withStringCompressed(true).requireClassRegistration(false).build();
private static StringSerializer stringSerializer = new StringSerializer(fory.getConfig());
@@ -63,14 +53,6 @@ public Object createJDK11StringByCopyStr() {
return new String(str);
}
- // @Benchmark
- public Object createJDK11StringByUnsafe() {
- String str = new String(stubStr);
- UnsafeOps.putObject(str, STRING_VALUE_FIELD_OFFSET, strBytes);
- UnsafeOps.putObject(str, STRING_CODER_FIELD_OFFSET, coder);
- return str;
- }
-
// @Benchmark
public Object createJDK8StringByMethodHandle() {
return StringSerializer.newBytesStringZeroCopy(coder, strBytes);
@@ -83,7 +65,6 @@ public Object createJDK8StringByFory() {
}
public static void main(String[] args) throws Exception {
- Preconditions.checkArgument(new NewJava11StringSuite().createJDK11StringByUnsafe().equals(str));
if (args.length == 0) {
String commandLine =
"org.apache.fory.*NewJava11StringSuite.* -f 3 -wi 5 -i 3 -t 1 -w 2s -r 2s -rf csv";
diff --git a/benchmarks/java/src/main/java/org/apache/fory/benchmark/NewStringSuite.java b/benchmarks/java/src/main/java/org/apache/fory/benchmark/NewStringSuite.java
index 114785ef17..af5a4d8942 100644
--- a/benchmarks/java/src/main/java/org/apache/fory/benchmark/NewStringSuite.java
+++ b/benchmarks/java/src/main/java/org/apache/fory/benchmark/NewStringSuite.java
@@ -19,14 +19,11 @@
package org.apache.fory.benchmark;
-import org.apache.fory.platform.UnsafeOps;
-import org.apache.fory.reflect.ReflectionUtils;
import org.apache.fory.serializer.StringSerializer;
import org.apache.fory.util.StringUtils;
import org.openjdk.jmh.Main;
public class NewStringSuite {
-
static String str = StringUtils.random(230);
static char[] strData = str.toCharArray();
static byte[] array = new byte[strData.length * 2];
@@ -41,17 +38,6 @@ public Object createJDK8StringByCopy() {
return new String(strData);
}
- private static final long STRING_VALUE_FIELD_OFFSET =
- ReflectionUtils.getFieldOffset(String.class, "value");
- private static String stubStr = new String(new char[] {Character.MAX_VALUE, Character.MIN_VALUE});
-
- // @Benchmark
- public Object createJDK8StringByUnsafe() {
- String str = new String(stubStr);
- UnsafeOps.putObject(str, STRING_VALUE_FIELD_OFFSET, strData);
- return str;
- }
-
// @Benchmark
public Object createJDK8StringByMethodHandle() {
return StringSerializer.newCharsStringZeroCopy(strData);
diff --git a/benchmarks/java/src/main/java17/org/apache/fory/benchmark/ArrayCompressionSuite.java b/benchmarks/java/src/main/java17/org/apache/fory/benchmark/ArrayCompressionSuite.java
index 4830533064..dc2a975449 100644
--- a/benchmarks/java/src/main/java17/org/apache/fory/benchmark/ArrayCompressionSuite.java
+++ b/benchmarks/java/src/main/java17/org/apache/fory/benchmark/ArrayCompressionSuite.java
@@ -38,12 +38,12 @@
import org.openjdk.jmh.infra.Blackhole;
/**
- * Array compression benchmark suite using SIMD-accelerated compression from fory-simd.
+ * Array compression benchmark suite using SIMD-accelerated compression from fory-core.
*
*
SETUP REQUIRED TO RUN THIS BENCHMARK:
*
- *
1. This benchmark requires Java 16+ and the fory-simd module. To enable SIMD operations, you
- * must temporarily modify benchmark/pom.xml:
+ *
1. This benchmark requires Java 16+. To enable SIMD operations, you must temporarily modify
+ * benchmark/pom.xml:
*
*
2. Build and run the benchmark:
diff --git a/benchmarks/java25/README.md b/benchmarks/java25/README.md
new file mode 100644
index 0000000000..10d923a1b5
--- /dev/null
+++ b/benchmarks/java25/README.md
@@ -0,0 +1,30 @@
+# Java 25 Direct Memory Access Benchmark
+
+This diagnostic JMH module compares direct-buffer scalar access paths used to reason about
+`MemoryBuffer` on JDK 25:
+
+- `MemorySegment.get/set` with native-order unaligned layouts.
+- `MethodHandles.byteBufferViewVarHandle` over a direct `ByteBuffer`.
+
+Each benchmark invocation performs one absolute scalar access at a rolling aligned offset. This is
+closer to generated serializer calls into `MemoryBuffer.writeInt32`, `_unsafePutInt64`, and matching
+read paths than a bulk array-copy benchmark, while keeping the JDK25 benchmark module free of
+`jdk.unsupported` APIs.
+
+Build and run with JDK 25:
+
+```bash
+cd benchmarks/java25
+mvn package
+java -jar target/java25-memory-access-benchmarks.jar \
+ 'org.apache.fory.benchmark.java25.DirectMemoryAccessBenchmark.*' \
+ -f 1 -wi 5 -i 5 -t 1 -w 1s -r 1s
+```
+
+Run the direct-to-heap copy benchmark:
+
+```bash
+java -jar target/java25-memory-access-benchmarks.jar \
+ 'org.apache.fory.benchmark.java25.DirectToHeapCopyBenchmark.*' \
+ -f 1 -wi 5 -i 5 -t 1 -w 1s -r 1s
+```
diff --git a/benchmarks/java25/pom.xml b/benchmarks/java25/pom.xml
new file mode 100644
index 0000000000..72dd1d1ae3
--- /dev/null
+++ b/benchmarks/java25/pom.xml
@@ -0,0 +1,115 @@
+
+
+
+ 4.0.0
+
+ org.apache.fory.benchmark
+ java25-memory-access-benchmark
+ 1.1.0-SNAPSHOT
+ jar
+
+
+ UTF-8
+ 1.33
+ 25
+ true
+ true
+ java25-memory-access-benchmarks
+
+
+
+
+ org.openjdk.jmh
+ jmh-core
+ ${jmh.version}
+
+
+ org.openjdk.jmh
+ jmh-generator-annprocess
+ ${jmh.version}
+ provided
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.14.0
+
+ ${maven.compiler.release}
+
+
+ org.openjdk.jmh
+ jmh-generator-annprocess
+ ${jmh.version}
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.6.0
+
+
+ package
+
+ shade
+
+
+ ${uberjar.name}
+ false
+
+
+ org.openjdk.jmh.Main
+
+
+
+
+ *:*
+
+ META-INF/*.SF
+ META-INF/*.DSA
+ META-INF/*.RSA
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-deploy-plugin
+ 3.1.4
+
+ true
+
+
+
+
+
diff --git a/benchmarks/java25/src/main/java/org/apache/fory/benchmark/java25/DirectMemoryAccessBenchmark.java b/benchmarks/java25/src/main/java/org/apache/fory/benchmark/java25/DirectMemoryAccessBenchmark.java
new file mode 100644
index 0000000000..a66e4a86aa
--- /dev/null
+++ b/benchmarks/java25/src/main/java/org/apache/fory/benchmark/java25/DirectMemoryAccessBenchmark.java
@@ -0,0 +1,160 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fory.benchmark.java25;
+
+import java.lang.foreign.MemorySegment;
+import java.lang.foreign.ValueLayout;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.VarHandle;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.concurrent.TimeUnit;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Threads;
+import org.openjdk.jmh.annotations.Warmup;
+
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+@Warmup(iterations = 5, time = 1)
+@Measurement(iterations = 5, time = 1)
+@Fork(1)
+@Threads(1)
+public class DirectMemoryAccessBenchmark {
+ private static final int BUFFER_BYTES = 64 * 1024;
+ private static final int INT_SLOTS = BUFFER_BYTES / Integer.BYTES;
+ private static final int LONG_SLOTS = BUFFER_BYTES / Long.BYTES;
+ private static final int INT_SLOT_MASK = INT_SLOTS - 1;
+ private static final int LONG_SLOT_MASK = LONG_SLOTS - 1;
+ private static final ByteOrder NATIVE_ORDER = ByteOrder.nativeOrder();
+ private static final VarHandle INT_HANDLE =
+ MethodHandles.byteBufferViewVarHandle(int[].class, NATIVE_ORDER);
+ private static final VarHandle LONG_HANDLE =
+ MethodHandles.byteBufferViewVarHandle(long[].class, NATIVE_ORDER);
+ private static final ValueLayout.OfInt INT_LAYOUT =
+ ValueLayout.JAVA_INT_UNALIGNED.withOrder(NATIVE_ORDER);
+ private static final ValueLayout.OfLong LONG_LAYOUT =
+ ValueLayout.JAVA_LONG_UNALIGNED.withOrder(NATIVE_ORDER);
+
+ @State(Scope.Thread)
+ public static class DirectState {
+ ByteBuffer buffer;
+ MemorySegment segment;
+ int intValue;
+ int intCursor;
+ long longValue;
+ int longCursor;
+
+ @Setup
+ public void setup() {
+ buffer = ByteBuffer.allocateDirect(BUFFER_BYTES).order(NATIVE_ORDER);
+ segment = MemorySegment.ofBuffer(buffer);
+ intValue = 0x12345678;
+ longValue = 0x123456789abcdef0L;
+ for (int i = 0; i < INT_SLOTS; i++) {
+ int intOffset = i << 2;
+ int intPattern = intValue + i;
+ segment.set(INT_LAYOUT, intOffset, intPattern);
+ }
+ for (int i = 0; i < LONG_SLOTS; i++) {
+ int longOffset = i << 3;
+ long longPattern = longValue + i;
+ segment.set(LONG_LAYOUT, longOffset, longPattern);
+ }
+ }
+
+ int nextIntOffset() {
+ return (intCursor++ & INT_SLOT_MASK) << 2;
+ }
+
+ int nextIntValue() {
+ intValue += 0x9e3779b9;
+ return intValue;
+ }
+
+ int nextLongOffset() {
+ return (longCursor++ & LONG_SLOT_MASK) << 3;
+ }
+
+ long nextLongValue() {
+ longValue += 0x9e3779b97f4a7c15L;
+ return longValue;
+ }
+ }
+
+ @Benchmark
+ public int memorySegmentPutInt(DirectState state) {
+ int offset = state.nextIntOffset();
+ int value = state.nextIntValue();
+ state.segment.set(INT_LAYOUT, offset, value);
+ return value;
+ }
+
+ @Benchmark
+ public int varHandlePutInt(DirectState state) {
+ int offset = state.nextIntOffset();
+ int value = state.nextIntValue();
+ INT_HANDLE.set(state.buffer, offset, value);
+ return value;
+ }
+
+ @Benchmark
+ public int memorySegmentGetInt(DirectState state) {
+ return state.segment.get(INT_LAYOUT, state.nextIntOffset());
+ }
+
+ @Benchmark
+ public int varHandleGetInt(DirectState state) {
+ return (int) INT_HANDLE.get(state.buffer, state.nextIntOffset());
+ }
+
+ @Benchmark
+ public long memorySegmentPutLong(DirectState state) {
+ int offset = state.nextLongOffset();
+ long value = state.nextLongValue();
+ state.segment.set(LONG_LAYOUT, offset, value);
+ return value;
+ }
+
+ @Benchmark
+ public long varHandlePutLong(DirectState state) {
+ int offset = state.nextLongOffset();
+ long value = state.nextLongValue();
+ LONG_HANDLE.set(state.buffer, offset, value);
+ return value;
+ }
+
+ @Benchmark
+ public long memorySegmentGetLong(DirectState state) {
+ return state.segment.get(LONG_LAYOUT, state.nextLongOffset());
+ }
+
+ @Benchmark
+ public long varHandleGetLong(DirectState state) {
+ return (long) LONG_HANDLE.get(state.buffer, state.nextLongOffset());
+ }
+}
diff --git a/benchmarks/java25/src/main/java/org/apache/fory/benchmark/java25/DirectToHeapCopyBenchmark.java b/benchmarks/java25/src/main/java/org/apache/fory/benchmark/java25/DirectToHeapCopyBenchmark.java
new file mode 100644
index 0000000000..5b64c1b611
--- /dev/null
+++ b/benchmarks/java25/src/main/java/org/apache/fory/benchmark/java25/DirectToHeapCopyBenchmark.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fory.benchmark.java25;
+
+import java.lang.foreign.MemorySegment;
+import java.nio.ByteBuffer;
+import java.util.concurrent.TimeUnit;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Threads;
+import org.openjdk.jmh.annotations.Warmup;
+
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+@Warmup(iterations = 5, time = 1)
+@Measurement(iterations = 5, time = 1)
+@Fork(1)
+@Threads(1)
+public class DirectToHeapCopyBenchmark {
+ private static final int BUFFER_BYTES = 64 * 1024;
+
+ @State(Scope.Thread)
+ public static class CopyState {
+ @Param({"128", "256", "512", "1024", "2048"})
+ int copySize;
+
+ ByteBuffer directBuffer;
+ MemorySegment directSegment;
+ byte[] heapBuffer;
+ MemorySegment heapSegment;
+
+ @Setup
+ public void setup() {
+ directBuffer = ByteBuffer.allocateDirect(BUFFER_BYTES);
+ directSegment = MemorySegment.ofBuffer(directBuffer);
+ heapBuffer = new byte[BUFFER_BYTES];
+ heapSegment = MemorySegment.ofArray(heapBuffer);
+ for (int i = 0; i < BUFFER_BYTES; i++) {
+ directBuffer.put(i, (byte) (i * 31));
+ }
+ }
+ }
+
+ @Benchmark
+ public int byteBufferGet(CopyState state) {
+ int copySize = state.copySize;
+ byte[] heap = state.heapBuffer;
+ state.directBuffer.get(0, heap, 0, copySize);
+ return heap[copySize - 1];
+ }
+
+ @Benchmark
+ public int memorySegmentCopy(CopyState state) {
+ int copySize = state.copySize;
+ byte[] heap = state.heapBuffer;
+ MemorySegment.copy(state.directSegment, 0, state.heapSegment, 0, copySize);
+ return heap[copySize - 1];
+ }
+}
diff --git a/ci/deploy.sh b/ci/deploy.sh
index fb0123eb16..c88ac210cd 100755
--- a/ci/deploy.sh
+++ b/ci/deploy.sh
@@ -75,6 +75,17 @@ bump_javascript_version() {
}
deploy_jars() {
+ local java_version java_major
+ java_version=$(java -version 2>&1 | awk -F '"' '/version/ {print $2; exit}')
+ if [[ "$java_version" == 1.* ]]; then
+ java_major=$(echo "$java_version" | cut -d. -f2)
+ else
+ java_major=$(echo "$java_version" | cut -d. -f1)
+ fi
+ if [[ "$java_major" -lt 25 ]]; then
+ echo "Java releases must run on JDK25+ so MR-JAR entries are packaged"
+ exit 1
+ fi
cd "$ROOT/java"
mvn -T10 clean deploy --no-transfer-progress -DskipTests -Prelease
}
diff --git a/ci/run_ci.py b/ci/run_ci.py
index 334ad7c77c..12192dcbd4 100644
--- a/ci/run_ci.py
+++ b/ci/run_ci.py
@@ -315,6 +315,7 @@ def parse_args():
"17",
"21",
"25",
+ "26",
"windows_java21",
"integration_tests",
"graalvm",
diff --git a/ci/run_ci.sh b/ci/run_ci.sh
index fafe3185de..316d5c30c0 100755
--- a/ci/run_ci.sh
+++ b/ci/run_ci.sh
@@ -61,6 +61,8 @@ install_pyfory() {
}
JDKS=(
+"zulu26.30.11-ca-crac-jdk26.0.1-linux_x64"
+"zulu25.30.17-ca-jdk25.0.1-linux_x64"
"zulu21.28.85-ca-jdk21.0.0-linux_x64"
"zulu17.44.17-ca-crac-jdk17.0.8-linux_x64"
"zulu15.46.17-ca-jdk15.0.10-linux_x64"
@@ -78,6 +80,17 @@ install_jdks() {
}
graalvm_test() {
+ java_version=$(java -version 2>&1 | awk -F '"' '/version/ {print $2; exit}')
+ if [[ "$java_version" == 1.* ]]; then
+ java_major=$(echo "$java_version" | cut -d. -f2)
+ else
+ java_major=$(echo "$java_version" | cut -d. -f1)
+ fi
+ if [[ "$java_major" -ge 25 ]]; then
+ export JDK_JAVA_OPTIONS="$(jdk25_runtime_options "ALL-UNNAMED") $(jdk25_javac_options)"
+ else
+ unset JDK_JAVA_OPTIONS
+ fi
cd "$ROOT"/java
mvn -T10 -B --no-transfer-progress clean install -DskipTests -pl '!:fory-testsuite'
echo "Start to build graalvm native image"
@@ -89,28 +102,108 @@ graalvm_test() {
echo "Execute graalvm tests succeed!"
}
-integration_tests() {
+jdk25_access_options() {
+ local fory_open_targets="${1:-org.apache.fory.core}"
+ printf "%s" "--sun-misc-unsafe-memory-access=deny"
+ printf " %s" "--add-opens=java.base/java.lang.invoke=${fory_open_targets}"
+}
+
+jdk25_runtime_options() {
+ local fory_targets="${1:-org.apache.fory.core}"
+ printf "%s" "$(jdk25_access_options "$fory_targets")"
+}
+
+jdk25_javac_options() {
+ printf "%s" "--add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED"
+ printf " %s" "--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED"
+ printf " %s" "--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED"
+ printf " %s" "--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED"
+ printf " %s" "--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED"
+ printf " %s" "--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED"
+ printf " %s" "--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED"
+ printf " %s" "--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED"
+ printf " %s" "--add-opens=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED"
+ printf " %s" "--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED"
+ printf " %s" "--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED"
+}
+
+use_jdk() {
+ local jdk="$1"
+ export JAVA_HOME="$ROOT/$jdk"
+ export PATH=$JAVA_HOME/bin:$PATH
+ if [[ "$jdk" =~ zulu([0-9]+) ]]; then
+ local java_major="${BASH_REMATCH[1]}"
+ if [[ "$java_major" -ge 25 ]]; then
+ export JDK_JAVA_OPTIONS="$(jdk25_runtime_options) $(jdk25_javac_options)"
+ else
+ unset JDK_JAVA_OPTIONS
+ fi
+ else
+ unset JDK_JAVA_OPTIONS
+ fi
+}
+
+install_jdk25_fory_artifacts() {
+ local old_java_home="${JAVA_HOME:-}"
+ local old_path="$PATH"
+ local old_jdk_java_options="${JDK_JAVA_OPTIONS:-}"
+ local had_java_home=0
+ local had_jdk_java_options=0
+ [[ -n "${JAVA_HOME+x}" ]] && had_java_home=1
+ [[ -n "${JDK_JAVA_OPTIONS+x}" ]] && had_jdk_java_options=1
+
+ use_jdk "zulu25.30.17-ca-jdk25.0.1-linux_x64"
+ export JDK_JAVA_OPTIONS="$(jdk25_javac_options)"
cd "$ROOT"/java
- mvn -T10 -B --no-transfer-progress clean install -DskipTests
+ mvn -T10 -B --no-transfer-progress clean install -DskipTests -pl '!:fory-testsuite'
+ echo "Verify JDK25 benchmark multi-release jar"
+ cd "$ROOT"/benchmarks/java
+ mvn -T10 -B --no-transfer-progress -Pjmh -DskipTests install
+ unset JDK_JAVA_OPTIONS
+ echo "Verify JPMS tests on JDK25"
+ cd "$ROOT"/integration_tests/jpms_tests
+ mvn -T10 -B --no-transfer-progress clean test
+
+ if [[ "$had_java_home" -eq 1 ]]; then
+ export JAVA_HOME="$old_java_home"
+ else
+ unset JAVA_HOME
+ fi
+ export PATH="$old_path"
+ if [[ "$had_jdk_java_options" -eq 1 ]]; then
+ export JDK_JAVA_OPTIONS="$old_jdk_java_options"
+ else
+ unset JDK_JAVA_OPTIONS
+ fi
+}
+
+integration_tests() {
+ install_jdk25_fory_artifacts
echo "benchmark tests"
cd "$ROOT"/benchmarks/java
mvn -T10 -B --no-transfer-progress clean test install -Pjmh
echo "Start JPMS tests"
cd "$ROOT"/integration_tests/jpms_tests
- mvn -T10 -B --no-transfer-progress clean compile
+ mvn -T10 -B --no-transfer-progress clean test
./run_jlink_smoke.sh
echo "Start jdk compatibility tests"
cd "$ROOT"/integration_tests/jdk_compatibility_tests
mvn -T10 -B --no-transfer-progress clean test
for jdk in "${JDKS[@]}"; do
- export JAVA_HOME="$ROOT/$jdk"
- export PATH=$JAVA_HOME/bin:$PATH
+ if [[ "$jdk" =~ zulu([0-9]+) && "${BASH_REMATCH[1]}" -ge 25 ]]; then
+ echo "Skipping classpath JDK compatibility data generation for ${jdk}; JDK25+ zero-Unsafe coverage runs on JPMS"
+ continue
+ fi
+ use_jdk "$jdk"
echo "First round for generate data: ${jdk}"
mvn -T10 --no-transfer-progress clean test -Dtest=org.apache.fory.integration_tests.JDKCompatibilityTest
done
for jdk in "${JDKS[@]}"; do
- export JAVA_HOME="$ROOT/$jdk"
- export PATH=$JAVA_HOME/bin:$PATH
+ if [[ "$jdk" =~ zulu([0-9]+) && "${BASH_REMATCH[1]}" -ge 25 ]]; then
+ echo "Skipping classpath JDK compatibility verification for ${jdk}; JDK25+ zero-Unsafe coverage runs on JPMS"
+ continue
+ fi
+ use_jdk "$jdk"
echo "Second round for compatibility: ${jdk}"
mvn -T10 --no-transfer-progress clean test -Dtest=org.apache.fory.integration_tests.JDKCompatibilityTest
done
@@ -118,15 +211,42 @@ integration_tests() {
jdk17_plus_tests() {
java -version
- export JDK_JAVA_OPTIONS="--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED"
+ java_version=$(java -version 2>&1 | awk -F '"' '/version/ {print $2; exit}')
+ if [[ "$java_version" == 1.* ]]; then
+ java_major=$(echo "$java_version" | cut -d. -f2)
+ else
+ java_major=$(echo "$java_version" | cut -d. -f1)
+ fi
+ JDK_JAVA_OPTIONS="--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED"
+ if [[ "$java_major" -ge 25 ]]; then
+ JDK_JAVA_OPTIONS="$JDK_JAVA_OPTIONS $(jdk25_runtime_options "ALL-UNNAMED") $(jdk25_javac_options)"
+ fi
+ export JDK_JAVA_OPTIONS
echo "Executing fory java tests"
cd "$ROOT/java"
set +e
- mvn -T10 --batch-mode --no-transfer-progress install
+ if [[ "$java_major" -ge 25 ]]; then
+ # The JDK25+ profile overlays Surefire's classpath with Java25 replacement
+ # classes while keeping the test run unnamed. Keep JPMS coverage below as a
+ # separate named-module check.
+ mvn -T10 --batch-mode --no-transfer-progress clean install
+ else
+ mvn -T10 --batch-mode --no-transfer-progress install
+ fi
testcode=$?
if [[ $testcode -ne 0 ]]; then
exit $testcode
fi
+ if [[ "$java_major" -ge 25 ]]; then
+ unset JDK_JAVA_OPTIONS
+ echo "Executing JDK${java_major} JPMS tests"
+ cd "$ROOT/integration_tests/jpms_tests"
+ mvn -T10 --batch-mode --no-transfer-progress clean test
+ testcode=$?
+ if [[ $testcode -ne 0 ]]; then
+ exit $testcode
+ fi
+ fi
echo "Executing fory java tests succeeds"
}
@@ -179,7 +299,7 @@ case $1 in
echo "Executing fory java tests"
cd "$ROOT/java"
set +e
- mvn -T16 --batch-mode --no-transfer-progress test
+ mvn -T16 --batch-mode --no-transfer-progress clean test
testcode=$?
if [[ $testcode -ne 0 ]]; then
exit $testcode
@@ -191,7 +311,7 @@ case $1 in
echo "Executing fory java tests"
cd "$ROOT/java"
set +e
- mvn -T16 --batch-mode --no-transfer-progress test
+ mvn -T16 --batch-mode --no-transfer-progress clean test
testcode=$?
if [[ $testcode -ne 0 ]]; then
exit $testcode
@@ -207,6 +327,9 @@ case $1 in
java25)
jdk17_plus_tests
;;
+ java26)
+ jdk17_plus_tests
+ ;;
kotlin)
kotlin_tests
;;
diff --git a/ci/tasks/java.py b/ci/tasks/java.py
index c42cd39627..8e6ecc0826 100644
--- a/ci/tasks/java.py
+++ b/ci/tasks/java.py
@@ -56,6 +56,7 @@ def get_jdk_major_version():
"21": "zulu21.28.85-ca-jdk21.0.0-linux_x64",
"24": "zulu24.32.13-ca-fx-jdk24.0.2-linux_x64",
"25": "zulu25.30.17-ca-jdk25.0.1-linux_x64",
+ "26": "zulu26.30.11-ca-crac-jdk26.0.1-linux_x64",
}
@@ -76,6 +77,86 @@ def install_jdks():
logging.info("JDKs downloaded and installed successfully")
+def jdk25_access_options(fory_targets="org.apache.fory.core"):
+ return [
+ "--sun-misc-unsafe-memory-access=deny",
+ f"--add-opens=java.base/java.lang.invoke={fory_targets}",
+ ]
+
+
+def jdk25_runtime_options(fory_targets="org.apache.fory.core"):
+ return jdk25_access_options(fory_targets)
+
+
+def jdk25_javac_options():
+ return [
+ "--add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED",
+ "--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED",
+ "--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED",
+ "--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
+ "--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED",
+ "--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED",
+ "--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED",
+ "--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED",
+ "--add-opens=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED",
+ "--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED",
+ "--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
+ ]
+
+
+def set_jdk_options(java_version):
+ if int(java_version) >= 25:
+ os.environ["JDK_JAVA_OPTIONS"] = " ".join(
+ jdk25_runtime_options() + jdk25_javac_options()
+ )
+ else:
+ os.environ.pop("JDK_JAVA_OPTIONS", None)
+
+
+def use_jdk(java_version):
+ java_home = os.path.join(common.PROJECT_ROOT_DIR, JDKS[java_version])
+ os.environ["JAVA_HOME"] = java_home
+ os.environ["PATH"] = f"{java_home}/bin:{os.environ.get('PATH', '')}"
+ set_jdk_options(java_version)
+
+
+def save_java_env():
+ return {
+ "JAVA_HOME": os.environ.get("JAVA_HOME"),
+ "PATH": os.environ.get("PATH"),
+ "JDK_JAVA_OPTIONS": os.environ.get("JDK_JAVA_OPTIONS"),
+ }
+
+
+def restore_java_env(env):
+ for key, value in env.items():
+ if value is None:
+ os.environ.pop(key, None)
+ else:
+ os.environ[key] = value
+
+
+def install_jdk25_fory_artifacts():
+ env = save_java_env()
+ try:
+ use_jdk("25")
+ os.environ["JDK_JAVA_OPTIONS"] = " ".join(jdk25_javac_options())
+ common.cd_project_subdir("java")
+ common.exec_cmd(
+ "mvn -T10 -B --no-transfer-progress clean install -DskipTests "
+ "-pl '!:fory-testsuite'"
+ )
+ logging.info("Verify JDK25 benchmark multi-release jar")
+ common.cd_project_subdir("benchmarks/java")
+ common.exec_cmd("mvn -T10 -B --no-transfer-progress -Pjmh -DskipTests install")
+ logging.info("Verify JPMS tests on JDK25")
+ os.environ.pop("JDK_JAVA_OPTIONS", None)
+ common.cd_project_subdir("integration_tests/jpms_tests")
+ common.exec_cmd("mvn -T10 -B --no-transfer-progress clean test")
+ finally:
+ restore_java_env(env)
+
+
def create_toolchains_xml(jdk_mappings):
"""Create toolchains.xml file in ~/.m2/ directory."""
import os
@@ -151,7 +232,7 @@ def run_java8():
logging.info("Executing fory java tests with Java 8")
install_jdks()
common.cd_project_subdir("java")
- common.exec_cmd("mvn -T16 --batch-mode --no-transfer-progress test")
+ common.exec_cmd("mvn -T16 --batch-mode --no-transfer-progress clean test")
logging.info("Executing fory java tests succeeds")
@@ -159,7 +240,7 @@ def run_java11():
"""Run Java 11 tests."""
logging.info("Executing fory java tests with Java 11")
common.cd_project_subdir("java")
- common.exec_cmd("mvn -T16 --batch-mode --no-transfer-progress test")
+ common.exec_cmd("mvn -T16 --batch-mode --no-transfer-progress clean test")
logging.info("Executing fory java tests succeeds")
@@ -167,12 +248,26 @@ def run_jdk17_plus(java_version="17"):
"""Run Java 17+ tests."""
logging.info(f"Executing fory java tests with Java {java_version}")
common.exec_cmd("java -version")
- os.environ["JDK_JAVA_OPTIONS"] = (
+ jdk_options = [
"--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED"
- )
+ ]
+ if int(java_version) >= 25:
+ jdk_options.extend(jdk25_runtime_options("ALL-UNNAMED"))
+ jdk_options.extend(jdk25_javac_options())
+ os.environ["JDK_JAVA_OPTIONS"] = " ".join(jdk_options)
common.cd_project_subdir("java")
- common.exec_cmd("mvn -T10 --batch-mode --no-transfer-progress install")
+ if int(java_version) >= 25:
+ # The JDK25+ profile overlays Surefire's classpath with Java25 replacement
+ # classes while keeping the test run unnamed. Keep JPMS coverage below as a
+ # separate named-module check.
+ common.exec_cmd("mvn -T10 --batch-mode --no-transfer-progress clean install")
+ os.environ.pop("JDK_JAVA_OPTIONS", None)
+ logging.info(f"Executing JDK{java_version} JPMS tests")
+ common.cd_project_subdir("integration_tests/jpms_tests")
+ common.exec_cmd("mvn -T10 --batch-mode --no-transfer-progress clean test")
+ else:
+ common.exec_cmd("mvn -T10 --batch-mode --no-transfer-progress install")
logging.info("Executing fory java tests succeeds")
@@ -203,10 +298,7 @@ def run_integration_tests():
logging.info("Executing fory integration tests")
- common.cd_project_subdir("java")
- common.exec_cmd(
- "mvn -T10 -B --no-transfer-progress clean install -DskipTests -pl '!:fory-testsuite'"
- )
+ install_jdk25_fory_artifacts()
logging.info("benchmark tests")
common.cd_project_subdir("benchmarks/java")
@@ -214,7 +306,7 @@ def run_integration_tests():
logging.info("Start JPMS tests")
common.cd_project_subdir("integration_tests/jpms_tests")
- common.exec_cmd("mvn -T10 -B --no-transfer-progress clean compile")
+ common.exec_cmd("mvn -T10 -B --no-transfer-progress clean test")
logging.info("Start jdk compatibility tests")
common.cd_project_subdir("integration_tests/jdk_compatibility_tests")
@@ -227,10 +319,14 @@ def run_integration_tests():
# First round: Generate serialized data files
logging.info("First round: Generate serialized data files for each JDK version")
- for jdk in JDKS.values():
- java_home = os.path.join(common.PROJECT_ROOT_DIR, jdk)
- os.environ["JAVA_HOME"] = java_home
- os.environ["PATH"] = f"{java_home}/bin:{os.environ.get('PATH', '')}"
+ for java_version, jdk in JDKS.items():
+ if int(java_version) >= 25:
+ logging.info(
+ "Skipping classpath JDK compatibility data generation for "
+ f"JDK{java_version}; JDK25+ zero-Unsafe coverage runs on JPMS"
+ )
+ continue
+ use_jdk(java_version)
logging.info(f"Generating data with JDK: {jdk}")
common.exec_cmd(
@@ -239,10 +335,14 @@ def run_integration_tests():
# Second round: Test cross-JDK compatibility
logging.info("Second round: Test cross-JDK compatibility")
- for jdk in JDKS.values():
- java_home = os.path.join(common.PROJECT_ROOT_DIR, jdk)
- os.environ["JAVA_HOME"] = java_home
- os.environ["PATH"] = f"{java_home}/bin:{os.environ.get('PATH', '')}"
+ for java_version, jdk in JDKS.items():
+ if int(java_version) >= 25:
+ logging.info(
+ "Skipping classpath JDK compatibility verification for "
+ f"JDK{java_version}; JDK25+ zero-Unsafe coverage runs on JPMS"
+ )
+ continue
+ use_jdk(java_version)
logging.info(f"Testing compatibility with JDK: {jdk}")
common.exec_cmd(
@@ -255,6 +355,11 @@ def run_integration_tests():
def run_graalvm_test():
"""Run GraalVM tests."""
logging.info("Start GraalVM tests")
+ java_major = get_jdk_major_version()
+ if java_major is not None and java_major >= 25:
+ os.environ["JDK_JAVA_OPTIONS"] = " ".join(jdk25_javac_options())
+ else:
+ os.environ.pop("JDK_JAVA_OPTIONS", None)
common.cd_project_subdir("java")
common.exec_cmd(
@@ -275,17 +380,30 @@ def run_graalvm_test():
def run_release():
"""Release to Maven Central."""
logging.info("Starting release to Maven Central with Java")
+ java_major = get_jdk_major_version()
+ if java_major is None or java_major < 25:
+ raise RuntimeError(
+ "Java releases must run on JDK25+ so MR-JAR entries are packaged"
+ )
common.cd_project_subdir("java")
- # Clean and install without tests first
- logging.info("Cleaning and installing dependencies")
- common.exec_cmd("mvn -T10 -B --no-transfer-progress clean install -DskipTests")
+ previous_jdk_options = os.environ.get("JDK_JAVA_OPTIONS")
+ os.environ["JDK_JAVA_OPTIONS"] = " ".join(jdk25_javac_options())
+ try:
+ # Clean and install without tests first
+ logging.info("Cleaning and installing dependencies")
+ common.exec_cmd("mvn -T10 -B --no-transfer-progress clean install -DskipTests")
- # Deploy to Maven Central
- logging.info("Deploying to Maven Central")
- common.exec_cmd(
- "mvn -T10 -B --no-transfer-progress clean deploy -Dgpg.skip -DskipTests -Papache-release"
- )
+ # Deploy to Maven Central
+ logging.info("Deploying to Maven Central")
+ common.exec_cmd(
+ "mvn -T10 -B --no-transfer-progress clean deploy -Dgpg.skip -DskipTests -Papache-release"
+ )
+ finally:
+ if previous_jdk_options is None:
+ os.environ.pop("JDK_JAVA_OPTIONS", None)
+ else:
+ os.environ["JDK_JAVA_OPTIONS"] = previous_jdk_options
logging.info("Release to Maven Central completed successfully")
@@ -309,6 +427,8 @@ def run(version=None, release=False, install_jdks=False, install_fory=False):
run_jdk17_plus("21")
elif version == "25":
run_jdk17_plus("25")
+ elif version == "26":
+ run_jdk17_plus("26")
elif version == "windows_java21":
run_windows_java21()
elif version == "integration_tests":
diff --git a/docs/guide/java/compression.md b/docs/guide/java/compression.md
index 0892e3b297..587b2c1f7c 100644
--- a/docs/guide/java/compression.md
+++ b/docs/guide/java/compression.md
@@ -76,17 +76,7 @@ Fory fory = Fory.builder()
CompressedArraySerializers.registerSerializers(fory);
```
-**Note**: The `fory-simd` module must be included in your dependencies for compressed array serializers to be available.
-
-### Maven Dependency
-
-```xml
-
- org.apache.fory
- fory-simd
- 1.1.0
-
-```
+Compressed array serializers are included in `fory-core` and use the Java 16+ Vector API when it is available.
## String Compression
diff --git a/docs/guide/java/index.md b/docs/guide/java/index.md
index c52237d803..37a9db48f5 100644
--- a/docs/guide/java/index.md
+++ b/docs/guide/java/index.md
@@ -38,7 +38,7 @@ Apache Foryâ„¢ provides blazingly fast Java object serialization with JIT compil
### Drop-in Replacement
- **100% JDK Serialization Compatible**: Supports `writeObject`/`readObject`/`writeReplace`/`readResolve`/`readObjectNoData`/`Externalizable`
-- **Java 8-24 Support**: Works across all modern Java versions including Java 17+ records
+- **Java 8+ Support**: Works across all modern Java versions including Java 17+ records
- **GraalVM Native Image**: AOT compilation support without reflection configuration
- **Android API 26+ Support**: Core object serialization works on Android without runtime code generation.
@@ -50,6 +50,39 @@ Apache Foryâ„¢ provides blazingly fast Java object serialization with JIT compil
- **Deep Copy**: Efficient deep cloning of complex object graphs with reference preservation
- **Security**: Class registration and configurable deserialization policies
+## Installation
+
+### Maven
+
+```xml
+
+ org.apache.fory
+ fory-core
+ 1.1.0
+
+```
+
+### Gradle
+
+```kotlin
+implementation("org.apache.fory:fory-core:1.1.0")
+```
+
+### JDK25+
+
+On JDK25+, open `java.lang.invoke` to Fory. Use `ALL-UNNAMED` when Fory is on
+the classpath:
+
+```bash
+--add-opens=java.base/java.lang.invoke=ALL-UNNAMED
+```
+
+Use the Fory core module name when Fory is on the module path:
+
+```bash
+--add-opens=java.base/java.lang.invoke=org.apache.fory.core
+```
+
## Quick Start
Note that Fory creation is not cheap, the **Fory instances should be reused between serializations** instead of creating it every time. You should keep Fory as a static global variable, or instance variable of some singleton object or limited objects.
diff --git a/docs/guide/java/troubleshooting.md b/docs/guide/java/troubleshooting.md
index e04a325185..0c0e66c8ef 100644
--- a/docs/guide/java/troubleshooting.md
+++ b/docs/guide/java/troubleshooting.md
@@ -148,6 +148,23 @@ Fory fory = Fory.builder()
fory.registerSerializer(MyClass.class, new MyClassSerializer(fory.getTypeResolver()));
```
+### JDK25+ access errors
+
+On JDK25+, if an error names `java.base/java.lang.invoke`, open `java.lang.invoke` to Fory. Use
+`ALL-UNNAMED` when Fory is on the classpath:
+
+```bash
+--add-opens=java.base/java.lang.invoke=ALL-UNNAMED
+```
+
+Use the Fory core module name when Fory is on the module path:
+
+```bash
+--add-opens=java.base/java.lang.invoke=org.apache.fory.core
+```
+
+Fory does not require application package opens for private-field access.
+
## Performance Issues
### Slow Initial Serialization
diff --git a/docs/guide/java/type-registration.md b/docs/guide/java/type-registration.md
index f05a7665db..15270c7ee9 100644
--- a/docs/guide/java/type-registration.md
+++ b/docs/guide/java/type-registration.md
@@ -40,6 +40,9 @@ fory.register(SomeClass1.class, 1);
```
Note that class registration order is important. Serialization and deserialization peers should have the same registration order.
+Register classes and custom serializers before the first top-level `serialize`, `deserialize`, or
+`copy` call on a `Fory` instance. Fory freezes registration at that point so runtime lookups can use
+the finalized registration state.
Internal type IDs 0-32 are reserved for built-in xlang types. Java native built-ins start at
`Types.NONE + 1`, and user IDs are encoded as `(user_id << 8) | internal_type_id`.
diff --git a/docs/guide/kotlin/index.md b/docs/guide/kotlin/index.md
index fb4ccffc40..391174bf70 100644
--- a/docs/guide/kotlin/index.md
+++ b/docs/guide/kotlin/index.md
@@ -62,6 +62,21 @@ See [Java Features](../java/index.md#features) for complete feature list.
implementation("org.apache.fory:fory-kotlin:1.1.0")
```
+### JDK25+
+
+Kotlin uses the Fory Java core at runtime. On JDK25+, open `java.lang.invoke`
+to Fory. Use `ALL-UNNAMED` when Fory is on the classpath:
+
+```bash
+--add-opens=java.base/java.lang.invoke=ALL-UNNAMED
+```
+
+Use the Fory core module name when Fory is on the module path:
+
+```bash
+--add-opens=java.base/java.lang.invoke=org.apache.fory.core
+```
+
## Quick Start
```kotlin
diff --git a/docs/guide/scala/index.md b/docs/guide/scala/index.md
index 2c925c2854..d60d1961a2 100644
--- a/docs/guide/scala/index.md
+++ b/docs/guide/scala/index.md
@@ -52,6 +52,21 @@ Add the dependency with sbt:
libraryDependencies += "org.apache.fory" %% "fory-scala" % "1.1.0"
```
+### JDK25+
+
+Scala uses the Fory Java core at runtime. On JDK25+, open `java.lang.invoke` to
+Fory. Use `ALL-UNNAMED` when Fory is on the classpath:
+
+```bash
+--add-opens=java.base/java.lang.invoke=ALL-UNNAMED
+```
+
+Use the Fory core module name when Fory is on the module path:
+
+```bash
+--add-opens=java.base/java.lang.invoke=org.apache.fory.core
+```
+
## Quick Start
```scala
diff --git a/integration_tests/android_tests/src/main/java/org/apache/fory/android/AndroidForyRuntimeScenarios.java b/integration_tests/android_tests/src/main/java/org/apache/fory/android/AndroidForyRuntimeScenarios.java
index ed1cb3ea01..54f84806f5 100644
--- a/integration_tests/android_tests/src/main/java/org/apache/fory/android/AndroidForyRuntimeScenarios.java
+++ b/integration_tests/android_tests/src/main/java/org/apache/fory/android/AndroidForyRuntimeScenarios.java
@@ -64,12 +64,8 @@ public static void androidRuntimeDisablesCodegenAndUnsafeCopies() {
"async compilation must be disabled on Android");
MemoryBuffer buffer = MemoryUtils.buffer(16);
- try {
- buffer.copyToUnsafe(0, new byte[16], 0, 1);
- fail("copyToUnsafe should fail on Android");
- } catch (UnsupportedOperationException expected) {
- check(expected.getMessage().contains("Android"), expected.getMessage());
- }
+ byte[] target = new byte[16];
+ buffer.copyToByteArray(0, target, 0, 1);
}
public static void structEnumCollectionAndMapRoundTrip() {
diff --git a/integration_tests/graalvm_tests/README.md b/integration_tests/graalvm_tests/README.md
index 8073b9ee35..10473f941c 100644
--- a/integration_tests/graalvm_tests/README.md
+++ b/integration_tests/graalvm_tests/README.md
@@ -11,8 +11,5 @@ mvn -DskipTests=true -Pnative package
## Benchmark
```bash
-BENCHMARK_REPEAT=400000 mvn -Pnative -Dagent=true -DskipTests -DskipNativeBuild=true package exec:exec@java-agent
-BENCHMARK_REPEAT=400000 mvn -DskipTests=true -Pnative -Dagent=true package
+BENCHMARK_REPEAT=400000 mvn -DskipTests=true -Pnative package
```
-
-`-Dagent=true` is needed by JDK serialization only to build reflection config, it's not needed for fory serialization.
diff --git a/integration_tests/graalvm_tests/pom.xml b/integration_tests/graalvm_tests/pom.xml
index f2f9945dd8..a070e22b97 100644
--- a/integration_tests/graalvm_tests/pom.xml
+++ b/integration_tests/graalvm_tests/pom.xml
@@ -140,6 +140,25 @@
+
+ native-jdk25
+
+ [25,)
+
+
+
+
+ org.graalvm.buildtools
+ native-maven-plugin
+
+
+ -J--add-opens=java.base/java.lang.invoke=ALL-UNNAMED
+
+
+
+
+
+ native
@@ -169,9 +188,6 @@
false
-
- true
-
diff --git a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/FeatureTestExample.java b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/FeatureTestExample.java
index c39d39412e..1c2d52c3d5 100644
--- a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/FeatureTestExample.java
+++ b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/FeatureTestExample.java
@@ -19,12 +19,14 @@
package org.apache.fory.graalvm;
+import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import org.apache.fory.Fory;
import org.apache.fory.builder.Generated;
import org.apache.fory.platform.GraalvmSupport;
+import org.apache.fory.platform.JdkVersion;
import org.apache.fory.util.Preconditions;
public class FeatureTestExample {
@@ -43,11 +45,41 @@ public String getValue() {
}
}
+ public static class PrivateNoArgParent {
+ static int noArgCalls;
+ private final String parentName;
+
+ private PrivateNoArgParent() {
+ noArgCalls++;
+ parentName = "no-arg";
+ }
+
+ PrivateNoArgParent(String parentName) {
+ this.parentName = parentName;
+ }
+
+ String parentName() {
+ return parentName;
+ }
+ }
+
+ public static final class SerializablePrivateParentBean extends PrivateNoArgParent
+ implements Serializable {
+ private String childName;
+
+ public SerializablePrivateParentBean(String parentName, String childName) {
+ super(parentName);
+ this.childName = childName;
+ }
+ }
+
public interface TestInterface {
String getValue();
}
- public static class TestInvocationHandler implements InvocationHandler {
+ public static class TestInvocationHandler implements InvocationHandler, Serializable {
+ private static final long serialVersionUID = 1L;
+
private final String value;
public TestInvocationHandler(String value) {
@@ -77,6 +109,7 @@ private static Fory createFory() {
.requireClassRegistration(true)
.build();
fory.register(PrivateConstructorClass.class);
+ fory.register(SerializablePrivateParentBean.class);
fory.register(TestInvocationHandler.class);
GraalvmSupport.registerProxySupport(TestInterface.class);
fory.ensureSerializersCompiled();
@@ -96,6 +129,8 @@ public static void main(String[] args) {
Preconditions.checkArgument("test-value".equals(deserialized.getValue()));
System.out.println("Private constructor class test passed");
+ testNormalObjectCreation();
+
// Test proxy serialization
TestInterface proxy =
(TestInterface)
@@ -109,4 +144,38 @@ public static void main(String[] args) {
System.out.println("FeatureTestExample succeed");
}
+
+ private static void testNormalObjectCreation() {
+ SerializablePrivateParentBean original =
+ new SerializablePrivateParentBean("parent-value", "child-value");
+ PrivateNoArgParent.noArgCalls = 0;
+ try {
+ SerializablePrivateParentBean deserialized =
+ (SerializablePrivateParentBean) fory.deserialize(fory.serialize(original));
+ Preconditions.checkArgument("parent-value".equals(deserialized.parentName()));
+ Preconditions.checkArgument("child-value".equals(deserialized.childName));
+ Preconditions.checkArgument(PrivateNoArgParent.noArgCalls == 0);
+ System.out.println("Normal object creation test passed");
+ } catch (RuntimeException e) {
+ if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE && JdkVersion.MAJOR_VERSION >= 25) {
+ Preconditions.checkArgument(
+ hasMessage(e, "would change ordinary Fory object-creation semantics"));
+ Preconditions.checkArgument(PrivateNoArgParent.noArgCalls == 0);
+ System.out.println("Normal object creation unsupported test passed");
+ return;
+ }
+ throw e;
+ }
+ }
+
+ private static boolean hasMessage(Throwable throwable, String message) {
+ while (throwable != null) {
+ String throwableMessage = throwable.getMessage();
+ if (throwableMessage != null && throwableMessage.contains(message)) {
+ return true;
+ }
+ throwable = throwable.getCause();
+ }
+ return false;
+ }
}
diff --git a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ObjectStreamExample.java b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ObjectStreamExample.java
index f7a9c29932..51693a22d5 100644
--- a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ObjectStreamExample.java
+++ b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ObjectStreamExample.java
@@ -66,7 +66,15 @@ public class ObjectStreamExample extends AbstractMap {
FORY.ensureSerializersCompiled();
}
- final int[] ints = new int[10];
+ final int[] ints;
+
+ public ObjectStreamExample() {
+ this(new int[10]);
+ }
+
+ public ObjectStreamExample(int[] ints) {
+ this.ints = ints;
+ }
public static void main(String[] args) {
AsyncTreeSetSubclass values = new AsyncTreeSetSubclass();
diff --git a/integration_tests/jdk_compatibility_tests/.gitignore b/integration_tests/jdk_compatibility_tests/.gitignore
new file mode 100644
index 0000000000..5cee0c0b39
--- /dev/null
+++ b/integration_tests/jdk_compatibility_tests/.gitignore
@@ -0,0 +1,4 @@
+/object_schema_consistent*
+/object_schema_compatible*
+/custom_object_schema_consistent*
+/custom_object_schema_compatible*
diff --git a/integration_tests/jdk_compatibility_tests/pom.xml b/integration_tests/jdk_compatibility_tests/pom.xml
index 89e588219e..9df4aeb431 100644
--- a/integration_tests/jdk_compatibility_tests/pom.xml
+++ b/integration_tests/jdk_compatibility_tests/pom.xml
@@ -48,11 +48,6 @@
fory-format${project.version}
-
- org.apache.fory
- benchmark
- ${project.version}
- org.testngtestng
@@ -81,4 +76,27 @@
+
+
+
+ skip-jdk25-and-higher
+
+ [25,)
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+ true
+
+
+
+
+
+
+
diff --git a/integration_tests/jdk_compatibility_tests/src/test/java/org/apache/fory/integration_tests/ForyTest.java b/integration_tests/jdk_compatibility_tests/src/test/java/org/apache/fory/integration_tests/ForyTest.java
index 19a2cb7934..6349a85f7d 100644
--- a/integration_tests/jdk_compatibility_tests/src/test/java/org/apache/fory/integration_tests/ForyTest.java
+++ b/integration_tests/jdk_compatibility_tests/src/test/java/org/apache/fory/integration_tests/ForyTest.java
@@ -19,29 +19,119 @@
package org.apache.fory.integration_tests;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
import org.apache.fory.Fory;
-import org.apache.fory.benchmark.data.MediaContent;
-import org.apache.fory.benchmark.data.Sample;
+import org.apache.fory.platform.JdkVersion;
import org.testng.Assert;
import org.testng.annotations.Test;
public class ForyTest {
@Test
- public void testMediaContent() {
- Sample object = new Sample().populate(false);
+ public void testClasspathBeanGraph() {
+ ClasspathBean object =
+ new ClasspathBean(
+ "graph", Arrays.asList(new ClasspathItem("left", 1), new ClasspathItem("right", 2)));
Fory fory = Fory.builder().withXlang(false).requireClassRegistration(false).build();
byte[] data = fory.serialize(object);
- Sample sample = (Sample) fory.deserialize(data);
- Assert.assertEquals(sample, object);
+ Assert.assertEquals(fory.deserialize(data), object);
}
@Test
- public void testSample() {
- MediaContent object = new MediaContent().populate(false);
+ public void testClasspathRuntime() throws Exception {
+ if (JdkVersion.MAJOR_VERSION >= 9) {
+ Object module = Class.class.getMethod("getModule").invoke(Fory.class);
+ boolean named = (boolean) module.getClass().getMethod("isNamed").invoke(module);
+ Assert.assertFalse(named);
+ }
+ }
+
+ @Test
+ public void testFinalFieldBean() {
Fory fory = Fory.builder().withXlang(false).requireClassRegistration(false).build();
- byte[] data = fory.serialize(object);
- MediaContent mediaContent = (MediaContent) fory.deserialize(data);
- Assert.assertEquals(mediaContent, object);
+ FinalFieldBean value = new FinalFieldBean("amy", 42);
+ Object deserialized = fory.deserialize(fory.serialize(value));
+ Assert.assertEquals(deserialized, value);
+ }
+
+ static final class FinalFieldBean implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ private final String name;
+ private final int age;
+
+ private FinalFieldBean(String name, int age) {
+ this.name = name;
+ this.age = age;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof FinalFieldBean)) {
+ return false;
+ }
+ FinalFieldBean other = (FinalFieldBean) obj;
+ return age == other.age && name.equals(other.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return name.hashCode() * 31 + age;
+ }
+ }
+
+ static final class ClasspathBean implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ private String name;
+ private List items;
+
+ private ClasspathBean(String name, List items) {
+ this.name = name;
+ this.items = items;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof ClasspathBean)) {
+ return false;
+ }
+ ClasspathBean other = (ClasspathBean) obj;
+ return Objects.equals(name, other.name) && Objects.equals(items, other.items);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, items);
+ }
+ }
+
+ static final class ClasspathItem implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ private String name;
+ private int count;
+
+ private ClasspathItem(String name, int count) {
+ this.name = name;
+ this.count = count;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof ClasspathItem)) {
+ return false;
+ }
+ ClasspathItem other = (ClasspathItem) obj;
+ return count == other.count && Objects.equals(name, other.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, count);
+ }
}
}
diff --git a/integration_tests/jdk_compatibility_tests/src/test/java/org/apache/fory/integration_tests/JDKCompatibilityTest.java b/integration_tests/jdk_compatibility_tests/src/test/java/org/apache/fory/integration_tests/JDKCompatibilityTest.java
index 24a1ad9dd8..a24b1d8a0c 100644
--- a/integration_tests/jdk_compatibility_tests/src/test/java/org/apache/fory/integration_tests/JDKCompatibilityTest.java
+++ b/integration_tests/jdk_compatibility_tests/src/test/java/org/apache/fory/integration_tests/JDKCompatibilityTest.java
@@ -85,12 +85,10 @@ public void testSchemaConsist() throws IOException {
Fory fory = builder().build();
fory.register(CustomObject.class);
File dir = new File(".");
- File[] files = dir.listFiles((d, name) -> name.startsWith("object_schema_consistent"));
- assert files != null;
+ File[] files = preJdk25Files(dir, "object_schema_consistent");
check(object, fory, files);
CustomObject customObject = createCustomObject();
- File[] files1 = dir.listFiles((d, name) -> name.startsWith("custom_object_schema_consistent"));
- assert files1 != null;
+ File[] files1 = preJdk25Files(dir, "custom_object_schema_consistent");
check(customObject, fory, files1);
}
@@ -100,15 +98,32 @@ public void testSchemaCompatible() throws IOException {
Fory fory = builder().withCompatible(true).build();
fory.register(CustomObject.class);
File dir = new File(".");
- File[] files = dir.listFiles((d, name) -> name.startsWith("object_schema_compatible"));
- assert files != null;
+ File[] files = preJdk25Files(dir, "object_schema_compatible");
check(object, fory, files);
CustomObject customObject = createCustomObject();
- File[] files1 = dir.listFiles((d, name) -> name.startsWith("custom_object_schema_compatible"));
- assert files1 != null;
+ File[] files1 = preJdk25Files(dir, "custom_object_schema_compatible");
check(customObject, fory, files1);
}
+ private static File[] preJdk25Files(File dir, String prefix) {
+ File[] files =
+ dir.listFiles((d, name) -> name.startsWith(prefix) && isPreJdk25Fixture(name, prefix));
+ assert files != null;
+ return files;
+ }
+
+ private static boolean isPreJdk25Fixture(String name, String prefix) {
+ String suffix = name.substring(prefix.length());
+ if (suffix.isEmpty()) {
+ return false;
+ }
+ try {
+ return Integer.parseInt(suffix) < 25;
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
+
private static void check(Object object, Fory fory, File[] files) throws IOException {
for (File file : files) {
byte[] bytes = Files.readAllBytes(file.toPath());
diff --git a/integration_tests/jpms_tests/pom.xml b/integration_tests/jpms_tests/pom.xml
index 5ad4ed100e..e796a80e9b 100644
--- a/integration_tests/jpms_tests/pom.xml
+++ b/integration_tests/jpms_tests/pom.xml
@@ -36,14 +36,10 @@
1111UTF-8
+
-
- org.apache.fory
- benchmark
- ${project.version}
- org.apache.foryfory-core
@@ -76,4 +72,39 @@
+
+
+ jdk25-and-higher
+
+ [25,)
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ --sun-misc-unsafe-memory-access=deny
+ --add-opens=java.base/java.lang.invoke=org.apache.fory.core
+ ${fory.final.field.policy.arg}
+
+
+
+
+
+
+
+ jdk26-and-higher
+
+ [26,)
+
+
+
+ --illegal-final-field-mutation=deny
+
+
+
+
+
diff --git a/integration_tests/jpms_tests/src/main/java/module-info.java b/integration_tests/jpms_tests/src/main/java/module-info.java
index 2cde522de1..be2ec6f58c 100644
--- a/integration_tests/jpms_tests/src/main/java/module-info.java
+++ b/integration_tests/jpms_tests/src/main/java/module-info.java
@@ -18,12 +18,13 @@
*/
module org.apache.fory.integration_tests {
+ requires org.apache.fory.core;
+ requires org.apache.fory.format;
+ requires org.apache.fory.test.core;
- requires org.apache.fory.benchmark;
- requires org.apache.fory.core;
- requires org.apache.fory.format;
- requires org.apache.fory.test.core;
+ // we can't really test any classes from this module because it only contains test-classes
+ requires org.apache.fory.test.suite;
- // we can't really test any classes from this module because it only contains test-classes
- requires org.apache.fory.test.suite;
+ exports org.apache.fory.integration_tests.model;
+ exports org.apache.fory.integration_tests.publicserializer;
}
diff --git a/integration_tests/jpms_tests/src/main/java/org/apache/fory/integration_tests/Test.java b/integration_tests/jpms_tests/src/main/java/org/apache/fory/integration_tests/Test.java
index 56ac832e75..c1a9d58bea 100644
--- a/integration_tests/jpms_tests/src/main/java/org/apache/fory/integration_tests/Test.java
+++ b/integration_tests/jpms_tests/src/main/java/org/apache/fory/integration_tests/Test.java
@@ -20,20 +20,19 @@
package org.apache.fory.integration_tests;
import org.apache.fory.Fory;
-import org.apache.fory.benchmark.Benchmark;
import org.apache.fory.format.encoder.Encoders;
import org.apache.fory.test.bean.Foo;
/**
- * A test class that simply references classes from the various Fory artifacts to check whether
- * they specify the module names referenced in the module-info descriptor.
+ * A test class that simply references classes from the various Fory artifacts to check whether they
+ * specify the module names referenced in the module-info descriptor.
*/
public class Test {
- public static void main(String[] args) {
- final Fory fory = Fory.builder().build();
- fory.serialize(Foo.create());
+ public static void main(String[] args) {
+ final Fory fory = Fory.builder().build();
+ fory.serialize(Foo.create());
- Encoders.bean(Benchmark.class, fory);
- }
+ Encoders.bean(Foo.class, fory);
+ }
}
diff --git a/integration_tests/jpms_tests/src/main/java/org/apache/fory/integration_tests/model/NonSerializableNoNoArgBean.java b/integration_tests/jpms_tests/src/main/java/org/apache/fory/integration_tests/model/NonSerializableNoNoArgBean.java
new file mode 100644
index 0000000000..2f44ada084
--- /dev/null
+++ b/integration_tests/jpms_tests/src/main/java/org/apache/fory/integration_tests/model/NonSerializableNoNoArgBean.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fory.integration_tests.model;
+
+class NonSerializableNoNoArgParent {
+ static int constructorCalls;
+ final int parentValue;
+
+ NonSerializableNoNoArgParent(int parentValue) {
+ constructorCalls++;
+ this.parentValue = parentValue;
+ }
+}
+
+public final class NonSerializableNoNoArgBean extends NonSerializableNoNoArgParent {
+ private final int value;
+
+ public NonSerializableNoNoArgBean(int parentValue, int value) {
+ super(parentValue);
+ this.value = value;
+ }
+
+ public int parentValue() {
+ return parentValue;
+ }
+
+ public int value() {
+ return value;
+ }
+
+ public static void resetParentConstructorCalls() {
+ NonSerializableNoNoArgParent.constructorCalls = 0;
+ }
+
+ public static int parentConstructorCalls() {
+ return NonSerializableNoNoArgParent.constructorCalls;
+ }
+}
diff --git a/integration_tests/jpms_tests/src/main/java/org/apache/fory/integration_tests/model/PrivateFieldBean.java b/integration_tests/jpms_tests/src/main/java/org/apache/fory/integration_tests/model/PrivateFieldBean.java
new file mode 100644
index 0000000000..036d405fac
--- /dev/null
+++ b/integration_tests/jpms_tests/src/main/java/org/apache/fory/integration_tests/model/PrivateFieldBean.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fory.integration_tests.model;
+
+import java.io.Serializable;
+
+public final class PrivateFieldBean implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ private final int value;
+
+ public PrivateFieldBean(int value) {
+ this.value = value;
+ }
+
+ public int value() {
+ return value;
+ }
+}
diff --git a/integration_tests/jpms_tests/src/main/java/org/apache/fory/integration_tests/publicserializer/PublicSerializerValue.java b/integration_tests/jpms_tests/src/main/java/org/apache/fory/integration_tests/publicserializer/PublicSerializerValue.java
new file mode 100644
index 0000000000..6b2e131f2d
--- /dev/null
+++ b/integration_tests/jpms_tests/src/main/java/org/apache/fory/integration_tests/publicserializer/PublicSerializerValue.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fory.integration_tests.publicserializer;
+
+public final class PublicSerializerValue {
+ public final int value;
+
+ public PublicSerializerValue(int value) {
+ this.value = value;
+ }
+}
diff --git a/integration_tests/jpms_tests/src/main/java/org/apache/fory/integration_tests/publicserializer/PublicSerializerValueSerializer.java b/integration_tests/jpms_tests/src/main/java/org/apache/fory/integration_tests/publicserializer/PublicSerializerValueSerializer.java
new file mode 100644
index 0000000000..bf37b26eed
--- /dev/null
+++ b/integration_tests/jpms_tests/src/main/java/org/apache/fory/integration_tests/publicserializer/PublicSerializerValueSerializer.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fory.integration_tests.publicserializer;
+
+import org.apache.fory.context.ReadContext;
+import org.apache.fory.context.WriteContext;
+import org.apache.fory.resolver.TypeResolver;
+import org.apache.fory.serializer.Serializer;
+
+public final class PublicSerializerValueSerializer extends Serializer {
+ public PublicSerializerValueSerializer(
+ TypeResolver typeResolver, Class type) {
+ super(typeResolver.getConfig(), type);
+ }
+
+ @Override
+ public void write(WriteContext writeContext, PublicSerializerValue value) {
+ writeContext.getBuffer().writeInt32(value.value);
+ }
+
+ @Override
+ public PublicSerializerValue read(ReadContext readContext) {
+ return new PublicSerializerValue(readContext.getBuffer().readInt32());
+ }
+}
diff --git a/integration_tests/jpms_tests/src/test/java/org/apache/fory/integration_tests/JpmsFieldAccessorTest.java b/integration_tests/jpms_tests/src/test/java/org/apache/fory/integration_tests/JpmsFieldAccessorTest.java
new file mode 100644
index 0000000000..700f3a20bb
--- /dev/null
+++ b/integration_tests/jpms_tests/src/test/java/org/apache/fory/integration_tests/JpmsFieldAccessorTest.java
@@ -0,0 +1,161 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fory.integration_tests;
+
+import java.lang.reflect.Field;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import org.apache.fory.Fory;
+import org.apache.fory.integration_tests.model.NonSerializableNoNoArgBean;
+import org.apache.fory.integration_tests.model.PrivateFieldBean;
+import org.apache.fory.integration_tests.publicserializer.PublicSerializerValue;
+import org.apache.fory.integration_tests.publicserializer.PublicSerializerValueSerializer;
+import org.apache.fory.reflect.FieldAccessor;
+import org.apache.fory.serializer.CodegenSerializer.LazyInitBeanSerializer;
+import org.apache.fory.serializer.Serializer;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+public class JpmsFieldAccessorTest {
+ private static final int JDK_MAJOR_VERSION = Runtime.version().feature();
+ private static final String INSTANCE_ACCESSOR =
+ "org.apache.fory.reflect.InstanceFieldAccessors$InstanceAccessor";
+
+ @Test
+ public void testPrivateFieldAccess() throws Exception {
+ PrivateFieldBean bean = new PrivateFieldBean(7);
+ Field field = PrivateFieldBean.class.getDeclaredField("value");
+ FieldAccessor accessor = FieldAccessor.createAccessor(field);
+ Assert.assertEquals(accessor.getInt(bean), 7);
+ accessor.putInt(bean, 9);
+ Assert.assertEquals(bean.value(), 9);
+ }
+
+ @Test
+ public void testPrivateFinalFieldSerialization() {
+ Fory fory =
+ Fory.builder().withXlang(false).withCodegen(false).requireClassRegistration(false).build();
+ PrivateFieldBean result =
+ (PrivateFieldBean) fory.deserialize(fory.serialize(new PrivateFieldBean(13)));
+ Assert.assertEquals(result.value(), 13);
+ }
+
+ @Test
+ public void testNonSerializableNoNoArgSerialization() {
+ if (JDK_MAJOR_VERSION < 25) {
+ return;
+ }
+ Fory fory =
+ Fory.builder().withXlang(false).withCodegen(false).requireClassRegistration(false).build();
+ byte[] bytes = fory.serialize(new NonSerializableNoNoArgBean(5, 13));
+ NonSerializableNoNoArgBean.resetParentConstructorCalls();
+ NonSerializableNoNoArgBean result = (NonSerializableNoNoArgBean) fory.deserialize(bytes);
+ Assert.assertEquals(result.parentValue(), 5);
+ Assert.assertEquals(result.value(), 13);
+ Assert.assertEquals(NonSerializableNoNoArgBean.parentConstructorCalls(), 0);
+ }
+
+ @Test
+ public void testSetFromMapJdkFieldRestore() throws Exception {
+ if (JDK_MAJOR_VERSION < 25) {
+ return;
+ }
+ Fory fory =
+ Fory.builder().withXlang(false).withCodegen(false).requireClassRegistration(false).build();
+ Map backingMap = Collections.synchronizedMap(new HashMap<>());
+ Set set = Collections.newSetFromMap(backingMap);
+ set.add("alpha");
+ set.add("beta");
+
+ Set> result = (Set>) fory.deserialize(fory.serialize(set));
+ Assert.assertEquals(result, set);
+ Field mapField = result.getClass().getDeclaredField("m");
+ Object restoredMap = FieldAccessor.createAccessor(mapField).getObject(result);
+ Assert.assertEquals(restoredMap.getClass().getName(), backingMap.getClass().getName());
+
+ Set> copied = fory.copy(set);
+ Assert.assertEquals(copied, set);
+ Object copiedMap = FieldAccessor.createAccessor(mapField).getObject(copied);
+ Assert.assertEquals(copiedMap.getClass().getName(), backingMap.getClass().getName());
+ }
+
+ @Test
+ public void testCodegenFinalFieldAccess() throws Exception {
+ if (JDK_MAJOR_VERSION < 25) {
+ return;
+ }
+ Fory fory =
+ Fory.builder().withXlang(false).withCodegen(true).requireClassRegistration(false).build();
+ PrivateFieldBean result =
+ (PrivateFieldBean) fory.deserialize(fory.serialize(new PrivateFieldBean(17)));
+ Assert.assertEquals(result.value(), 17);
+
+ Class> serializerClass = serializerClass(fory, PrivateFieldBean.class);
+ Assert.assertTrue((Boolean) Class.class.getMethod("isHidden").invoke(serializerClass));
+ Assert.assertSame(
+ Class.class.getMethod("getNestHost").invoke(serializerClass), PrivateFieldBean.class);
+ assertAccessorField(serializerClass, "value");
+ }
+
+ @Test
+ public void testReflectionFinalWriteDenied() throws Exception {
+ if (JDK_MAJOR_VERSION < 26) {
+ return;
+ }
+ PrivateFieldBean bean = new PrivateFieldBean(29);
+ Field field = PrivateFieldBean.class.getDeclaredField("value");
+ field.setAccessible(true);
+ try {
+ field.setInt(bean, 31);
+ Assert.fail("JDK26 denial mode should reject ordinary final-field reflection writes");
+ } catch (IllegalAccessException | RuntimeException expected) {
+ Assert.assertEquals(bean.value(), 29);
+ }
+ }
+
+ @Test
+ public void testPublicSerializerInExportedPackage() {
+ Fory fory = Fory.builder().withXlang(false).requireClassRegistration(false).build();
+ fory.registerSerializer(PublicSerializerValue.class, PublicSerializerValueSerializer.class);
+ PublicSerializerValue result =
+ (PublicSerializerValue) fory.deserialize(fory.serialize(new PublicSerializerValue(11)));
+ Assert.assertEquals(result.value, 11);
+ }
+
+ private static Class> serializerClass(Fory fory, Class> type) {
+ Serializer> serializer = fory.getTypeResolver().getSerializer(type);
+ if (serializer instanceof LazyInitBeanSerializer) {
+ return ((LazyInitBeanSerializer>) serializer).getGeneratedSerializerClass();
+ }
+ return serializer.getClass();
+ }
+
+ private static void assertAccessorField(Class> serializerClass, String fieldName) {
+ for (Field field : serializerClass.getDeclaredFields()) {
+ if (field.getName().contains(fieldName + "_accessor_")) {
+ Assert.assertEquals(field.getType().getName(), INSTANCE_ACCESSOR);
+ return;
+ }
+ }
+ Assert.fail("Missing generated accessor field for " + fieldName + " in " + serializerClass);
+ }
+}
diff --git a/java/README.md b/java/README.md
index c864cf3753..6b07bbea21 100644
--- a/java/README.md
+++ b/java/README.md
@@ -1,7 +1,7 @@
# Apache Foryâ„¢ Java
[](https://search.maven.org/#search|gav|1|g:"org.apache.fory"%20AND%20a:"fory-core")
-[](https://www.oracle.com/java/)
+[](https://www.oracle.com/java/)
[](https://opensource.org/licenses/Apache-2.0)
Apache Foryâ„¢ Java provides blazingly-fast serialization for the Java ecosystem, delivering up to **170x performance improvement** over traditional frameworks through JIT compilation and zero-copy techniques.
@@ -23,7 +23,7 @@ Apache Foryâ„¢ Java provides blazingly-fast serialization for the Java ecosystem
### Drop-in Replacement
- **100% JDK Serialization Compatible**: Supports `writeObject`/`readObject`/`writeReplace`/`readResolve`/`readObjectNoData`/`Externalizable`
-- **Java 8-24 Support**: Works across all modern Java versions including Java 17+ records
+- **Java 8+ Support**: Works across all modern Java versions including Java 17+ records
- **GraalVM Native Image**: AOT compilation support without reflection configuration
### Advanced Features
@@ -49,7 +49,6 @@ Apache Foryâ„¢ Java provides blazingly-fast serialization for the Java ecosystem
| ------------------- | ------------------------------------- | --------------------------------- |
| **fory-core** | Core serialization engine | `org.apache.fory:fory-core` |
| **fory-format** | Row format and Apache Arrow support | `org.apache.fory:fory-format` |
-| **fory-simd** | SIMD-accelerated array compression | `org.apache.fory:fory-simd` |
| **fory-extensions** | Protobuf support and meta compression | `org.apache.fory:fory-extensions` |
| **fory-test-core** | Testing utilities and data generators | `org.apache.fory:fory-test-core` |
@@ -78,12 +77,6 @@ Apache Foryâ„¢ Java provides blazingly-fast serialization for the Java ecosystem
1.1.0
-
-
- org.apache.fory
- fory-simd
- 1.1.0
-
```
### Gradle
@@ -93,11 +86,25 @@ dependencies {
implementation 'org.apache.fory:fory-core:1.1.0'
// Optional modules
implementation 'org.apache.fory:fory-format:1.1.0'
- implementation 'org.apache.fory:fory-simd:1.1.0'
implementation 'org.apache.fory:fory-extensions:1.1.0'
}
```
+### JDK25+
+
+On JDK25+, open `java.lang.invoke` to Fory. Use `ALL-UNNAMED` when Fory is on
+the classpath:
+
+```bash
+--add-opens=java.base/java.lang.invoke=ALL-UNNAMED
+```
+
+Use the Fory core module name when Fory is on the module path:
+
+```bash
+--add-opens=java.base/java.lang.invoke=org.apache.fory.core
+```
+
## Quick Start
### Basic Usage
diff --git a/java/fory-core/pom.xml b/java/fory-core/pom.xml
index 6bc324f3bb..1dd276218f 100644
--- a/java/fory-core/pom.xml
+++ b/java/fory-core/pom.xml
@@ -39,6 +39,8 @@
88${basedir}/..
+ ${project.build.directory}/multi-release-classes
+ ${project.build.directory}/jdk25-test-classes
@@ -93,6 +95,7 @@
shade
+ falsetrue
@@ -157,11 +160,11 @@
-
+
- inject-java9-module-info
+ inject-multi-release-classespackagerun
@@ -186,36 +189,61 @@
-
+
+
+
+
+
+ jpms-java16
+
+ [16,)
+
+
+ org.apache.maven.plugins
- maven-resources-plugin
- 3.3.1
+ maven-antrun-plugin
+ 3.1.0
- copy-java9-module-info
- prepare-package
+ compile-java16-sources
+ process-classes
- copy-resources
+ run
- ${project.build.outputDirectory}/META-INF/versions/9
-
-
- ${project.build.directory}/jpms-classes/java9
-
- module-info.class
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -224,105 +252,209 @@
- jpms-java16
+ jdk25-multi-release
- [16,)
+ [25,)
+
+ org.apache.maven.plugins
+ maven-source-plugin
+
+
+ attach-sources
+ package
+
+ jar
+
+
+
+ org.apache.maven.pluginsmaven-antrun-plugin3.1.0
- compile-java16-sources
+ compile-jdk25-classes
+ process-classes
+
+ run
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ compile-jdk25-module-infoprocess-classesrun
-
-
-
-
+
-
+
-
+
-
+
- clean-java16-package-classes
- prepare-package
+ prepare-jdk25-test-classes
+ process-test-classesrun
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- inject-java16-classes
- package
+ verify-jdk25-multi-release-jar
+ verifyrun
-
-
-
+
+
+
+
+
+
-
-
-
- org.apache.maven.plugins
- maven-resources-plugin
- 3.3.1
-
- copy-java16-classes
- prepare-package
+ patch-multi-release-source-jar
+ package
- copy-resources
+ run
- ${project.build.outputDirectory}/META-INF/versions/16
-
-
- ${project.build.directory}/jpms-classes/java16
-
- **/*.class
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+ ${fory.jdk25.test.classes}
+
+
@@ -378,5 +510,49 @@
+
+ refresh-module-packages
+
+ [9,)
+
+
+
+
+ org.apache.maven.plugins
+ maven-antrun-plugin
+ 3.1.0
+
+
+ refresh-module-packages
+ package
+
+ run
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/java/fory-core/src/main/java/org/apache/fory/Fory.java b/java/fory-core/src/main/java/org/apache/fory/Fory.java
index a4bf27ae37..4a33285306 100644
--- a/java/fory-core/src/main/java/org/apache/fory/Fory.java
+++ b/java/fory-core/src/main/java/org/apache/fory/Fory.java
@@ -19,7 +19,6 @@
package org.apache.fory;
-import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
@@ -52,7 +51,6 @@
import org.apache.fory.logging.LoggerFactory;
import org.apache.fory.memory.MemoryBuffer;
import org.apache.fory.memory.MemoryUtils;
-import org.apache.fory.platform.AndroidSupport;
import org.apache.fory.resolver.ClassResolver;
import org.apache.fory.resolver.SharedRegistry;
import org.apache.fory.resolver.TypeChecker;
@@ -156,11 +154,13 @@ public Fory(ForyBuilder builder, ClassLoader classLoader, SharedRegistry sharedR
@Override
public void register(Class> cls) {
+ checkRegisterAllowed();
getTypeResolver().register(cls);
}
@Override
public void register(Class> cls, int id) {
+ checkRegisterAllowed();
getTypeResolver().register(cls, Integer.toUnsignedLong(id));
}
@@ -170,6 +170,7 @@ public void register(Class> cls, int id) {
*/
@Override
public void register(Class> cls, String typeName) {
+ checkRegisterAllowed();
int idx = typeName.lastIndexOf('.');
String namespace = "";
if (idx > 0) {
@@ -180,21 +181,25 @@ public void register(Class> cls, String typeName) {
}
public void register(Class> cls, String namespace, String typeName) {
+ checkRegisterAllowed();
getTypeResolver().register(cls, namespace, typeName);
}
@Override
public void register(String className) {
+ checkRegisterAllowed();
getTypeResolver().register(className);
}
@Override
public void register(String className, int classId) {
+ checkRegisterAllowed();
getTypeResolver().register(className, Integer.toUnsignedLong(classId));
}
@Override
public void register(String className, String namespace, String typeName) {
+ checkRegisterAllowed();
getTypeResolver().register(className, namespace, typeName);
}
@@ -204,6 +209,7 @@ public void register(ForyModule module) {
if (installedModules.containsKey(module)) {
return;
}
+ checkRegisterAllowed();
installedModules.put(module, Boolean.TRUE);
try {
module.install(this);
@@ -215,50 +221,59 @@ public void register(ForyModule module) {
@Override
public void registerUnion(Class> cls, int id, Serializer> serializer) {
+ checkRegisterAllowed();
getTypeResolver().registerUnion(cls, Integer.toUnsignedLong(id), serializer);
}
@Override
public void registerUnion(
Class> cls, String namespace, String typeName, Serializer> serializer) {
+ checkRegisterAllowed();
getTypeResolver().registerUnion(cls, namespace, typeName, serializer);
}
@Override
public void registerSerializer(Class type, Class extends Serializer> serializerClass) {
+ checkRegisterAllowed();
getTypeResolver().registerSerializer(type, serializerClass);
}
@Override
public void registerSerializer(Class> type, Serializer> serializer) {
+ checkRegisterAllowed();
getTypeResolver().registerSerializer(type, serializer);
}
@Override
public void registerSerializer(
Class> type, Function> serializerCreator) {
+ checkRegisterAllowed();
getTypeResolver().registerSerializer(type, serializerCreator.apply(typeResolver));
}
@Override
public void registerSerializerAndType(
Class type, Class extends Serializer> serializerClass) {
+ checkRegisterAllowed();
getTypeResolver().registerSerializerAndType(type, serializerClass);
}
@Override
public void registerSerializerAndType(Class> type, Serializer> serializer) {
+ checkRegisterAllowed();
getTypeResolver().registerSerializerAndType(type, serializer);
}
@Override
public void registerSerializerAndType(
Class> type, Function> serializerCreator) {
+ checkRegisterAllowed();
getTypeResolver().registerSerializerAndType(type, serializerCreator.apply(typeResolver));
}
@Override
public void registerSerializerFactory(SerializerFactory serializerFactory) {
+ checkRegisterAllowed();
typeResolver.registerSerializerFactory(serializerFactory);
}
@@ -564,30 +579,20 @@ public T copy(T obj) {
private void serializeToStream(OutputStream outputStream, Consumer function) {
MemoryBuffer buf = getBuffer();
- if (!AndroidSupport.IS_ANDROID && outputStream.getClass() == ByteArrayOutputStream.class) {
- byte[] oldBytes = buf.getHeapMemory(); // Note: This should not be null.
- assert oldBytes != null;
- MemoryUtils.wrap((ByteArrayOutputStream) outputStream, buf);
- function.accept(buf);
- MemoryUtils.wrap(buf, (ByteArrayOutputStream) outputStream);
- buf.pointTo(oldBytes, 0, oldBytes.length);
- resetBuffer();
- } else {
- buf.writerIndex(0);
- function.accept(buf);
- try {
- byte[] bytes = buf.getHeapMemory();
- if (bytes != null) {
- outputStream.write(bytes, 0, buf.writerIndex());
- } else {
- outputStream.write(buf.getBytes(0, buf.writerIndex()));
- }
- outputStream.flush();
- } catch (IOException e) {
- throw new SerializationException(e);
- } finally {
- resetBuffer();
+ buf.writerIndex(0);
+ function.accept(buf);
+ try {
+ byte[] bytes = buf.getHeapMemory();
+ if (bytes != null) {
+ outputStream.write(bytes, 0, buf.writerIndex());
+ } else {
+ outputStream.write(buf.getBytes(0, buf.writerIndex()));
}
+ outputStream.flush();
+ } catch (IOException e) {
+ throw new SerializationException(e);
+ } finally {
+ resetBuffer();
}
}
@@ -671,6 +676,15 @@ SharedRegistry getSharedRegistry() {
return sharedRegistry;
}
+ private void checkRegisterAllowed() {
+ if (typeResolver.isRegistrationFinished()) {
+ throw new ForyException(
+ "Cannot register class/serializer after registration has been frozen. Please register "
+ + "all classes before invoking top-level `serialize/deserialize/copy` methods of "
+ + "Fory.");
+ }
+ }
+
public Config getConfig() {
return config;
}
diff --git a/java/fory-core/src/main/java/org/apache/fory/ThreadLocalFory.java b/java/fory-core/src/main/java/org/apache/fory/ThreadLocalFory.java
index 0b00c719c5..57f0d947dc 100644
--- a/java/fory-core/src/main/java/org/apache/fory/ThreadLocalFory.java
+++ b/java/fory-core/src/main/java/org/apache/fory/ThreadLocalFory.java
@@ -79,10 +79,10 @@ private Fory currentFory() {
@Override
public void registerCallback(Consumer callback) {
synchronized (callbackLock) {
- factoryCallback = factoryCallback.andThen(callback);
synchronized (allFory) {
allFory.keySet().forEach(callback);
}
+ factoryCallback = factoryCallback.andThen(callback);
}
}
diff --git a/java/fory-core/src/main/java/org/apache/fory/builder/AccessorHelper.java b/java/fory-core/src/main/java/org/apache/fory/builder/AccessorHelper.java
index aa76fa7c5b..f2979d5361 100644
--- a/java/fory-core/src/main/java/org/apache/fory/builder/AccessorHelper.java
+++ b/java/fory-core/src/main/java/org/apache/fory/builder/AccessorHelper.java
@@ -43,7 +43,7 @@
/**
* Define accessor helper methods in beanClass's classloader and same package to avoid reflective
- * call overhead. {@link sun.misc.Unsafe} is another method to avoid reflection cost.
+ * call overhead.
*/
public class AccessorHelper {
private static final Logger LOG = LoggerFactory.getLogger(AccessorHelper.class);
diff --git a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java
index 0848c5d2db..ce5bdae3db 100644
--- a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java
+++ b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java
@@ -121,7 +121,7 @@
import org.apache.fory.memory.MemoryBuffer;
import org.apache.fory.meta.TypeExtMeta;
import org.apache.fory.platform.GraalvmSupport;
-import org.apache.fory.platform.UnsafeOps;
+import org.apache.fory.reflect.ObjectInstantiator;
import org.apache.fory.reflect.ReflectionUtils;
import org.apache.fory.reflect.TypeRef;
import org.apache.fory.resolver.ClassResolver;
@@ -220,6 +220,10 @@ public BaseObjectCodecBuilder(TypeRef> beanType, Fory fory, Class> parentSer
descriptorDispatchId = new HashMap<>();
}
+ void setSamePackageAccess(boolean samePackageAccess) {
+ ctx.setSamePackageAccess(samePackageAccess);
+ }
+
// Must be static to be shared across the whole process life.
private static final Map> idGenerator = new ConcurrentHashMap<>();
@@ -275,6 +279,26 @@ protected static T typeResolver(Fory fory, Function functio
return fory.getJITContext().asyncVisitFory(f -> function.apply(f.getTypeResolver()));
}
+ @Override
+ protected void cacheObjectInstantiator(Class> type) {
+ typeResolver.getObjectInstantiator(type);
+ }
+
+ @Override
+ protected Expression getObjectInstantiator(Class> type) {
+ cacheObjectInstantiator(type);
+ return getOrCreateField(
+ false,
+ ObjectInstantiator.class,
+ ctx.newName("objectInstantiator_" + type.getSimpleName()),
+ () ->
+ new Invoke(
+ typeResolverRef,
+ "getObjectInstantiator",
+ TypeRef.of(ObjectInstantiator.class),
+ getClassExpr(type)));
+ }
+
protected boolean needWriteRef(TypeRef> type) {
return typeResolver(r -> r.needToWriteRef(type));
}
@@ -419,8 +443,7 @@ protected void registerJITNotifyCallback() {
* @see CodeGenerator#getClassUniqueId
*/
protected void addCommonImports() {
- ctx.addImports(
- Fory.class, MemoryBuffer.class, WriteContext.class, ReadContext.class, UnsafeOps.class);
+ ctx.addImports(Fory.class, MemoryBuffer.class, WriteContext.class, ReadContext.class);
ctx.addImports(TypeInfo.class, TypeInfoHolder.class, ClassResolver.class);
ctx.addImport(Generated.class);
ctx.addImports(LazyInitBeanSerializer.class, EnumSerializer.class);
@@ -943,6 +966,10 @@ private Expression getOrCreateSerializer(Class> cls, boolean isField) {
// TODO(chaokunyang) add jdk17+ unexported class check.
// non-public class can't be accessed in generated class.
serializerClass = Serializer.class;
+ } else if (isHiddenClass(serializerClass)) {
+ // Hidden generated serializers are represented by their Class object but cannot be named
+ // or loaded from Java source, so generated fields must use the stable Serializer type.
+ serializerClass = Serializer.class;
} else {
ClassLoader beanClassClassLoader = beanClass.getClassLoader();
if (beanClassClassLoader == null) {
@@ -1006,6 +1033,14 @@ private Expression getOrCreateSerializer(Class> cls, boolean isField) {
return serializerRef;
}
+ private static boolean isHiddenClass(Class> cls) {
+ try {
+ return (Boolean) Class.class.getMethod("isHidden").invoke(cls);
+ } catch (ReflectiveOperationException | RuntimeException e) {
+ return false;
+ }
+ }
+
protected Expression getOrCreateStringSerializer() {
return getOrCreateSerializer(String.class);
}
@@ -1728,6 +1763,45 @@ private TypeRef> getMapChunkLocalType(TypeRef> typeRef) {
return TypeRef.of(CodeGenerator.getSourcePublicAccessibleParentClass(rawType));
}
+ private TypeRef> mapChunkLocalType(TypeRef> typeRef, boolean monomorphic) {
+ if (monomorphic || useCollectionSerialization(typeRef) || useMapSerialization(typeRef)) {
+ return getMapChunkLocalType(typeRef);
+ }
+ return OBJECT_TYPE;
+ }
+
+ private Expression mapEntryRead(Expression entry, String methodName, TypeRef> localType) {
+ Expression value = inlineInvoke(entry, methodName, OBJECT_TYPE);
+ if (localType.equals(OBJECT_TYPE)) {
+ return value;
+ }
+ return new Cast(value, localType, "mapEntryValue", true, false);
+ }
+
+ private Expression mapEntryLocal(
+ Expression entry, String methodName, String namePrefix, TypeRef> localType) {
+ if (localType.equals(OBJECT_TYPE)) {
+ return new Invoke(entry, methodName, namePrefix, OBJECT_TYPE);
+ }
+ return new Cast(
+ inlineInvoke(entry, methodName, OBJECT_TYPE), localType, namePrefix, false, false);
+ }
+
+ private Expression writeMapChunkElement(
+ Expression value,
+ Expression buffer,
+ TypeRef> typeRef,
+ Expression serializer,
+ boolean monomorphic) {
+ if (!monomorphic && value.type().equals(OBJECT_TYPE)) {
+ // Dynamic non-container chunks already wrote runtime type info. Keep the local as Object so
+ // raw maps with a value outside the declared generic type do not fail before that serializer
+ // gets the value.
+ return new Invoke(serializer, writeMethodName, writeContextRef(), value);
+ }
+ return serializeForNotNull(value, buffer, typeRef, serializer);
+ }
+
protected Expression writeChunk(
Expression buffer,
Expression entry,
@@ -1739,16 +1813,16 @@ protected Expression writeChunk(
boolean valueMonomorphic = isMonomorphic(valueType);
Class> keyTypeRawType = keyType.getRawType();
Class> valueTypeRawType = valueType.getRawType();
- TypeRef> keyLocalType = keyMonomorphic ? getMapChunkLocalType(keyType) : keyType;
- TypeRef> valueLocalType = valueMonomorphic ? getMapChunkLocalType(valueType) : valueType;
+ TypeRef> keyLocalType = mapChunkLocalType(keyType, keyMonomorphic);
+ TypeRef> valueLocalType = mapChunkLocalType(valueType, valueMonomorphic);
Expression key =
keyMonomorphic
? new Variable("key", keyLocalType)
- : invoke(entry, "getKey", "key", keyType);
+ : mapEntryLocal(entry, "getKey", "key", keyLocalType);
Expression value =
valueMonomorphic
? new Variable("value", valueLocalType)
- : invoke(entry, "getValue", "value", valueType);
+ : mapEntryLocal(entry, "getValue", "value", valueLocalType);
Expression keyTypeExpr =
keyMonomorphic
? getClassExpr(keyTypeRawType)
@@ -1870,9 +1944,11 @@ protected Expression writeChunk(
new While(
Literal.ofBoolean(true),
() -> {
- Expression keyAssign = new Assign(key, invokeInline(entry, "getKey", keyLocalType));
+ // Map.Entry is public but generated codecs may be package peers of non-public key or
+ // value types. Invoke as Object, then cast with this builder's source-access rules.
+ Expression keyAssign = new Assign(key, mapEntryRead(entry, "getKey", keyLocalType));
Expression valueAssign =
- new Assign(value, invokeInline(entry, "getValue", valueLocalType));
+ new Assign(value, mapEntryRead(entry, "getValue", valueLocalType));
Expression breakCondition;
if (keyMonomorphic && valueMonomorphic) {
breakCondition = or(eqNull(key), eqNull(value));
@@ -1896,7 +1972,8 @@ protected Expression writeChunk(
neq(inlineInvoke(key, "getClass", CLASS_TYPE), keyTypeExpr),
neq(inlineInvoke(value, "getClass", CLASS_TYPE), valueTypeExpr));
}
- Expression writeKey = serializeForNotNull(key, buffer, keyType, keySerializer);
+ Expression writeKey =
+ writeMapChunkElement(key, buffer, keyType, keySerializer, keyMonomorphic);
if (trackingKeyRef) {
writeKey =
new If(
@@ -1911,7 +1988,7 @@ protected Expression writeChunk(
writeKey);
}
Expression writeValue =
- serializeForNotNull(value, buffer, valueType, valueSerializer);
+ writeMapChunkElement(value, buffer, valueType, valueSerializer, valueMonomorphic);
if (trackingValueRef) {
writeValue =
new If(
@@ -2242,9 +2319,11 @@ protected boolean hasCompatibleCollectionArrayRead(Descriptor descriptor) {
return CompatibleSerializer.hasCompatibleCollectionArrayRead(typeResolver, descriptor);
}
- protected TypeRef> compatibleReadTargetTypeRef(Descriptor descriptor) {
+ protected TypeRef> compatibleLocalTypeRef(Descriptor descriptor) {
+ // Compatible collection/array metadata may keep the peer wire shape in TypeRef. Generated
+ // setters must cast to the local carrier that readCompatibleCollectionArrayField materializes.
return descriptor.getField() == null
- ? descriptor.getTypeRef()
+ ? TypeRef.of(descriptor.getRawType())
: TypeRef.of(descriptor.getField().getGenericType());
}
diff --git a/java/fory-core/src/main/java/org/apache/fory/builder/CodecBuilder.java b/java/fory-core/src/main/java/org/apache/fory/builder/CodecBuilder.java
index f786b10c57..68db4af0fc 100644
--- a/java/fory-core/src/main/java/org/apache/fory/builder/CodecBuilder.java
+++ b/java/fory-core/src/main/java/org/apache/fory/builder/CodecBuilder.java
@@ -36,6 +36,7 @@
import java.lang.invoke.MethodHandle;
import java.lang.reflect.Field;
+import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;
@@ -43,6 +44,7 @@
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
+import org.apache.fory.codegen.Code;
import org.apache.fory.codegen.CodegenContext;
import org.apache.fory.codegen.Expression;
import org.apache.fory.codegen.Expression.Cast;
@@ -57,9 +59,10 @@
import org.apache.fory.memory.NativeByteOrder;
import org.apache.fory.platform.GraalvmSupport;
import org.apache.fory.platform.JdkVersion;
-import org.apache.fory.platform.UnsafeOps;
-import org.apache.fory.reflect.ObjectCreator;
-import org.apache.fory.reflect.ObjectCreators;
+import org.apache.fory.reflect.FieldAccessor;
+import org.apache.fory.reflect.InstanceFieldAccessors.InstanceAccessor;
+import org.apache.fory.reflect.ObjectInstantiator;
+import org.apache.fory.reflect.ObjectInstantiators;
import org.apache.fory.reflect.ReflectionUtils;
import org.apache.fory.reflect.TypeRef;
import org.apache.fory.resolver.TypeInfo;
@@ -198,8 +201,8 @@ protected Reference getRecordCtrHandle() {
}
protected Expression buildDefaultComponentsArray() {
- return new StaticInvoke(
- UnsafeOps.class, "copyObjectArray", OBJECT_ARRAY_TYPE, recordComponentDefaultValues);
+ return new Cast(
+ new Invoke(recordComponentDefaultValues, "clone", OBJECT_TYPE), OBJECT_ARRAY_TYPE);
}
/** Returns an expression that get field value from bean. */
@@ -214,7 +217,7 @@ protected Expression getFieldValue(Expression inputBeanExpr, Descriptor descript
if (isRecord) {
return getRecordFieldValue(inputBeanExpr, descriptor);
}
- if (duplicatedFields.contains(fieldName) || !Modifier.isPublic(beanClass.getModifiers())) {
+ if (duplicatedFields.contains(fieldName) || !sourcePublicAccessible(beanClass)) {
return unsafeAccessField(inputBeanExpr, beanClass, descriptor);
}
if (!sourcePublicAccessible(rawType)) {
@@ -310,30 +313,37 @@ private Expression reflectAccessField(
private Expression unsafeAccessField(
Expression inputObject, Class> cls, Descriptor descriptor) {
String fieldName = descriptor.getName();
- Expression fieldOffsetExpr = getFieldOffset(cls, descriptor);
+ if (JdkVersion.MAJOR_VERSION >= 25) {
+ Reference fieldAccessor = getFieldAccessor(descriptor);
+ boolean fieldNullable = fieldNullable(descriptor);
+ if (descriptor.getTypeRef().isPrimitive()) {
+ Preconditions.checkArgument(!fieldNullable);
+ TypeRef> returnType = descriptor.getTypeRef();
+ String funcName = "get" + StringUtils.capitalize(descriptor.getRawType().toString());
+ return new Invoke(fieldAccessor, funcName, returnType, false, inputObject);
+ } else {
+ Invoke getObj =
+ new Invoke(fieldAccessor, "getObject", OBJECT_TYPE, fieldNullable, inputObject);
+ return tryCastIfPublic(getObj, descriptor.getTypeRef(), fieldName);
+ }
+ }
+ Expression fieldOffsetExpr = fieldOffsetExpr(cls, descriptor);
boolean fieldNullable = fieldNullable(descriptor);
if (descriptor.getTypeRef().isPrimitive()) {
- // ex: UnsafeOps.getFloat(obj, fieldOffset)
+ // ex: Unsafe.getFloat(obj, fieldOffset)
Preconditions.checkArgument(!fieldNullable);
TypeRef> returnType = descriptor.getTypeRef();
String funcName = "get" + StringUtils.capitalize(descriptor.getRawType().toString());
- return new StaticInvoke(
- UnsafeOps.class, funcName, returnType, false, inputObject, fieldOffsetExpr);
+ return unsafeInvoke(funcName, returnType, false, inputObject, fieldOffsetExpr);
} else {
- // ex: UnsafeOps.getObject(obj, fieldOffset)
- StaticInvoke getObj =
- new StaticInvoke(
- UnsafeOps.class,
- "getObject",
- OBJECT_TYPE,
- fieldNullable,
- inputObject,
- fieldOffsetExpr);
+ // ex: Unsafe.getObject(obj, fieldOffset)
+ Invoke getObj =
+ unsafeInvoke("getObject", OBJECT_TYPE, fieldNullable, inputObject, fieldOffsetExpr);
return tryCastIfPublic(getObj, descriptor.getTypeRef(), fieldName);
}
}
- private Expression getFieldOffset(Class> cls, Descriptor descriptor) {
+ private Expression fieldOffsetExpr(Class> cls, Descriptor descriptor) {
Field field = descriptor.getField();
String fieldName = descriptor.getName();
// Use Field in case the class has duplicate field name as `fieldName`.
@@ -345,16 +355,106 @@ private Expression getFieldOffset(Class> cls, Descriptor descriptor) {
() -> {
Expression classExpr = beanClassExpr(field.getDeclaringClass());
new Invoke(classExpr, "getDeclaredField", TypeRef.of(Field.class));
- Expression reflectFieldRef = getReflectField(cls, field, false);
- return new StaticInvoke(
- UnsafeOps.class, "objectFieldOffset", PRIMITIVE_LONG_TYPE, reflectFieldRef)
- .inline();
+ Expression reflectFieldRef = getReflectField(field.getDeclaringClass(), field, false);
+ return unsafeInvoke("objectFieldOffset", PRIMITIVE_LONG_TYPE, reflectFieldRef).inline();
});
} else {
- long fieldOffset = ReflectionUtils.getFieldOffset(field);
- Preconditions.checkArgument(fieldOffset != -1);
- return Literal.ofLong(fieldOffset);
+ return Literal.ofLong(UnsafeCodegenSupport.objectFieldOffset(field));
+ }
+ }
+
+ private Reference getUnsafe() {
+ String fieldName = "_unsafe_";
+ Reference fieldRef = fieldMap.get(fieldName);
+ if (fieldRef == null) {
+ String uniqueFieldName = ctx.newName(fieldName);
+ ctx.addField(
+ true,
+ true,
+ UnsafeCodegenSupport.unsafeTypeName(),
+ uniqueFieldName,
+ new UnsafeInitExpression());
+ fieldRef = new Reference(uniqueFieldName, OBJECT_TYPE);
+ fieldMap.put(fieldName, fieldRef);
+ }
+ return fieldRef;
+ }
+
+ private Invoke unsafeInvoke(String functionName, TypeRef> returnType, Expression... arguments) {
+ return unsafeInvoke(functionName, returnType, false, arguments);
+ }
+
+ private Invoke unsafeInvoke(
+ String functionName, TypeRef> returnType, boolean returnNullable, Expression... arguments) {
+ return new Invoke(
+ getUnsafe(),
+ functionName,
+ "",
+ returnType,
+ returnNullable,
+ "allocateInstance".equals(functionName),
+ arguments);
+ }
+
+ private Invoke unsafeInvoke(String functionName, Expression... arguments) {
+ return new Invoke(getUnsafe(), functionName, "", PRIMITIVE_VOID_TYPE, false, false, arguments);
+ }
+
+ private static final class UnsafeInitExpression extends Expression.AbstractExpression {
+ private UnsafeInitExpression() {
+ super(new Expression[0]);
+ }
+
+ @Override
+ public TypeRef> type() {
+ return OBJECT_TYPE;
+ }
+
+ @Override
+ public Code.ExprCode doGenCode(CodegenContext ctx) {
+ return new Code.ExprCode(
+ Code.LiteralValue.FalseLiteral,
+ Code.exprValue(Object.class, UnsafeCodegenSupport.unsafeInitCode()));
+ }
+ }
+
+ private Reference getFieldAccessor(Descriptor descriptor) {
+ Field field = descriptor.getField();
+ String fieldName = descriptor.getName();
+ String fieldAccessorName =
+ (duplicatedFields.contains(fieldName)
+ ? field.getDeclaringClass().getName().replaceAll("\\.|\\$", "_") + "_"
+ : "")
+ + fieldName
+ + "_accessor_";
+ if (JdkVersion.MAJOR_VERSION >= 25) {
+ // JDK25+ field writes go through the VarHandle-backed instance accessor. Keep the generated
+ // static field typed as the concrete final accessor so hot-path putX calls do not pay a
+ // FieldAccessor virtual dispatch. FieldAccessor.createAccessor still owns platform dispatch;
+ // this one-time cast happens only during generated-class initialization.
+ return getOrCreateField(
+ true,
+ InstanceAccessor.class,
+ fieldAccessorName,
+ () ->
+ new Cast(
+ new StaticInvoke(
+ FieldAccessor.class,
+ "createAccessor",
+ TypeRef.of(FieldAccessor.class),
+ getReflectField(field.getDeclaringClass(), field, false)),
+ TypeRef.of(InstanceAccessor.class)));
}
+ return getOrCreateField(
+ true,
+ FieldAccessor.class,
+ fieldAccessorName,
+ () ->
+ new StaticInvoke(
+ FieldAccessor.class,
+ "createAccessor",
+ TypeRef.of(FieldAccessor.class),
+ getReflectField(field.getDeclaringClass(), field, false)));
}
/**
@@ -366,6 +466,8 @@ private Expression getFieldOffset(Class> cls, Descriptor descriptor) {
/** Returns an expression that set field value to bean. */
protected Expression setFieldValue(Expression bean, Descriptor d, Expression value) {
String fieldName = d.getName();
+ TypeRef> memberType = memberTypeRef(d);
+ Class> memberRawType = getRawType(memberType);
if (value instanceof Inlineable) {
((Inlineable) value).inline();
}
@@ -374,22 +476,22 @@ protected Expression setFieldValue(Expression bean, Descriptor d, Expression val
}
if (!d.isFinalField()
&& Modifier.isPublic(d.getModifiers())
- && Modifier.isPublic(d.getRawType().getModifiers())) {
- if (!d.getRawType().isAssignableFrom(value.type().getRawType())) {
- value = tryInlineCast(value, d.getTypeRef());
+ && sourcePublicAccessible(memberRawType)) {
+ if (!memberRawType.isAssignableFrom(value.type().getRawType())) {
+ value = tryInlineCast(value, memberType);
}
return new Expression.SetField(bean, fieldName, value);
} else if (d.getWriteMethod() != null && Modifier.isPublic(d.getWriteMethod().getModifiers())) {
- if (!d.getRawType().isAssignableFrom(value.type().getRawType())) {
- value = tryInlineCast(value, d.getTypeRef());
+ if (!memberRawType.isAssignableFrom(value.type().getRawType())) {
+ value = tryInlineCast(value, memberType);
}
return new Invoke(bean, d.getWriteMethod().getName(), value);
} else {
if (!d.isFinalField() && !Modifier.isPrivate(d.getModifiers())) {
if (AccessorHelper.defineSetter(d.getField())) {
Class> accessorClass = AccessorHelper.getAccessorClass(d.getField());
- if (!d.getRawType().isAssignableFrom(value.type().getRawType())) {
- value = tryInlineCast(value, d.getTypeRef());
+ if (!memberRawType.isAssignableFrom(value.type().getRawType())) {
+ value = tryInlineCast(value, memberType);
}
return new StaticInvoke(
accessorClass, d.getName(), PRIMITIVE_VOID_TYPE, false, bean, value);
@@ -398,8 +500,8 @@ protected Expression setFieldValue(Expression bean, Descriptor d, Expression val
if (d.getWriteMethod() != null && !Modifier.isPrivate(d.getWriteMethod().getModifiers())) {
if (AccessorHelper.defineSetter(d.getWriteMethod())) {
Class> accessorClass = AccessorHelper.getAccessorClass(d.getWriteMethod());
- if (!d.getRawType().isAssignableFrom(value.type().getRawType())) {
- value = tryInlineCast(value, d.getTypeRef());
+ if (!memberRawType.isAssignableFrom(value.type().getRawType())) {
+ value = tryInlineCast(value, memberType);
}
return new StaticInvoke(
accessorClass, d.getWriteMethod().getName(), PRIMITIVE_VOID_TYPE, false, bean, value);
@@ -409,6 +511,18 @@ protected Expression setFieldValue(Expression bean, Descriptor d, Expression val
}
}
+ private TypeRef> memberTypeRef(Descriptor descriptor) {
+ Field field = descriptor.getField();
+ if (field != null) {
+ return TypeRef.of(field.getGenericType());
+ }
+ Method method = descriptor.getWriteMethod();
+ if (method != null) {
+ return TypeRef.of(method.getGenericParameterTypes()[0]);
+ }
+ return descriptor.getTypeRef();
+ }
+
/**
* Returns an expression that set field value to bean using reflection.
*/
@@ -425,14 +539,24 @@ private Expression reflectSetField(Expression bean, Field field, Expression valu
*/
private Expression unsafeSetField(Expression bean, Descriptor descriptor, Expression value) {
TypeRef> fieldType = descriptor.getTypeRef();
+ if (JdkVersion.MAJOR_VERSION >= 25) {
+ Reference fieldAccessor = getFieldAccessor(descriptor);
+ if (descriptor.getTypeRef().isPrimitive()) {
+ Preconditions.checkArgument(getRawType(value.type()) == getRawType(fieldType));
+ String funcName = "put" + StringUtils.capitalize(getRawType(fieldType).toString());
+ return new Invoke(fieldAccessor, funcName, bean, value);
+ } else {
+ return new Invoke(fieldAccessor, "putObject", bean, value);
+ }
+ }
// Use Field in case the class has duplicate field name as `fieldName`.
- Expression fieldOffsetExpr = getFieldOffset(beanClass, descriptor);
+ Expression fieldOffsetExpr = fieldOffsetExpr(beanClass, descriptor);
if (descriptor.getTypeRef().isPrimitive()) {
Preconditions.checkArgument(getRawType(value.type()) == getRawType(fieldType));
String funcName = "put" + StringUtils.capitalize(getRawType(fieldType).toString());
- return new StaticInvoke(UnsafeOps.class, funcName, bean, fieldOffsetExpr, value);
+ return unsafeInvoke(funcName, bean, fieldOffsetExpr, value);
} else {
- return new StaticInvoke(UnsafeOps.class, "putObject", bean, fieldOffsetExpr, value);
+ return unsafeInvoke("putObject", bean, fieldOffsetExpr, value);
}
}
@@ -444,7 +568,11 @@ private Reference getReflectField(Class> cls, Field field, boolean setAccessib
String fieldName = field.getName();
String fieldRefName;
if (duplicatedFields.contains(fieldName)) {
- fieldRefName = cls.getName().replaceAll("\\.|\\$", "_") + "_" + fieldName + "_Field";
+ fieldRefName =
+ field.getDeclaringClass().getName().replaceAll("\\.|\\$", "_")
+ + "_"
+ + fieldName
+ + "_Field";
} else {
fieldRefName = fieldName + "_Field";
}
@@ -454,7 +582,11 @@ private Reference getReflectField(Class> cls, Field field, boolean setAccessib
fieldRefName,
() -> {
TypeRef fieldTypeRef = TypeRef.of(Field.class);
- Expression classExpr = beanClassExpr(field.getDeclaringClass());
+ Class> declaringClass = field.getDeclaringClass();
+ Expression classExpr =
+ staticClassFieldExpr(
+ declaringClass,
+ declaringClass.getName().replaceAll("\\.|\\$", "_") + "__class__");
Expression fieldExpr;
if (GraalvmSupport.isGraalBuildTime()) {
fieldExpr =
@@ -488,28 +620,37 @@ protected Reference getOrCreateField(
/** Returns an Expression that create a new java object of type {@link CodecBuilder#beanClass}. */
protected Expression newBean() {
// TODO allow default access-level class.
- if (sourcePublicAccessible(beanClass)) {
+ if (sourcePublicAccessible(beanClass)
+ && (JdkVersion.MAJOR_VERSION < 25
+ || ReflectionUtils.hasPublicNoArgConstructor(beanClass))) {
return new Expression.NewInstance(beanType);
} else {
- if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE && JdkVersion.MAJOR_VERSION >= 25) {
- ObjectCreators.getObjectCreator(beanClass); // trigger cache
- return new Invoke(getObjectCreator(beanClass), "newInstance", OBJECT_TYPE);
+ if (JdkVersion.MAJOR_VERSION >= 25) {
+ cacheObjectInstantiator(beanClass); // trigger cache
+ Invoke newInstance =
+ new Invoke(getObjectInstantiator(beanClass), "newInstance", OBJECT_TYPE);
+ return sourcePublicAccessible(beanClass) ? new Cast(newInstance, beanType) : newInstance;
}
- return new StaticInvoke(UnsafeOps.class, "newInstance", OBJECT_TYPE, beanClassExpr());
+ Invoke newInstance = unsafeInvoke("allocateInstance", OBJECT_TYPE, beanClassExpr());
+ return sourcePublicAccessible(beanClass) ? new Cast(newInstance, beanType) : newInstance;
}
}
- protected Expression getObjectCreator(Class> type) {
- ObjectCreators.getObjectCreator(type); // trigger cache
+ protected void cacheObjectInstantiator(Class> type) {
+ ObjectInstantiators.getObjectInstantiator(type);
+ }
+
+ protected Expression getObjectInstantiator(Class> type) {
+ cacheObjectInstantiator(type);
return getOrCreateField(
true,
- ObjectCreator.class,
- ctx.newName("objectCreator_" + type.getSimpleName()),
+ ObjectInstantiator.class,
+ ctx.newName("objectInstantiator_" + type.getSimpleName()),
() ->
new StaticInvoke(
- ObjectCreators.class,
- "getObjectCreator",
- TypeRef.of(ObjectCreator.class),
+ ObjectInstantiators.class,
+ "getObjectInstantiator",
+ TypeRef.of(ObjectInstantiator.class),
staticBeanClassExpr()));
}
@@ -594,50 +735,49 @@ private StaticInvoke inlineReflectionUtilsInvoke(
/** Build unsafePut operation. */
protected Expression unsafePut(Expression base, Expression pos, Expression value) {
- return new StaticInvoke(UnsafeOps.class, "putByte", base, pos, value);
+ return unsafeInvoke("putByte", base, pos, value);
}
protected Expression unsafePutBoolean(Expression base, Expression pos, Expression value) {
- return new StaticInvoke(UnsafeOps.class, "putBoolean", base, pos, value);
+ return unsafeInvoke("putBoolean", base, pos, value);
}
protected Expression unsafePutChar(Expression base, Expression pos, Expression value) {
- return new StaticInvoke(UnsafeOps.class, "putChar", base, pos, value);
+ return unsafeInvoke("putChar", base, pos, value);
}
protected Expression unsafePutShort(Expression base, Expression pos, Expression value) {
- return new StaticInvoke(UnsafeOps.class, "putShort", base, pos, value);
+ return unsafeInvoke("putShort", base, pos, value);
}
protected Expression unsafePutInt(Expression base, Expression pos, Expression value) {
- return new StaticInvoke(UnsafeOps.class, "putInt", base, pos, value);
+ return unsafeInvoke("putInt", base, pos, value);
}
protected Expression unsafePutLong(Expression base, Expression pos, Expression value) {
- return new StaticInvoke(UnsafeOps.class, "putLong", base, pos, value);
+ return unsafeInvoke("putLong", base, pos, value);
}
protected Expression unsafePutFloat(Expression base, Expression pos, Expression value) {
- return new StaticInvoke(UnsafeOps.class, "putFloat", base, pos, value);
+ return unsafeInvoke("putFloat", base, pos, value);
}
/** Build unsafePutDouble operation. */
protected Expression unsafePutDouble(Expression base, Expression pos, Expression value) {
- return new StaticInvoke(UnsafeOps.class, "putDouble", base, pos, value);
+ return unsafeInvoke("putDouble", base, pos, value);
}
/** Build unsafeGet operation. */
protected Expression unsafeGet(Expression base, Expression pos) {
- return new StaticInvoke(UnsafeOps.class, "getByte", PRIMITIVE_BYTE_TYPE, base, pos);
+ return unsafeInvoke("getByte", PRIMITIVE_BYTE_TYPE, base, pos);
}
protected Expression unsafeGetBoolean(Expression base, Expression pos) {
- return new StaticInvoke(UnsafeOps.class, "getBoolean", PRIMITIVE_BOOLEAN_TYPE, base, pos);
+ return unsafeInvoke("getBoolean", PRIMITIVE_BOOLEAN_TYPE, base, pos);
}
protected Expression unsafeGetChar(Expression base, Expression pos) {
- StaticInvoke expr =
- new StaticInvoke(UnsafeOps.class, "getChar", PRIMITIVE_CHAR_TYPE, base, pos);
+ Inlineable expr = unsafeInvoke("getChar", PRIMITIVE_CHAR_TYPE, base, pos);
if (!NativeByteOrder.IS_LITTLE_ENDIAN) {
expr = new StaticInvoke(Character.class, "reverseBytes", PRIMITIVE_CHAR_TYPE, expr.inline());
}
@@ -645,8 +785,7 @@ protected Expression unsafeGetChar(Expression base, Expression pos) {
}
protected Expression unsafeGetShort(Expression base, Expression pos) {
- StaticInvoke expr =
- new StaticInvoke(UnsafeOps.class, "getShort", PRIMITIVE_SHORT_TYPE, base, pos);
+ Inlineable expr = unsafeInvoke("getShort", PRIMITIVE_SHORT_TYPE, base, pos);
if (!NativeByteOrder.IS_LITTLE_ENDIAN) {
expr = new StaticInvoke(Short.class, "reverseBytes", PRIMITIVE_SHORT_TYPE, expr.inline());
}
@@ -654,7 +793,7 @@ protected Expression unsafeGetShort(Expression base, Expression pos) {
}
protected Expression unsafeGetInt(Expression base, Expression pos) {
- StaticInvoke expr = new StaticInvoke(UnsafeOps.class, "getInt", PRIMITIVE_INT_TYPE, base, pos);
+ Inlineable expr = unsafeInvoke("getInt", PRIMITIVE_INT_TYPE, base, pos);
if (!NativeByteOrder.IS_LITTLE_ENDIAN) {
expr = new StaticInvoke(Integer.class, "reverseBytes", PRIMITIVE_INT_TYPE, expr.inline());
}
@@ -662,8 +801,7 @@ protected Expression unsafeGetInt(Expression base, Expression pos) {
}
protected Expression unsafeGetLong(Expression base, Expression pos) {
- StaticInvoke expr =
- new StaticInvoke(UnsafeOps.class, "getLong", PRIMITIVE_LONG_TYPE, base, pos);
+ Inlineable expr = unsafeInvoke("getLong", PRIMITIVE_LONG_TYPE, base, pos);
if (!NativeByteOrder.IS_LITTLE_ENDIAN) {
expr = new StaticInvoke(Long.class, "reverseBytes", PRIMITIVE_LONG_TYPE, expr.inline());
}
@@ -671,7 +809,7 @@ protected Expression unsafeGetLong(Expression base, Expression pos) {
}
protected Expression unsafeGetFloat(Expression base, Expression pos) {
- StaticInvoke expr = new StaticInvoke(UnsafeOps.class, "getInt", PRIMITIVE_INT_TYPE, base, pos);
+ Inlineable expr = unsafeInvoke("getInt", PRIMITIVE_INT_TYPE, base, pos);
if (!NativeByteOrder.IS_LITTLE_ENDIAN) {
expr = new StaticInvoke(Integer.class, "reverseBytes", PRIMITIVE_INT_TYPE, expr.inline());
}
@@ -679,8 +817,7 @@ protected Expression unsafeGetFloat(Expression base, Expression pos) {
}
protected Expression unsafeGetDouble(Expression base, Expression pos) {
- StaticInvoke expr =
- new StaticInvoke(UnsafeOps.class, "getLong", PRIMITIVE_LONG_TYPE, base, pos);
+ Inlineable expr = unsafeInvoke("getLong", PRIMITIVE_LONG_TYPE, base, pos);
if (!NativeByteOrder.IS_LITTLE_ENDIAN) {
expr = new StaticInvoke(Long.class, "reverseBytes", PRIMITIVE_LONG_TYPE, expr.inline());
}
diff --git a/java/fory-core/src/main/java/org/apache/fory/builder/CodecUtils.java b/java/fory-core/src/main/java/org/apache/fory/builder/CodecUtils.java
index c9092f6723..2361179ddb 100644
--- a/java/fory-core/src/main/java/org/apache/fory/builder/CodecUtils.java
+++ b/java/fory-core/src/main/java/org/apache/fory/builder/CodecUtils.java
@@ -19,7 +19,6 @@
package org.apache.fory.builder;
-import java.util.Collections;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.fory.Fory;
@@ -27,7 +26,10 @@
import org.apache.fory.codegen.CompileUnit;
import org.apache.fory.collection.Tuple3;
import org.apache.fory.meta.TypeDef;
+import org.apache.fory.platform.AndroidSupport;
import org.apache.fory.platform.GraalvmSupport;
+import org.apache.fory.platform.JdkVersion;
+import org.apache.fory.reflect.ReflectionUtils;
import org.apache.fory.reflect.TypeRef;
import org.apache.fory.resolver.TypeResolver;
import org.apache.fory.serializer.Serializer;
@@ -118,12 +120,6 @@ public static Class extends Serializer> loadOrGenCompatibleLayerCodecClass
@SuppressWarnings("unchecked")
static Class extends Serializer> loadOrGenCodecClass(
Class beanClass, Fory fory, BaseObjectCodecBuilder codecBuilder) {
- // use genCodeFunc to avoid gen code repeatedly
- CompileUnit compileUnit =
- new CompileUnit(
- CodeGenerator.getPackage(beanClass),
- codecBuilder.codecClassName(beanClass),
- codecBuilder::genCode);
CodeGenerator codeGenerator;
ClassLoader beanClassClassLoader =
beanClass.getClassLoader() == null
@@ -134,15 +130,40 @@ static Class extends Serializer> loadOrGenCodecClass(
}
TypeResolver typeResolver = fory.getTypeResolver();
codeGenerator = getCodeGenerator(beanClassClassLoader, typeResolver);
- ClassLoader classLoader =
- codeGenerator.compile(
- Collections.singletonList(compileUnit), compileState -> compileState.lock.lock());
- String className = codecBuilder.codecQualifiedClassName(beanClass);
+ Class> neighborClass = codecNeighbor(beanClass, beanClassClassLoader);
+ codecBuilder.setSamePackageAccess(neighborClass != null);
+ // use genCodeFunc to avoid gen code repeatedly
+ CompileUnit compileUnit =
+ new CompileUnit(
+ CodeGenerator.getPackage(beanClass),
+ codecBuilder.codecClassName(beanClass),
+ codecBuilder::genCode,
+ neighborClass);
+ return (Class extends Serializer>)
+ codeGenerator.compileAndLoad(compileUnit, compileState -> compileState.lock.lock());
+ }
+
+ private static Class> codecNeighbor(Class> beanClass, ClassLoader beanClassClassLoader) {
+ // Hidden generated serializers are only a JDK25+ path. JDK8-24 keeps the existing unsafe-backed
+ // field/object access strategy, so broadening this to Java15+ adds complexity without removing
+ // unsafe from those runtimes.
+ if (AndroidSupport.IS_ANDROID
+ || JdkVersion.MAJOR_VERSION < 25
+ || beanClass.getClassLoader() == null) {
+ return null;
+ }
+ if (!CodeGenerator.getPackage(beanClass).equals(ReflectionUtils.getPackage(beanClass))) {
+ return null;
+ }
try {
- return (Class extends Serializer>) classLoader.loadClass(className);
+ // A generated serializer defined in the bean loader must resolve Fory runtime classes there.
+ if (beanClassClassLoader.loadClass(Fory.class.getName()) == Fory.class) {
+ return beanClass;
+ }
} catch (ClassNotFoundException e) {
- throw new IllegalStateException("Impossible because we just compiled class", e);
+ // The composed-loader path remains the owner when the bean loader cannot see Fory directly.
}
+ return null;
}
private static CodeGenerator getCodeGenerator(
diff --git a/java/fory-core/src/main/java/org/apache/fory/builder/CompatibleCodecBuilder.java b/java/fory-core/src/main/java/org/apache/fory/builder/CompatibleCodecBuilder.java
index 3b44448db5..45006d2ff7 100644
--- a/java/fory-core/src/main/java/org/apache/fory/builder/CompatibleCodecBuilder.java
+++ b/java/fory-core/src/main/java/org/apache/fory/builder/CompatibleCodecBuilder.java
@@ -295,6 +295,12 @@ protected Expression newBean() {
Expression.ListExpression setDefaultsExpr = new Expression.ListExpression();
setDefaultsExpr.add(bean);
+ addDefaultValueSetters(setDefaultsExpr, bean);
+ setDefaultsExpr.add(bean);
+ return setDefaultsExpr;
+ }
+
+ private void addDefaultValueSetters(Expression.ListExpression expressions, Expression bean) {
Map descriptors = Descriptor.getAllDescriptorsMap(beanClass);
for (DefaultValueUtils.DefaultValueField defaultField : defaultValueFields) {
Object defaultValue = defaultField.getDefaultValue();
@@ -322,9 +328,7 @@ protected Expression newBean() {
return new Expression.Cast(expr, typeRef);
});
}
- setDefaultsExpr.add(super.setFieldValue(bean, descriptor, defaultValueExpr));
+ expressions.add(super.setFieldValue(bean, descriptor, defaultValueExpr));
}
- setDefaultsExpr.add(bean);
- return setDefaultsExpr;
}
}
diff --git a/java/fory-core/src/main/java/org/apache/fory/builder/ObjectCodecBuilder.java b/java/fory-core/src/main/java/org/apache/fory/builder/ObjectCodecBuilder.java
index 5705592d79..edb7eccb0e 100644
--- a/java/fory-core/src/main/java/org/apache/fory/builder/ObjectCodecBuilder.java
+++ b/java/fory-core/src/main/java/org/apache/fory/builder/ObjectCodecBuilder.java
@@ -26,8 +26,12 @@
import static org.apache.fory.collection.Collections.ofHashSet;
import static org.apache.fory.type.TypeUtils.OBJECT_ARRAY_TYPE;
import static org.apache.fory.type.TypeUtils.OBJECT_TYPE;
+import static org.apache.fory.type.TypeUtils.PRIMITIVE_BOOLEAN_TYPE;
import static org.apache.fory.type.TypeUtils.PRIMITIVE_BYTE_ARRAY_TYPE;
import static org.apache.fory.type.TypeUtils.PRIMITIVE_BYTE_TYPE;
+import static org.apache.fory.type.TypeUtils.PRIMITIVE_CHAR_TYPE;
+import static org.apache.fory.type.TypeUtils.PRIMITIVE_DOUBLE_TYPE;
+import static org.apache.fory.type.TypeUtils.PRIMITIVE_FLOAT_TYPE;
import static org.apache.fory.type.TypeUtils.PRIMITIVE_INT_TYPE;
import static org.apache.fory.type.TypeUtils.PRIMITIVE_LONG_TYPE;
import static org.apache.fory.type.TypeUtils.PRIMITIVE_SHORT_TYPE;
@@ -40,12 +44,14 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import org.apache.fory.Fory;
import org.apache.fory.codegen.Code;
import org.apache.fory.codegen.CodegenContext;
import org.apache.fory.codegen.Expression;
+import org.apache.fory.codegen.Expression.Cast;
import org.apache.fory.codegen.Expression.Inlineable;
import org.apache.fory.codegen.Expression.Invoke;
import org.apache.fory.codegen.Expression.ListExpression;
@@ -58,8 +64,7 @@
import org.apache.fory.logging.Logger;
import org.apache.fory.logging.LoggerFactory;
import org.apache.fory.meta.TypeDef;
-import org.apache.fory.platform.UnsafeOps;
-import org.apache.fory.reflect.ObjectCreators;
+import org.apache.fory.platform.JdkVersion;
import org.apache.fory.reflect.TypeRef;
import org.apache.fory.serializer.ObjectSerializer;
import org.apache.fory.type.BFloat16;
@@ -251,20 +256,78 @@ protected int getNumPrimitiveFields(List> primitiveGroups) {
private List serializePrimitivesUnCompressed(
Expression bean, Expression buffer, List> primitiveGroups, int totalSize) {
List expressions = new ArrayList<>();
- int numPrimitiveFields = getNumPrimitiveFields(primitiveGroups);
- Literal totalSizeLiteral = new Literal(totalSize, PRIMITIVE_INT_TYPE);
- // After this grow, following writes can be unsafe without checks.
+ Literal totalSizeLiteral = Literal.ofInt(totalSize);
+ // After this grow, following writes can use unchecked low-level access.
expressions.add(new Invoke(buffer, "grow", totalSizeLiteral));
- // Must grow first, otherwise may get invalid address.
- Expression base = new Invoke(buffer, "getHeapMemory", "base", PRIMITIVE_BYTE_ARRAY_TYPE);
- Expression writerAddr =
- new Invoke(buffer, "_unsafeWriterAddress", "writerAddr", PRIMITIVE_LONG_TYPE);
- expressions.add(base);
- expressions.add(writerAddr);
- int acc = 0;
+ PrimitiveWriteAccess access;
+ if (useIndexedAccess()) {
+ Expression writerIndex = new Invoke(buffer, "writerIndex", "writerIndex", PRIMITIVE_INT_TYPE);
+ expressions.add(writerIndex);
+ access = new BufferWriteAccess(buffer, writerIndex);
+ } else {
+ // Must grow first, otherwise may get invalid address.
+ Expression base = new Invoke(buffer, "getHeapMemory", "base", PRIMITIVE_BYTE_ARRAY_TYPE);
+ Expression writerAddr =
+ new Invoke(buffer, "_unsafeWriterAddress", "writerAddr", PRIMITIVE_LONG_TYPE);
+ expressions.add(base);
+ expressions.add(writerAddr);
+ access = new UnsafeWriteAccess(buffer, base, writerAddr);
+ }
+ writePrimitiveGroups(expressions, bean, buffer, primitiveGroups, () -> access, false);
+ expressions.add(new Invoke(buffer, "_increaseWriterIndexUnsafe", totalSizeLiteral));
+ return expressions;
+ }
+
+ private List serializePrimitivesCompressed(
+ Expression bean, Expression buffer, List> primitiveGroups, int totalSize) {
+ List expressions = new ArrayList<>();
+ // int/long may need extra bytes for compressed writing.
+ int extraSize = extraPrimitiveSize(primitiveGroups);
+ int growSize = totalSize + extraSize;
+ // After this grow, following writes can use unchecked low-level access.
+ expressions.add(new Invoke(buffer, "grow", Literal.ofInt(growSize)));
+ if (useIndexedAccess()) {
+ writePrimitiveGroups(
+ expressions,
+ bean,
+ buffer,
+ primitiveGroups,
+ () ->
+ new BufferWriteAccess(
+ buffer, new Invoke(buffer, "writerIndex", "writerIndex", PRIMITIVE_INT_TYPE)),
+ true);
+ } else {
+ // Must grow first, otherwise may get invalid address.
+ Expression base = new Invoke(buffer, "getHeapMemory", "base", PRIMITIVE_BYTE_ARRAY_TYPE);
+ expressions.add(base);
+ writePrimitiveGroups(
+ expressions,
+ bean,
+ buffer,
+ primitiveGroups,
+ () ->
+ new UnsafeWriteAccess(
+ buffer,
+ base,
+ new Invoke(buffer, "_unsafeWriterAddress", "writerAddr", PRIMITIVE_LONG_TYPE)),
+ true);
+ }
+ return expressions;
+ }
+
+ private void writePrimitiveGroups(
+ List expressions,
+ Expression bean,
+ Expression buffer,
+ List> primitiveGroups,
+ WriteAccessFactory accessFactory,
+ boolean compressed) {
+ int numPrimitiveFields = getNumPrimitiveFields(primitiveGroups);
+ int rawAcc = 0;
for (List group : primitiveGroups) {
ListExpression groupExpressions = new ListExpression();
- // use Reference to cut-off expr dependency.
+ PrimitiveWriteAccess access = accessFactory.get();
+ PrimitiveWriteState state = new PrimitiveWriteState(compressed ? 0 : rawAcc);
for (Descriptor descriptor : group) {
int dispatchId = getNumericDescriptorDispatchId(descriptor);
// `bean` will be replaced by `Reference` to cut-off expr dependency.
@@ -272,79 +335,144 @@ private List serializePrimitivesUnCompressed(
if (fieldValue instanceof Inlineable) {
((Inlineable) fieldValue).inline();
}
- if (dispatchId == DispatchId.BOOL) {
- groupExpressions.add(unsafePutBoolean(base, getWriterPos(writerAddr, acc), fieldValue));
- acc += 1;
- } else if (dispatchId == DispatchId.INT8) {
- groupExpressions.add(unsafePut(base, getWriterPos(writerAddr, acc), fieldValue));
- acc += 1;
- } else if (dispatchId == DispatchId.UINT8) {
- groupExpressions.add(
- unsafePut(
- base, getWriterPos(writerAddr, acc), primitiveByteValue(fieldValue, descriptor)));
- acc += 1;
- } else if (dispatchId == DispatchId.CHAR) {
- groupExpressions.add(unsafePutChar(base, getWriterPos(writerAddr, acc), fieldValue));
- acc += 2;
- } else if (dispatchId == DispatchId.INT16) {
- groupExpressions.add(unsafePutShort(base, getWriterPos(writerAddr, acc), fieldValue));
- acc += 2;
- } else if (dispatchId == DispatchId.UINT16) {
- groupExpressions.add(
- unsafePutShort(
- base,
- getWriterPos(writerAddr, acc),
- primitiveShortValue(fieldValue, descriptor)));
- acc += 2;
- } else if (dispatchId == DispatchId.FLOAT16 || dispatchId == DispatchId.BFLOAT16) {
- groupExpressions.add(
- unsafePutShort(
- base,
- getWriterPos(writerAddr, acc),
- new Invoke(fieldValue, "toBits", SHORT_TYPE)));
- acc += 2;
- } else if (dispatchId == DispatchId.INT32) {
- groupExpressions.add(unsafePutInt(base, getWriterPos(writerAddr, acc), fieldValue));
- acc += 4;
- } else if (dispatchId == DispatchId.UINT32) {
- groupExpressions.add(
- unsafePutInt(
- base, getWriterPos(writerAddr, acc), primitiveIntValue(fieldValue, descriptor)));
- acc += 4;
- } else if (dispatchId == DispatchId.INT64 || dispatchId == DispatchId.UINT64) {
- groupExpressions.add(unsafePutLong(base, getWriterPos(writerAddr, acc), fieldValue));
- acc += 8;
- } else if (dispatchId == DispatchId.FLOAT32) {
- groupExpressions.add(unsafePutFloat(base, getWriterPos(writerAddr, acc), fieldValue));
- acc += 4;
- } else if (dispatchId == DispatchId.FLOAT64) {
- groupExpressions.add(unsafePutDouble(base, getWriterPos(writerAddr, acc), fieldValue));
- acc += 8;
+ if (compressed) {
+ writeCompressed(
+ groupExpressions, buffer, access, descriptor, dispatchId, fieldValue, state);
} else {
- throw new IllegalStateException("Unsupported dispatchId: " + dispatchId);
+ writeFixed(groupExpressions, access, descriptor, dispatchId, fieldValue, state);
+ }
+ }
+ if (compressed) {
+ if (!state.compressStarted) {
+ // int/long are sorted in the last.
+ addIncWriterIndexExpr(groupExpressions, buffer, state.acc);
}
+ } else {
+ rawAcc = state.acc;
}
if (hasFewFields() || numPrimitiveFields < 4) {
expressions.add(groupExpressions);
} else {
expressions.add(
objectCodecOptimizer.invokeGenerated(
- ofHashSet(bean, base, writerAddr), groupExpressions, "writeFields"));
+ compressed ? access.compressedScope(bean) : access.fixedScope(bean),
+ groupExpressions,
+ "writeFields"));
}
}
- Expression increaseWriterIndex =
- new Invoke(
- buffer,
- "_increaseWriterIndexUnsafe",
- new Literal(totalSizeLiteral, PRIMITIVE_INT_TYPE));
- expressions.add(increaseWriterIndex);
- return expressions;
}
- private List serializePrimitivesCompressed(
- Expression bean, Expression buffer, List> primitiveGroups, int totalSize) {
- List expressions = new ArrayList<>();
- // int/long may need extra one-byte for writing.
+ private void writeCompressed(
+ ListExpression expressions,
+ Expression buffer,
+ PrimitiveWriteAccess access,
+ Descriptor descriptor,
+ int dispatchId,
+ Expression fieldValue,
+ PrimitiveWriteState state) {
+ switch (dispatchId) {
+ case DispatchId.VARINT32:
+ startCompressed(expressions, buffer, state);
+ expressions.add(new Invoke(buffer, "_unsafeWriteVarInt32", fieldValue));
+ return;
+ case DispatchId.VAR_UINT32:
+ startCompressed(expressions, buffer, state);
+ expressions.add(
+ new Invoke(buffer, "_unsafeWriteVarUInt32", primitiveIntValue(fieldValue, descriptor)));
+ return;
+ case DispatchId.VARINT64:
+ startCompressed(expressions, buffer, state);
+ expressions.add(new Invoke(buffer, "writeVarInt64", fieldValue));
+ return;
+ case DispatchId.TAGGED_INT64:
+ startCompressed(expressions, buffer, state);
+ expressions.add(new Invoke(buffer, "writeTaggedInt64", fieldValue));
+ return;
+ case DispatchId.VAR_UINT64:
+ startCompressed(expressions, buffer, state);
+ expressions.add(new Invoke(buffer, "writeVarUInt64", fieldValue));
+ return;
+ case DispatchId.TAGGED_UINT64:
+ startCompressed(expressions, buffer, state);
+ expressions.add(new Invoke(buffer, "writeTaggedUInt64", fieldValue));
+ return;
+ default:
+ writeFixed(expressions, access, descriptor, dispatchId, fieldValue, state);
+ }
+ }
+
+ private void startCompressed(
+ ListExpression expressions, Expression buffer, PrimitiveWriteState state) {
+ if (!state.compressStarted) {
+ addIncWriterIndexExpr(expressions, buffer, state.acc);
+ state.compressStarted = true;
+ }
+ }
+
+ private void writeFixed(
+ ListExpression expressions,
+ PrimitiveWriteAccess access,
+ Descriptor descriptor,
+ int dispatchId,
+ Expression fieldValue,
+ PrimitiveWriteState state) {
+ switch (dispatchId) {
+ case DispatchId.BOOL:
+ expressions.add(access.putBoolean(state.acc, fieldValue));
+ state.acc += 1;
+ return;
+ case DispatchId.INT8:
+ expressions.add(access.putByte(state.acc, fieldValue));
+ state.acc += 1;
+ return;
+ case DispatchId.UINT8:
+ expressions.add(access.putByte(state.acc, primitiveByteValue(fieldValue, descriptor)));
+ state.acc += 1;
+ return;
+ case DispatchId.CHAR:
+ expressions.add(access.putChar(state.acc, fieldValue));
+ state.acc += 2;
+ return;
+ case DispatchId.INT16:
+ expressions.add(access.putInt16(state.acc, fieldValue));
+ state.acc += 2;
+ return;
+ case DispatchId.UINT16:
+ expressions.add(access.putInt16(state.acc, primitiveShortValue(fieldValue, descriptor)));
+ state.acc += 2;
+ return;
+ case DispatchId.FLOAT16:
+ case DispatchId.BFLOAT16:
+ expressions.add(access.putInt16(state.acc, new Invoke(fieldValue, "toBits", SHORT_TYPE)));
+ state.acc += 2;
+ return;
+ case DispatchId.INT32:
+ expressions.add(access.putInt32(state.acc, fieldValue));
+ state.acc += 4;
+ return;
+ case DispatchId.UINT32:
+ expressions.add(access.putInt32(state.acc, primitiveIntValue(fieldValue, descriptor)));
+ state.acc += 4;
+ return;
+ case DispatchId.INT64:
+ case DispatchId.UINT64:
+ expressions.add(access.putInt64(state.acc, fieldValue));
+ state.acc += 8;
+ return;
+ case DispatchId.FLOAT32:
+ expressions.add(access.putFloat32(state.acc, fieldValue));
+ state.acc += 4;
+ return;
+ case DispatchId.FLOAT64:
+ expressions.add(access.putFloat64(state.acc, fieldValue));
+ state.acc += 8;
+ return;
+ default:
+ throw new IllegalStateException("Unsupported dispatchId: " + dispatchId);
+ }
+ }
+
+ private int extraPrimitiveSize(List> primitiveGroups) {
int extraSize = 0;
for (List group : primitiveGroups) {
for (Descriptor d : group) {
@@ -353,7 +481,7 @@ private List serializePrimitivesCompressed(
|| id == DispatchId.VARINT32
|| id == DispatchId.VAR_UINT32
|| id == DispatchId.UINT32) {
- // varint may be written as 5bytes, use 8bytes for written as long to reduce cost.
+ // varint may be written as 5 bytes; reserve 4 extra bytes over the fixed size.
extraSize += 4;
} else if (id == DispatchId.INT64
|| id == DispatchId.VARINT64
@@ -361,134 +489,254 @@ private List serializePrimitivesCompressed(
|| id == DispatchId.VAR_UINT64
|| id == DispatchId.TAGGED_UINT64
|| id == DispatchId.UINT64) {
- extraSize += 1; // long use 1~9 bytes.
+ // long uses 1~9 bytes; reserve one byte over the fixed size.
+ extraSize += 1;
}
}
}
- int growSize = totalSize + extraSize;
- // After this grow, following writes can be unsafe without checks.
- expressions.add(new Invoke(buffer, "grow", Literal.ofInt(growSize)));
- // Must grow first, otherwise may get invalid address.
- Expression base = new Invoke(buffer, "getHeapMemory", "base", PRIMITIVE_BYTE_ARRAY_TYPE);
- expressions.add(base);
- int numPrimitiveFields = getNumPrimitiveFields(primitiveGroups);
- for (List group : primitiveGroups) {
- ListExpression groupExpressions = new ListExpression();
- Expression writerAddr =
- new Invoke(buffer, "_unsafeWriterAddress", "writerAddr", PRIMITIVE_LONG_TYPE);
- // use Reference to cut-off expr dependency.
- int acc = 0;
- boolean compressStarted = false;
- for (Descriptor descriptor : group) {
- int dispatchId = getNumericDescriptorDispatchId(descriptor);
- // `bean` will be replaced by `Reference` to cut-off expr dependency.
- Expression fieldValue = getFieldValue(bean, descriptor);
- if (fieldValue instanceof Inlineable) {
- ((Inlineable) fieldValue).inline();
- }
- if (dispatchId == DispatchId.BOOL) {
- groupExpressions.add(unsafePutBoolean(base, getWriterPos(writerAddr, acc), fieldValue));
- acc += 1;
- } else if (dispatchId == DispatchId.INT8) {
- groupExpressions.add(unsafePut(base, getWriterPos(writerAddr, acc), fieldValue));
- acc += 1;
- } else if (dispatchId == DispatchId.UINT8) {
- groupExpressions.add(
- unsafePut(
- base, getWriterPos(writerAddr, acc), primitiveByteValue(fieldValue, descriptor)));
- acc += 1;
- } else if (dispatchId == DispatchId.CHAR) {
- groupExpressions.add(unsafePutChar(base, getWriterPos(writerAddr, acc), fieldValue));
- acc += 2;
- } else if (dispatchId == DispatchId.INT16) {
- groupExpressions.add(unsafePutShort(base, getWriterPos(writerAddr, acc), fieldValue));
- acc += 2;
- } else if (dispatchId == DispatchId.UINT16) {
- groupExpressions.add(
- unsafePutShort(
- base,
- getWriterPos(writerAddr, acc),
- primitiveShortValue(fieldValue, descriptor)));
- acc += 2;
- } else if (dispatchId == DispatchId.FLOAT16 || dispatchId == DispatchId.BFLOAT16) {
- groupExpressions.add(
- unsafePutShort(
- base,
- getWriterPos(writerAddr, acc),
- new Invoke(fieldValue, "toBits", SHORT_TYPE)));
- acc += 2;
- } else if (dispatchId == DispatchId.FLOAT32) {
- groupExpressions.add(unsafePutFloat(base, getWriterPos(writerAddr, acc), fieldValue));
- acc += 4;
- } else if (dispatchId == DispatchId.FLOAT64) {
- groupExpressions.add(unsafePutDouble(base, getWriterPos(writerAddr, acc), fieldValue));
- acc += 8;
- } else if (dispatchId == DispatchId.INT32) {
- groupExpressions.add(unsafePutInt(base, getWriterPos(writerAddr, acc), fieldValue));
- acc += 4;
- } else if (dispatchId == DispatchId.UINT32) {
- groupExpressions.add(
- unsafePutInt(
- base, getWriterPos(writerAddr, acc), primitiveIntValue(fieldValue, descriptor)));
- acc += 4;
- } else if (dispatchId == DispatchId.INT64 || dispatchId == DispatchId.UINT64) {
- groupExpressions.add(unsafePutLong(base, getWriterPos(writerAddr, acc), fieldValue));
- acc += 8;
- } else if (dispatchId == DispatchId.VARINT32) {
- if (!compressStarted) {
- addIncWriterIndexExpr(groupExpressions, buffer, acc);
- compressStarted = true;
- }
- groupExpressions.add(new Invoke(buffer, "_unsafeWriteVarInt32", fieldValue));
- } else if (dispatchId == DispatchId.VAR_UINT32) {
- if (!compressStarted) {
- addIncWriterIndexExpr(groupExpressions, buffer, acc);
- compressStarted = true;
- }
- groupExpressions.add(
- new Invoke(
- buffer, "_unsafeWriteVarUInt32", primitiveIntValue(fieldValue, descriptor)));
- } else if (dispatchId == DispatchId.VARINT64) {
- if (!compressStarted) {
- addIncWriterIndexExpr(groupExpressions, buffer, acc);
- compressStarted = true;
- }
- groupExpressions.add(new Invoke(buffer, "writeVarInt64", fieldValue));
- } else if (dispatchId == DispatchId.TAGGED_INT64) {
- if (!compressStarted) {
- addIncWriterIndexExpr(groupExpressions, buffer, acc);
- compressStarted = true;
- }
- groupExpressions.add(new Invoke(buffer, "writeTaggedInt64", fieldValue));
- } else if (dispatchId == DispatchId.VAR_UINT64) {
- if (!compressStarted) {
- addIncWriterIndexExpr(groupExpressions, buffer, acc);
- compressStarted = true;
- }
- groupExpressions.add(new Invoke(buffer, "writeVarUInt64", fieldValue));
- } else if (dispatchId == DispatchId.TAGGED_UINT64) {
- if (!compressStarted) {
- addIncWriterIndexExpr(groupExpressions, buffer, acc);
- compressStarted = true;
- }
- groupExpressions.add(new Invoke(buffer, "writeTaggedUInt64", fieldValue));
- } else {
- throw new IllegalStateException("Unsupported dispatchId: " + dispatchId);
- }
- }
- if (!compressStarted) {
- // int/long are sorted in the last.
- addIncWriterIndexExpr(groupExpressions, buffer, acc);
- }
- if (hasFewFields() || numPrimitiveFields < 4) {
- expressions.add(groupExpressions);
- } else {
- expressions.add(
- objectCodecOptimizer.invokeGenerated(
- ofHashSet(bean, buffer, base), groupExpressions, "writeFields"));
- }
+ return extraSize;
+ }
+
+ private boolean useIndexedAccess() {
+ return JdkVersion.MAJOR_VERSION >= 25;
+ }
+
+ private interface WriteAccessFactory {
+ PrimitiveWriteAccess get();
+ }
+
+ private static final class PrimitiveWriteState {
+ private int acc;
+ private boolean compressStarted;
+
+ private PrimitiveWriteState(int acc) {
+ this.acc = acc;
}
- return expressions;
+ }
+
+ private abstract class PrimitiveWriteAccess {
+ protected final Expression buffer;
+ protected final Expression cursor;
+
+ private PrimitiveWriteAccess(Expression buffer, Expression cursor) {
+ this.buffer = buffer;
+ this.cursor = cursor;
+ }
+
+ abstract Expression putByte(int acc, Expression value);
+
+ abstract Expression putBoolean(int acc, Expression value);
+
+ abstract Expression putChar(int acc, Expression value);
+
+ abstract Expression putInt16(int acc, Expression value);
+
+ abstract Expression putInt32(int acc, Expression value);
+
+ abstract Expression putInt64(int acc, Expression value);
+
+ abstract Expression putFloat32(int acc, Expression value);
+
+ abstract Expression putFloat64(int acc, Expression value);
+
+ abstract Set fixedScope(Expression bean);
+
+ abstract Set compressedScope(Expression bean);
+ }
+
+ private final class UnsafeWriteAccess extends PrimitiveWriteAccess {
+ private final Expression base;
+
+ private UnsafeWriteAccess(Expression buffer, Expression base, Expression writerAddr) {
+ super(buffer, writerAddr);
+ this.base = base;
+ }
+
+ private Expression pos(int acc) {
+ return getWriterPos(cursor, acc);
+ }
+
+ @Override
+ Expression putByte(int acc, Expression value) {
+ return unsafePut(base, pos(acc), value);
+ }
+
+ @Override
+ Expression putBoolean(int acc, Expression value) {
+ return unsafePutBoolean(base, pos(acc), value);
+ }
+
+ @Override
+ Expression putChar(int acc, Expression value) {
+ return unsafePutChar(base, pos(acc), value);
+ }
+
+ @Override
+ Expression putInt16(int acc, Expression value) {
+ return unsafePutShort(base, pos(acc), value);
+ }
+
+ @Override
+ Expression putInt32(int acc, Expression value) {
+ return unsafePutInt(base, pos(acc), value);
+ }
+
+ @Override
+ Expression putInt64(int acc, Expression value) {
+ return unsafePutLong(base, pos(acc), value);
+ }
+
+ @Override
+ Expression putFloat32(int acc, Expression value) {
+ return unsafePutFloat(base, pos(acc), value);
+ }
+
+ @Override
+ Expression putFloat64(int acc, Expression value) {
+ return unsafePutDouble(base, pos(acc), value);
+ }
+
+ @Override
+ Set fixedScope(Expression bean) {
+ return ofHashSet(bean, base, cursor);
+ }
+
+ @Override
+ Set compressedScope(Expression bean) {
+ return ofHashSet(bean, buffer, base);
+ }
+ }
+
+ private final class BufferWriteAccess extends PrimitiveWriteAccess {
+ private BufferWriteAccess(Expression buffer, Expression writerIndex) {
+ super(buffer, writerIndex);
+ }
+
+ private Expression index(int acc) {
+ return getBufferIndex(cursor, acc);
+ }
+
+ @Override
+ Expression putByte(int acc, Expression value) {
+ return bufferPutByte(buffer, index(acc), value);
+ }
+
+ @Override
+ Expression putBoolean(int acc, Expression value) {
+ return bufferPutBoolean(buffer, index(acc), value);
+ }
+
+ @Override
+ Expression putChar(int acc, Expression value) {
+ return bufferPutChar(buffer, index(acc), value);
+ }
+
+ @Override
+ Expression putInt16(int acc, Expression value) {
+ return bufferPutInt16(buffer, index(acc), value);
+ }
+
+ @Override
+ Expression putInt32(int acc, Expression value) {
+ return bufferPutInt32(buffer, index(acc), value);
+ }
+
+ @Override
+ Expression putInt64(int acc, Expression value) {
+ return bufferPutInt64(buffer, index(acc), value);
+ }
+
+ @Override
+ Expression putFloat32(int acc, Expression value) {
+ return bufferPutFloat32(buffer, index(acc), value);
+ }
+
+ @Override
+ Expression putFloat64(int acc, Expression value) {
+ return bufferPutFloat64(buffer, index(acc), value);
+ }
+
+ @Override
+ Set fixedScope(Expression bean) {
+ return ofHashSet(bean, buffer, cursor);
+ }
+
+ @Override
+ Set compressedScope(Expression bean) {
+ return ofHashSet(bean, buffer, cursor);
+ }
+ }
+
+ private Expression bufferPutByte(Expression buffer, Expression index, Expression value) {
+ return new Invoke(buffer, "_unsafePutByte", index, value);
+ }
+
+ private Expression bufferPutBoolean(Expression buffer, Expression index, Expression value) {
+ return new Invoke(buffer, "_unsafePutBoolean", index, value);
+ }
+
+ private Expression bufferPutChar(Expression buffer, Expression index, Expression value) {
+ return new Invoke(buffer, "_unsafePutChar", index, value);
+ }
+
+ private Expression bufferPutInt16(Expression buffer, Expression index, Expression value) {
+ return new Invoke(buffer, "_unsafePutInt16", index, value);
+ }
+
+ private Expression bufferPutInt32(Expression buffer, Expression index, Expression value) {
+ return new Invoke(buffer, "_unsafePutInt32", index, value);
+ }
+
+ private Expression bufferPutInt64(Expression buffer, Expression index, Expression value) {
+ return new Invoke(buffer, "_unsafePutInt64", index, value);
+ }
+
+ private Expression bufferPutFloat32(Expression buffer, Expression index, Expression value) {
+ return bufferPutInt32(
+ buffer,
+ index,
+ new StaticInvoke(Float.class, "floatToRawIntBits", PRIMITIVE_INT_TYPE, value));
+ }
+
+ private Expression bufferPutFloat64(Expression buffer, Expression index, Expression value) {
+ return bufferPutInt64(
+ buffer,
+ index,
+ new StaticInvoke(Double.class, "doubleToRawLongBits", PRIMITIVE_LONG_TYPE, value));
+ }
+
+ private Expression bufferGetByte(Expression buffer, Expression index) {
+ return new Invoke(buffer, "_unsafeGetByte", PRIMITIVE_BYTE_TYPE, index);
+ }
+
+ private Expression bufferGetBoolean(Expression buffer, Expression index) {
+ return new Invoke(buffer, "_unsafeGetBoolean", PRIMITIVE_BOOLEAN_TYPE, index);
+ }
+
+ private Expression bufferGetChar(Expression buffer, Expression index) {
+ return new Invoke(buffer, "_unsafeGetChar", PRIMITIVE_CHAR_TYPE, index);
+ }
+
+ private Expression bufferGetInt16(Expression buffer, Expression index) {
+ return new Invoke(buffer, "_unsafeGetInt16", PRIMITIVE_SHORT_TYPE, index);
+ }
+
+ private Expression bufferGetInt32(Expression buffer, Expression index) {
+ return new Invoke(buffer, "_unsafeGetInt32", PRIMITIVE_INT_TYPE, index);
+ }
+
+ private Expression bufferGetInt64(Expression buffer, Expression index) {
+ return new Invoke(buffer, "_unsafeGetInt64", PRIMITIVE_LONG_TYPE, index);
+ }
+
+ private Expression bufferGetFloat32(Expression buffer, Expression index) {
+ return new StaticInvoke(
+ Float.class, "intBitsToFloat", PRIMITIVE_FLOAT_TYPE, bufferGetInt32(buffer, index));
+ }
+
+ private Expression bufferGetFloat64(Expression buffer, Expression index) {
+ return new StaticInvoke(
+ Double.class, "longBitsToDouble", PRIMITIVE_DOUBLE_TYPE, bufferGetInt64(buffer, index));
}
private Expression primitiveByteValue(Expression fieldValue, Descriptor descriptor) {
@@ -573,9 +821,10 @@ public Expression buildDecodeExpression() {
FieldsCollector collector = (FieldsCollector) bean;
bean = createRecord(collector.recordValuesMap);
} else {
- ObjectCreators.getObjectCreator(beanClass); // trigger cache and make error raised early
+ typeResolver.getObjectInstantiator(beanClass); // trigger cache and make error raised early
bean =
- new Invoke(getObjectCreator(beanClass), "newInstanceWithArguments", OBJECT_TYPE, bean);
+ new Invoke(
+ getObjectInstantiator(beanClass), "newInstanceWithArguments", OBJECT_TYPE, bean);
}
}
expressions.add(new Expression.Return(bean));
@@ -598,8 +847,8 @@ protected void deserializeReadGroup(
}
protected Expression buildComponentsArray() {
- return new StaticInvoke(
- UnsafeOps.class, "copyObjectArray", OBJECT_ARRAY_TYPE, recordComponentDefaultValues);
+ return new Cast(
+ new Invoke(recordComponentDefaultValues, "clone", OBJECT_TYPE), OBJECT_ARRAY_TYPE);
}
protected Expression createRecord(SortedMap recordComponents) {
@@ -657,9 +906,7 @@ protected Expression deserializeGroup(
ExpressionVisitor.ExprHolder exprHolder = ExpressionVisitor.ExprHolder.of("bean", bean);
walkPath.add(d.getDeclaringClass() + d.getName());
TypeRef> castTypeRef =
- hasCompatibleCollectionArrayRead(d)
- ? compatibleReadTargetTypeRef(d)
- : d.getTypeRef();
+ hasCompatibleCollectionArrayRead(d) ? compatibleLocalTypeRef(d) : d.getTypeRef();
Expression action =
deserializeField(
buffer,
@@ -699,7 +946,7 @@ protected Expression deserializeGroupForRecord(
// use Reference to cut-off expr dependency.
for (Descriptor d : group) {
TypeRef> castTypeRef =
- hasCompatibleCollectionArrayRead(d) ? compatibleReadTargetTypeRef(d) : d.getTypeRef();
+ hasCompatibleCollectionArrayRead(d) ? compatibleLocalTypeRef(d) : d.getTypeRef();
Expression value = deserializeField(buffer, d, expr -> expr);
Expression action = setFieldValue(bean, d, tryInlineCast(value, castTypeRef));
groupExpressions.add(action);
@@ -738,254 +985,376 @@ protected List deserializePrimitives(
private List deserializeUnCompressedPrimitives(
Expression bean, Expression buffer, List> primitiveGroups, int totalSize) {
List expressions = new ArrayList<>();
- int numPrimitiveFields = getNumPrimitiveFields(primitiveGroups);
Literal totalSizeLiteral = Literal.ofInt(totalSize);
- // After this check, following read can be totally unsafe without checks
+ // After this check, following reads can use unchecked low-level access.
expressions.add(new Invoke(buffer, "checkReadableBytes", totalSizeLiteral));
- Expression heapBuffer =
- new Invoke(buffer, "getHeapMemory", "heapBuffer", PRIMITIVE_BYTE_ARRAY_TYPE);
- Expression readerAddr =
- new Invoke(buffer, "getUnsafeReaderAddress", "readerAddr", PRIMITIVE_LONG_TYPE);
- expressions.add(heapBuffer);
- expressions.add(readerAddr);
- int acc = 0;
- for (List group : primitiveGroups) {
- ListExpression groupExpressions = new ListExpression();
- for (Descriptor descriptor : group) {
- int dispatchId = getNumericDescriptorDispatchId(descriptor);
- Expression fieldValue;
- if (dispatchId == DispatchId.BOOL) {
- fieldValue = unsafeGetBoolean(heapBuffer, getReaderAddress(readerAddr, acc));
- acc += 1;
- } else if (dispatchId == DispatchId.INT8) {
- fieldValue = unsafeGet(heapBuffer, getReaderAddress(readerAddr, acc));
- acc += 1;
- } else if (dispatchId == DispatchId.UINT8) {
- fieldValue =
- new StaticInvoke(
- Byte.class,
- "toUnsignedInt",
- descriptor.getTypeRef(),
- unsafeGet(heapBuffer, getReaderAddress(readerAddr, acc)));
- acc += 1;
- } else if (dispatchId == DispatchId.CHAR) {
- fieldValue = unsafeGetChar(heapBuffer, getReaderAddress(readerAddr, acc));
- acc += 2;
- } else if (dispatchId == DispatchId.INT16) {
- fieldValue = unsafeGetShort(heapBuffer, getReaderAddress(readerAddr, acc));
- acc += 2;
- } else if (dispatchId == DispatchId.UINT16) {
- fieldValue =
- new StaticInvoke(
- Short.class,
- "toUnsignedInt",
- descriptor.getTypeRef(),
- unsafeGetShort(heapBuffer, getReaderAddress(readerAddr, acc)));
- acc += 2;
- } else if (dispatchId == DispatchId.FLOAT16) {
- fieldValue =
- new StaticInvoke(
- Float16.class,
- "fromBits",
- TypeRef.of(Float16.class),
- unsafeGetShort(heapBuffer, getReaderAddress(readerAddr, acc)));
- acc += 2;
- } else if (dispatchId == DispatchId.BFLOAT16) {
- fieldValue =
- new StaticInvoke(
- BFloat16.class,
- "fromBits",
- TypeRef.of(BFloat16.class),
- unsafeGetShort(heapBuffer, getReaderAddress(readerAddr, acc)));
- acc += 2;
- } else if (dispatchId == DispatchId.INT32) {
- fieldValue = unsafeGetInt(heapBuffer, getReaderAddress(readerAddr, acc));
- acc += 4;
- } else if (dispatchId == DispatchId.UINT32) {
- fieldValue =
- new StaticInvoke(
- Integer.class,
- "toUnsignedLong",
- descriptor.getTypeRef(),
- unsafeGetInt(heapBuffer, getReaderAddress(readerAddr, acc)));
- acc += 4;
- } else if (dispatchId == DispatchId.INT64 || dispatchId == DispatchId.UINT64) {
- fieldValue = unsafeGetLong(heapBuffer, getReaderAddress(readerAddr, acc));
- acc += 8;
- } else if (dispatchId == DispatchId.FLOAT32) {
- fieldValue = unsafeGetFloat(heapBuffer, getReaderAddress(readerAddr, acc));
- acc += 4;
- } else if (dispatchId == DispatchId.FLOAT64) {
- fieldValue = unsafeGetDouble(heapBuffer, getReaderAddress(readerAddr, acc));
- acc += 8;
- } else {
- throw new IllegalStateException("Unsupported dispatchId: " + dispatchId);
- }
- // `bean` will be replaced by `Reference` to cut-off expr dependency.
- groupExpressions.add(setFieldValue(bean, descriptor, fieldValue));
- }
- if (hasFewFields() || numPrimitiveFields < 4 || isRecord) {
- expressions.add(groupExpressions);
- } else {
- expressions.add(
- objectCodecOptimizer.invokeGenerated(
- ofHashSet(bean, heapBuffer, readerAddr), groupExpressions, "readFields"));
- }
+ PrimitiveReadAccess access;
+ if (useIndexedAccess()) {
+ Expression readerIndex = new Invoke(buffer, "readerIndex", "readerIndex", PRIMITIVE_INT_TYPE);
+ expressions.add(readerIndex);
+ access = new BufferReadAccess(buffer, readerIndex);
+ } else {
+ Expression heapBuffer =
+ new Invoke(buffer, "getHeapMemory", "heapBuffer", PRIMITIVE_BYTE_ARRAY_TYPE);
+ Expression readerAddr =
+ new Invoke(buffer, "getUnsafeReaderAddress", "readerAddr", PRIMITIVE_LONG_TYPE);
+ expressions.add(heapBuffer);
+ expressions.add(readerAddr);
+ access = new UnsafeReadAccess(buffer, heapBuffer, readerAddr);
}
- Expression increaseReaderIndex =
- new Invoke(
- buffer, "increaseReaderIndex", new Literal(totalSizeLiteral, PRIMITIVE_INT_TYPE));
- expressions.add(increaseReaderIndex);
+ readPrimitiveGroups(expressions, bean, buffer, primitiveGroups, ignored -> access, false);
+ expressions.add(new Invoke(buffer, "increaseReaderIndex", totalSizeLiteral));
return expressions;
}
private List deserializeCompressedPrimitives(
Expression bean, Expression buffer, List> primitiveGroups) {
List expressions = new ArrayList<>();
+ if (useIndexedAccess()) {
+ readPrimitiveGroups(
+ expressions,
+ bean,
+ buffer,
+ primitiveGroups,
+ readExpressions -> {
+ Expression readerIndex =
+ new Invoke(buffer, "readerIndex", "readerIndex", PRIMITIVE_INT_TYPE);
+ readExpressions.add(readerIndex);
+ return new BufferReadAccess(buffer, readerIndex);
+ },
+ true);
+ } else {
+ readPrimitiveGroups(
+ expressions,
+ bean,
+ buffer,
+ primitiveGroups,
+ readExpressions -> {
+ // checkReadableBytes first, `fillBuffer` may create a new heap buffer.
+ Expression heapBuffer =
+ new Invoke(buffer, "getHeapMemory", "heapBuffer", PRIMITIVE_BYTE_ARRAY_TYPE);
+ readExpressions.add(heapBuffer);
+ Expression readerAddr =
+ new Invoke(buffer, "getUnsafeReaderAddress", "readerAddr", PRIMITIVE_LONG_TYPE);
+ return new UnsafeReadAccess(buffer, heapBuffer, readerAddr);
+ },
+ true);
+ }
+ return expressions;
+ }
+
+ private void readPrimitiveGroups(
+ List expressions,
+ Expression bean,
+ Expression buffer,
+ List> primitiveGroups,
+ ReadAccessFactory accessFactory,
+ boolean compressed) {
int numPrimitiveFields = getNumPrimitiveFields(primitiveGroups);
+ int rawAcc = 0;
for (List group : primitiveGroups) {
- // After this check, following read can be totally unsafe without checks.
- // checkReadableBytes first, `fillBuffer` may create a new heap buffer.
- ReplaceStub checkReadableBytesStub = new ReplaceStub();
- expressions.add(checkReadableBytesStub);
- Expression heapBuffer =
- new Invoke(buffer, "getHeapMemory", "heapBuffer", PRIMITIVE_BYTE_ARRAY_TYPE);
- expressions.add(heapBuffer);
+ ReplaceStub checkReadableBytesStub = null;
+ if (compressed) {
+ // After this check, following reads can use unchecked low-level access.
+ checkReadableBytesStub = new ReplaceStub();
+ expressions.add(checkReadableBytesStub);
+ }
+ PrimitiveReadAccess access = accessFactory.get(expressions);
ListExpression groupExpressions = new ListExpression();
- Expression readerAddr =
- new Invoke(buffer, "getUnsafeReaderAddress", "readerAddr", PRIMITIVE_LONG_TYPE);
- int acc = 0;
- boolean compressStarted = false;
+ PrimitiveReadState state = new PrimitiveReadState(compressed ? 0 : rawAcc);
for (Descriptor descriptor : group) {
int dispatchId = getNumericDescriptorDispatchId(descriptor);
- Expression fieldValue;
- if (dispatchId == DispatchId.BOOL) {
- fieldValue = unsafeGetBoolean(heapBuffer, getReaderAddress(readerAddr, acc));
- acc += 1;
- } else if (dispatchId == DispatchId.INT8) {
- fieldValue = unsafeGet(heapBuffer, getReaderAddress(readerAddr, acc));
- acc += 1;
- } else if (dispatchId == DispatchId.UINT8) {
- fieldValue =
- new StaticInvoke(
- Byte.class,
- "toUnsignedInt",
- descriptor.getTypeRef(),
- unsafeGet(heapBuffer, getReaderAddress(readerAddr, acc)));
- acc += 1;
- } else if (dispatchId == DispatchId.CHAR) {
- fieldValue = unsafeGetChar(heapBuffer, getReaderAddress(readerAddr, acc));
- acc += 2;
- } else if (dispatchId == DispatchId.INT16) {
- fieldValue = unsafeGetShort(heapBuffer, getReaderAddress(readerAddr, acc));
- acc += 2;
- } else if (dispatchId == DispatchId.UINT16) {
- fieldValue =
- new StaticInvoke(
- Short.class,
- "toUnsignedInt",
- descriptor.getTypeRef(),
- unsafeGetShort(heapBuffer, getReaderAddress(readerAddr, acc)));
- acc += 2;
- } else if (dispatchId == DispatchId.FLOAT16) {
- fieldValue =
- new StaticInvoke(
- Float16.class,
- "fromBits",
- TypeRef.of(Float16.class),
- unsafeGetShort(heapBuffer, getReaderAddress(readerAddr, acc)));
- acc += 2;
- } else if (dispatchId == DispatchId.BFLOAT16) {
- fieldValue =
- new StaticInvoke(
- BFloat16.class,
- "fromBits",
- TypeRef.of(BFloat16.class),
- unsafeGetShort(heapBuffer, getReaderAddress(readerAddr, acc)));
- acc += 2;
- } else if (dispatchId == DispatchId.FLOAT32) {
- fieldValue = unsafeGetFloat(heapBuffer, getReaderAddress(readerAddr, acc));
- acc += 4;
- } else if (dispatchId == DispatchId.FLOAT64) {
- fieldValue = unsafeGetDouble(heapBuffer, getReaderAddress(readerAddr, acc));
- acc += 8;
- } else if (dispatchId == DispatchId.INT32) {
- fieldValue = unsafeGetInt(heapBuffer, getReaderAddress(readerAddr, acc));
- acc += 4;
- } else if (dispatchId == DispatchId.UINT32) {
- fieldValue =
- new StaticInvoke(
- Integer.class,
- "toUnsignedLong",
- descriptor.getTypeRef(),
- unsafeGetInt(heapBuffer, getReaderAddress(readerAddr, acc)));
- acc += 4;
- } else if (dispatchId == DispatchId.INT64 || dispatchId == DispatchId.UINT64) {
- fieldValue = unsafeGetLong(heapBuffer, getReaderAddress(readerAddr, acc));
- acc += 8;
- } else if (dispatchId == DispatchId.VARINT32) {
- if (!compressStarted) {
- compressStarted = true;
- addIncReaderIndexExpr(groupExpressions, buffer, acc);
- }
- fieldValue = readVarInt32(buffer);
- } else if (dispatchId == DispatchId.VAR_UINT32) {
- if (!compressStarted) {
- compressStarted = true;
- addIncReaderIndexExpr(groupExpressions, buffer, acc);
- }
- fieldValue =
- new StaticInvoke(
- Integer.class,
- "toUnsignedLong",
- descriptor.getTypeRef(),
- new Invoke(buffer, "readVarUInt32", PRIMITIVE_INT_TYPE));
- } else if (dispatchId == DispatchId.VARINT64) {
- if (!compressStarted) {
- compressStarted = true;
- addIncReaderIndexExpr(groupExpressions, buffer, acc);
- }
- fieldValue = new Invoke(buffer, "readVarInt64", PRIMITIVE_LONG_TYPE);
- } else if (dispatchId == DispatchId.TAGGED_INT64) {
- if (!compressStarted) {
- compressStarted = true;
- addIncReaderIndexExpr(groupExpressions, buffer, acc);
- }
- fieldValue = new Invoke(buffer, "readTaggedInt64", PRIMITIVE_LONG_TYPE);
- } else if (dispatchId == DispatchId.VAR_UINT64) {
- if (!compressStarted) {
- compressStarted = true;
- addIncReaderIndexExpr(groupExpressions, buffer, acc);
- }
- fieldValue = new Invoke(buffer, "readVarUInt64", PRIMITIVE_LONG_TYPE);
- } else if (dispatchId == DispatchId.TAGGED_UINT64) {
- if (!compressStarted) {
- compressStarted = true;
- addIncReaderIndexExpr(groupExpressions, buffer, acc);
- }
- fieldValue = new Invoke(buffer, "readTaggedUInt64", PRIMITIVE_LONG_TYPE);
- } else {
- throw new IllegalStateException("Unsupported dispatchId: " + dispatchId);
- }
+ Expression fieldValue =
+ compressed
+ ? readCompressed(groupExpressions, buffer, access, descriptor, dispatchId, state)
+ : readFixed(access, descriptor, dispatchId, state);
// `bean` will be replaced by `Reference` to cut-off expr dependency.
groupExpressions.add(setFieldValue(bean, descriptor, fieldValue));
}
- if (acc != 0) {
- checkReadableBytesStub.setTargetObject(
- new Invoke(buffer, "checkReadableBytes", Literal.ofInt(acc)));
- }
- if (!compressStarted) {
- addIncReaderIndexExpr(groupExpressions, buffer, acc);
+ if (compressed) {
+ if (state.acc != 0) {
+ checkReadableBytesStub.setTargetObject(
+ new Invoke(buffer, "checkReadableBytes", Literal.ofInt(state.acc)));
+ }
+ if (!state.compressStarted) {
+ addIncReaderIndexExpr(groupExpressions, buffer, state.acc);
+ }
+ } else {
+ rawAcc = state.acc;
}
if (hasFewFields() || numPrimitiveFields < 4 || isRecord) {
expressions.add(groupExpressions);
} else {
expressions.add(
objectCodecOptimizer.invokeGenerated(
- ofHashSet(bean, buffer, heapBuffer), groupExpressions, "readFields"));
+ compressed ? access.compressedScope(bean) : access.fixedScope(bean),
+ groupExpressions,
+ "readFields"));
}
}
- return expressions;
+ }
+
+ private Expression readCompressed(
+ ListExpression expressions,
+ Expression buffer,
+ PrimitiveReadAccess access,
+ Descriptor descriptor,
+ int dispatchId,
+ PrimitiveReadState state) {
+ switch (dispatchId) {
+ case DispatchId.VARINT32:
+ startReadCompressed(expressions, buffer, state);
+ return readVarInt32(buffer);
+ case DispatchId.VAR_UINT32:
+ startReadCompressed(expressions, buffer, state);
+ return new StaticInvoke(
+ Integer.class,
+ "toUnsignedLong",
+ descriptor.getTypeRef(),
+ new Invoke(buffer, "readVarUInt32", PRIMITIVE_INT_TYPE));
+ case DispatchId.VARINT64:
+ startReadCompressed(expressions, buffer, state);
+ return new Invoke(buffer, "readVarInt64", PRIMITIVE_LONG_TYPE);
+ case DispatchId.TAGGED_INT64:
+ startReadCompressed(expressions, buffer, state);
+ return new Invoke(buffer, "readTaggedInt64", PRIMITIVE_LONG_TYPE);
+ case DispatchId.VAR_UINT64:
+ startReadCompressed(expressions, buffer, state);
+ return new Invoke(buffer, "readVarUInt64", PRIMITIVE_LONG_TYPE);
+ case DispatchId.TAGGED_UINT64:
+ startReadCompressed(expressions, buffer, state);
+ return new Invoke(buffer, "readTaggedUInt64", PRIMITIVE_LONG_TYPE);
+ default:
+ return readFixed(access, descriptor, dispatchId, state);
+ }
+ }
+
+ private void startReadCompressed(
+ ListExpression expressions, Expression buffer, PrimitiveReadState state) {
+ if (!state.compressStarted) {
+ state.compressStarted = true;
+ addIncReaderIndexExpr(expressions, buffer, state.acc);
+ }
+ }
+
+ private Expression readFixed(
+ PrimitiveReadAccess access, Descriptor descriptor, int dispatchId, PrimitiveReadState state) {
+ int acc = state.acc;
+ switch (dispatchId) {
+ case DispatchId.BOOL:
+ state.acc = acc + 1;
+ return access.getBoolean(acc);
+ case DispatchId.INT8:
+ state.acc = acc + 1;
+ return access.getByte(acc);
+ case DispatchId.UINT8:
+ state.acc = acc + 1;
+ return new StaticInvoke(
+ Byte.class, "toUnsignedInt", descriptor.getTypeRef(), access.getByte(acc));
+ case DispatchId.CHAR:
+ state.acc = acc + 2;
+ return access.getChar(acc);
+ case DispatchId.INT16:
+ state.acc = acc + 2;
+ return access.getInt16(acc);
+ case DispatchId.UINT16:
+ state.acc = acc + 2;
+ return new StaticInvoke(
+ Short.class, "toUnsignedInt", descriptor.getTypeRef(), access.getInt16(acc));
+ case DispatchId.FLOAT16:
+ state.acc = acc + 2;
+ return new StaticInvoke(
+ Float16.class, "fromBits", TypeRef.of(Float16.class), access.getInt16(acc));
+ case DispatchId.BFLOAT16:
+ state.acc = acc + 2;
+ return new StaticInvoke(
+ BFloat16.class, "fromBits", TypeRef.of(BFloat16.class), access.getInt16(acc));
+ case DispatchId.INT32:
+ state.acc = acc + 4;
+ return access.getInt32(acc);
+ case DispatchId.UINT32:
+ state.acc = acc + 4;
+ return new StaticInvoke(
+ Integer.class, "toUnsignedLong", descriptor.getTypeRef(), access.getInt32(acc));
+ case DispatchId.INT64:
+ case DispatchId.UINT64:
+ state.acc = acc + 8;
+ return access.getInt64(acc);
+ case DispatchId.FLOAT32:
+ state.acc = acc + 4;
+ return access.getFloat32(acc);
+ case DispatchId.FLOAT64:
+ state.acc = acc + 8;
+ return access.getFloat64(acc);
+ default:
+ throw new IllegalStateException("Unsupported dispatchId: " + dispatchId);
+ }
+ }
+
+ private interface ReadAccessFactory {
+ PrimitiveReadAccess get(List expressions);
+ }
+
+ private static final class PrimitiveReadState {
+ private int acc;
+ private boolean compressStarted;
+
+ private PrimitiveReadState(int acc) {
+ this.acc = acc;
+ }
+ }
+
+ private abstract class PrimitiveReadAccess {
+ protected final Expression buffer;
+ protected final Expression cursor;
+
+ private PrimitiveReadAccess(Expression buffer, Expression cursor) {
+ this.buffer = buffer;
+ this.cursor = cursor;
+ }
+
+ abstract Expression getByte(int acc);
+
+ abstract Expression getBoolean(int acc);
+
+ abstract Expression getChar(int acc);
+
+ abstract Expression getInt16(int acc);
+
+ abstract Expression getInt32(int acc);
+
+ abstract Expression getInt64(int acc);
+
+ abstract Expression getFloat32(int acc);
+
+ abstract Expression getFloat64(int acc);
+
+ abstract Set fixedScope(Expression bean);
+
+ abstract Set compressedScope(Expression bean);
+ }
+
+ private final class UnsafeReadAccess extends PrimitiveReadAccess {
+ private final Expression heapBuffer;
+
+ private UnsafeReadAccess(Expression buffer, Expression heapBuffer, Expression readerAddr) {
+ super(buffer, readerAddr);
+ this.heapBuffer = heapBuffer;
+ }
+
+ private Expression pos(int acc) {
+ return getReaderAddress(cursor, acc);
+ }
+
+ @Override
+ Expression getByte(int acc) {
+ return unsafeGet(heapBuffer, pos(acc));
+ }
+
+ @Override
+ Expression getBoolean(int acc) {
+ return unsafeGetBoolean(heapBuffer, pos(acc));
+ }
+
+ @Override
+ Expression getChar(int acc) {
+ return unsafeGetChar(heapBuffer, pos(acc));
+ }
+
+ @Override
+ Expression getInt16(int acc) {
+ return unsafeGetShort(heapBuffer, pos(acc));
+ }
+
+ @Override
+ Expression getInt32(int acc) {
+ return unsafeGetInt(heapBuffer, pos(acc));
+ }
+
+ @Override
+ Expression getInt64(int acc) {
+ return unsafeGetLong(heapBuffer, pos(acc));
+ }
+
+ @Override
+ Expression getFloat32(int acc) {
+ return unsafeGetFloat(heapBuffer, pos(acc));
+ }
+
+ @Override
+ Expression getFloat64(int acc) {
+ return unsafeGetDouble(heapBuffer, pos(acc));
+ }
+
+ @Override
+ Set fixedScope(Expression bean) {
+ return ofHashSet(bean, heapBuffer, cursor);
+ }
+
+ @Override
+ Set compressedScope(Expression bean) {
+ return ofHashSet(bean, buffer, heapBuffer);
+ }
+ }
+
+ private final class BufferReadAccess extends PrimitiveReadAccess {
+ private BufferReadAccess(Expression buffer, Expression readerIndex) {
+ super(buffer, readerIndex);
+ }
+
+ private Expression index(int acc) {
+ return getBufferIndex(cursor, acc);
+ }
+
+ @Override
+ Expression getByte(int acc) {
+ return bufferGetByte(buffer, index(acc));
+ }
+
+ @Override
+ Expression getBoolean(int acc) {
+ return bufferGetBoolean(buffer, index(acc));
+ }
+
+ @Override
+ Expression getChar(int acc) {
+ return bufferGetChar(buffer, index(acc));
+ }
+
+ @Override
+ Expression getInt16(int acc) {
+ return bufferGetInt16(buffer, index(acc));
+ }
+
+ @Override
+ Expression getInt32(int acc) {
+ return bufferGetInt32(buffer, index(acc));
+ }
+
+ @Override
+ Expression getInt64(int acc) {
+ return bufferGetInt64(buffer, index(acc));
+ }
+
+ @Override
+ Expression getFloat32(int acc) {
+ return bufferGetFloat32(buffer, index(acc));
+ }
+
+ @Override
+ Expression getFloat64(int acc) {
+ return bufferGetFloat64(buffer, index(acc));
+ }
+
+ @Override
+ Set fixedScope(Expression bean) {
+ return ofHashSet(bean, buffer, cursor);
+ }
+
+ @Override
+ Set compressedScope(Expression bean) {
+ return ofHashSet(bean, buffer, cursor);
+ }
}
private void addIncReaderIndexExpr(ListExpression expressions, Expression buffer, int diff) {
@@ -1000,4 +1369,11 @@ private Expression getReaderAddress(Expression readerPos, long acc) {
}
return add(readerPos, new Literal(acc, PRIMITIVE_LONG_TYPE));
}
+
+ private Expression getBufferIndex(Expression index, int acc) {
+ if (acc == 0) {
+ return index;
+ }
+ return add(index, Literal.ofInt(acc));
+ }
}
diff --git a/java/fory-core/src/main/java/org/apache/fory/builder/StaticCompatibleCodecBuilder.java b/java/fory-core/src/main/java/org/apache/fory/builder/StaticCompatibleCodecBuilder.java
index 26307470c5..8173b15495 100644
--- a/java/fory-core/src/main/java/org/apache/fory/builder/StaticCompatibleCodecBuilder.java
+++ b/java/fory-core/src/main/java/org/apache/fory/builder/StaticCompatibleCodecBuilder.java
@@ -59,6 +59,9 @@
*/
public final class StaticCompatibleCodecBuilder extends ObjectCodecBuilder {
private static final int DISPATCH_GROUP_SIZE = 8;
+ // Hidden generated serializers cannot use private split helpers because Janino emits private
+ // self-invokes against the source binary name, which Lookup#defineHiddenClass cannot resolve.
+ private static final String DISPATCH_METHOD_MODIFIERS = "final";
private final List localDescriptors;
private final boolean debug;
@@ -216,7 +219,8 @@ private String genRecordCompatibleRead() {
ctx.clearExprState();
Reference values = new Reference(recordValues, objectArrayTypeRef, false);
Code.ExprCode newRecord =
- new Invoke(getObjectCreator(beanClass), "newInstanceWithArguments", OBJECT_TYPE, values)
+ new Invoke(
+ getObjectInstantiator(beanClass), "newInstanceWithArguments", OBJECT_TYPE, values)
.genCode(ctx);
if (StringUtils.isNotBlank(newRecord.code())) {
code.append(newRecord.code()).append('\n');
@@ -233,7 +237,7 @@ private void genDispatchMethods() {
int groupCount = (localDescriptors.size() + DISPATCH_GROUP_SIZE - 1) / DISPATCH_GROUP_SIZE;
if (isRecord) {
ctx.addMethod(
- "private",
+ DISPATCH_METHOD_MODIFIERS,
"readMatchedRecordField",
genDispatchRouter("readMatchedRecordField", groupCount),
void.class,
@@ -245,7 +249,7 @@ private void genDispatchMethods() {
"_f_remoteField");
for (int group = 0; group < groupCount; group++) {
ctx.addMethod(
- "private",
+ DISPATCH_METHOD_MODIFIERS,
"readMatchedRecordField" + group,
genRecordDispatchGroup(group),
void.class,
@@ -259,7 +263,7 @@ private void genDispatchMethods() {
return;
}
ctx.addMethod(
- "private",
+ DISPATCH_METHOD_MODIFIERS,
"readMatchedField",
genDispatchRouter("readMatchedField", groupCount),
void.class,
@@ -271,7 +275,7 @@ private void genDispatchMethods() {
"_f_remoteField");
for (int group = 0; group < groupCount; group++) {
ctx.addMethod(
- "private",
+ DISPATCH_METHOD_MODIFIERS,
"readMatchedField" + group,
genObjectDispatchGroup(group, valueTypeRef),
void.class,
diff --git a/java/fory-core/src/main/java/org/apache/fory/builder/UnsafeCodegenSupport.java b/java/fory-core/src/main/java/org/apache/fory/builder/UnsafeCodegenSupport.java
new file mode 100644
index 0000000000..b5ae3242cf
--- /dev/null
+++ b/java/fory-core/src/main/java/org/apache/fory/builder/UnsafeCodegenSupport.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fory.builder;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import org.apache.fory.annotation.Internal;
+import org.apache.fory.platform.JdkVersion;
+
+/** Internal bridge for JDK8-24 generated code that still uses Unsafe fast paths. */
+@Internal
+public final class UnsafeCodegenSupport {
+ private static final Object UNSAFE;
+ private static final Method OBJECT_FIELD_OFFSET;
+
+ static {
+ if (JdkVersion.MAJOR_VERSION >= 25) {
+ UNSAFE = null;
+ OBJECT_FIELD_OFFSET = null;
+ } else {
+ try {
+ Class> unsafeClass = Class.forName("sun.misc.Unsafe");
+ Field unsafeField = unsafeClass.getDeclaredField("theUnsafe");
+ unsafeField.setAccessible(true);
+ UNSAFE = unsafeField.get(null);
+ OBJECT_FIELD_OFFSET = unsafeClass.getMethod("objectFieldOffset", Field.class);
+ } catch (ReflectiveOperationException e) {
+ throw new UnsupportedOperationException("Unsafe is not supported in this platform.", e);
+ }
+ }
+ }
+
+ private UnsafeCodegenSupport() {}
+
+ public static Object unsafe() {
+ checkUnsafeSupported();
+ return UNSAFE;
+ }
+
+ public static long objectFieldOffset(Field field) {
+ checkUnsafeSupported();
+ try {
+ return (long) OBJECT_FIELD_OFFSET.invoke(UNSAFE, field);
+ } catch (IllegalAccessException e) {
+ throw new IllegalStateException("Unsafe objectFieldOffset is not accessible.", e);
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException(e.getCause());
+ }
+ }
+
+ static String unsafeTypeName() {
+ checkUnsafeSupported();
+ return "sun.misc.Unsafe";
+ }
+
+ public static String unsafeInitCode() {
+ checkUnsafeSupported();
+ return "((sun.misc.Unsafe) " + UnsafeCodegenSupport.class.getName() + ".unsafe())";
+ }
+
+ private static void checkUnsafeSupported() {
+ if (JdkVersion.MAJOR_VERSION >= 25) {
+ throw new UnsupportedOperationException("Generated Unsafe access is unsupported on JDK25+");
+ }
+ }
+}
diff --git a/java/fory-core/src/main/java/org/apache/fory/codegen/CodeGenerator.java b/java/fory-core/src/main/java/org/apache/fory/codegen/CodeGenerator.java
index 76b3a311f6..f63e4151c9 100644
--- a/java/fory-core/src/main/java/org/apache/fory/codegen/CodeGenerator.java
+++ b/java/fory-core/src/main/java/org/apache/fory/codegen/CodeGenerator.java
@@ -40,6 +40,7 @@
import org.apache.fory.logging.LoggerFactory;
import org.apache.fory.platform.AndroidSupport;
import org.apache.fory.platform.GraalvmSupport;
+import org.apache.fory.platform.internal.DefineClass;
import org.apache.fory.reflect.ReflectionUtils;
import org.apache.fory.util.ClassLoaderUtils;
import org.apache.fory.util.ClassLoaderUtils.ByteArrayClassLoader;
@@ -135,6 +136,46 @@ public ClassLoader compile(List units, CompileCallback callback) {
}
parentClassLoader = classLoader;
}
+ Map classes = compileToBytecode(compileUnits, parentClassLoader, callback);
+ return defineClasses(classes);
+ }
+
+ public Class> compileAndLoad(CompileUnit unit, CompileCallback callback) {
+ Class> neighborClass = unit.getNeighborClass();
+ if (neighborClass == null) {
+ ClassLoader loader = compile(java.util.Collections.singletonList(unit), callback);
+ try {
+ return loader.loadClass(unit.getQualifiedClassName());
+ } catch (ClassNotFoundException e) {
+ throw new IllegalStateException("Impossible because we just compiled class", e);
+ }
+ }
+ checkRuntimeCodegenSupported();
+ DefineState defineState = getDefineState(unit.getQualifiedClassName());
+ if (defineState.definedClass != null) {
+ return defineState.definedClass;
+ }
+ synchronized (defineState.lock) {
+ if (defineState.definedClass != null) {
+ return defineState.definedClass;
+ }
+ ClassLoader parentClassLoader = getClassLoader();
+ Map classes =
+ compileToBytecode(java.util.Collections.singletonList(unit), parentClassLoader, callback);
+ byte[] bytecodes = classes.get(classFilepath(unit));
+ if (bytecodes == null) {
+ throw new IllegalStateException(
+ "Compiler did not produce bytecode for " + unit.getQualifiedClassName());
+ }
+ Class> definedClass = DefineClass.defineHiddenNestmate(neighborClass, bytecodes);
+ defineState.definedClass = definedClass;
+ defineState.defined = true;
+ return definedClass;
+ }
+ }
+
+ private Map compileToBytecode(
+ List compileUnits, ClassLoader parentClassLoader, CompileCallback callback) {
CompileState compileState = getCompileState(compileUnits);
callback.lock(compileState);
Map classes;
@@ -150,12 +191,8 @@ public ClassLoader compile(List units, CompileCallback callback) {
} finally {
compileState.lock.unlock();
}
- for (Map.Entry e : classes.entrySet()) {
- String key = e.getKey();
- byte[] value = e.getValue();
- }
}
- return defineClasses(classes);
+ return classes;
}
/**
@@ -305,6 +342,7 @@ private String getCompileLockKey(List toCompile) {
private static class DefineState {
final Object lock;
volatile boolean defined;
+ volatile Class> definedClass;
private DefineState() {
this.lock = new Object();
@@ -411,7 +449,7 @@ public static String classFilepath(String fullClassName) {
}
public static String fullClassName(CompileUnit unit) {
- return unit.pkg + "." + unit.mainClassName;
+ return unit.getQualifiedClassName();
}
public static String fullClassNameFromClassFilePath(String classFilePath) {
@@ -526,10 +564,40 @@ private static boolean classSourcePublicAccessible(Class> clz) {
if (clz.isPrimitive()) {
return true;
}
- if (!ReflectionUtils.isPublic(clz)) {
+ if (!sourcePkgLevelAccessible(clz)) {
return false;
}
- return sourcePkgLevelAccessible(clz);
+ Class> current = clz;
+ while (current != null) {
+ if (!ReflectionUtils.isPublic(current)) {
+ return false;
+ }
+ current = current.getEnclosingClass();
+ }
+ return true;
+ }
+
+ public static boolean sourceAccessibleFrom(Class> clz, String pkg) {
+ if (sourcePublicAccessible(clz)) {
+ return true;
+ }
+ if (clz.isArray()) {
+ return sourceAccessibleFrom(clz.getComponentType(), pkg);
+ }
+ if (!sourcePkgLevelAccessible(clz)) {
+ return false;
+ }
+ if (!ReflectionUtils.getPackage(clz).equals(pkg)) {
+ return false;
+ }
+ Class> current = clz;
+ while (current != null) {
+ if (ReflectionUtils.isPrivate(current)) {
+ return false;
+ }
+ current = current.getEnclosingClass();
+ }
+ return true;
}
private static final Map, Boolean> sourcePkgLevelAccessible =
diff --git a/java/fory-core/src/main/java/org/apache/fory/codegen/CodegenContext.java b/java/fory-core/src/main/java/org/apache/fory/codegen/CodegenContext.java
index ccfed32371..779d7d9cf3 100644
--- a/java/fory-core/src/main/java/org/apache/fory/codegen/CodegenContext.java
+++ b/java/fory-core/src/main/java/org/apache/fory/codegen/CodegenContext.java
@@ -140,6 +140,7 @@ public class CodegenContext {
String pkg;
LinkedHashSet imports = new LinkedHashSet<>();
String className;
+ private boolean samePackageAccess;
String classModifiers = "public final";
String[] superClasses;
String[] interfaces;
@@ -355,6 +356,17 @@ public String getPackage() {
return pkg;
}
+ public void setSamePackageAccess(boolean samePackageAccess) {
+ if (this.samePackageAccess != samePackageAccess) {
+ sourcePublicAccessibleCache.clear();
+ }
+ this.samePackageAccess = samePackageAccess;
+ }
+
+ public boolean hasSamePackageAccess() {
+ return samePackageAccess;
+ }
+
public Set getValNames() {
return valNames;
}
@@ -729,7 +741,12 @@ public String optimizeMethodCode(String code) {
/** Returns true if class is public accessible from source. */
public boolean sourcePublicAccessible(Class> clz) {
- return sourcePublicAccessibleCache.computeIfAbsent(clz, CodeGenerator::sourcePublicAccessible);
+ return sourcePublicAccessibleCache.computeIfAbsent(
+ clz,
+ c ->
+ samePackageAccess
+ ? CodeGenerator.sourceAccessibleFrom(c, pkg)
+ : CodeGenerator.sourcePublicAccessible(c));
}
/** Returns true if class is package level accessible from source. */
diff --git a/java/fory-core/src/main/java/org/apache/fory/codegen/CompileUnit.java b/java/fory-core/src/main/java/org/apache/fory/codegen/CompileUnit.java
index f442c71c52..d90b6fdae4 100644
--- a/java/fory-core/src/main/java/org/apache/fory/codegen/CompileUnit.java
+++ b/java/fory-core/src/main/java/org/apache/fory/codegen/CompileUnit.java
@@ -31,19 +31,33 @@ public class CompileUnit {
String pkg;
String mainClassName;
+ // Non-null only when the compiled class must be defined as a JDK25+ hidden nestmate of this
+ // neighbor. Ordinary generated classes still use the CodeGenerator classloader path.
+ private final Class> neighborClass;
private String code;
private Supplier genCodeFunc;
public CompileUnit(String pkg, String mainClassName, String code) {
+ this(pkg, mainClassName, code, null);
+ }
+
+ public CompileUnit(String pkg, String mainClassName, String code, Class> neighborClass) {
this.pkg = pkg;
this.mainClassName = mainClassName;
this.code = code;
+ this.neighborClass = neighborClass;
}
public CompileUnit(String pkg, String mainClassName, Supplier genCodeFunc) {
+ this(pkg, mainClassName, genCodeFunc, null);
+ }
+
+ public CompileUnit(
+ String pkg, String mainClassName, Supplier genCodeFunc, Class> neighborClass) {
this.pkg = pkg;
this.mainClassName = mainClassName;
this.genCodeFunc = genCodeFunc;
+ this.neighborClass = neighborClass;
}
public String getCode() {
@@ -65,6 +79,10 @@ public String getQualifiedClassName() {
}
}
+ public Class> getNeighborClass() {
+ return neighborClass;
+ }
+
@Override
public String toString() {
return "CompileUnit{" + "pkg='" + pkg + '\'' + ", mainClassName='" + mainClassName + '\'' + '}';
diff --git a/java/fory-core/src/main/java/org/apache/fory/codegen/Expression.java b/java/fory-core/src/main/java/org/apache/fory/codegen/Expression.java
index d7cdfcb91a..0017b83400 100644
--- a/java/fory-core/src/main/java/org/apache/fory/codegen/Expression.java
+++ b/java/fory-core/src/main/java/org/apache/fory/codegen/Expression.java
@@ -59,7 +59,10 @@
import java.util.Locale;
import java.util.stream.Collectors;
import java.util.stream.Stream;
-import org.apache.fory.platform.UnsafeOps;
+import org.apache.fory.builder.UnsafeCodegenSupport;
+import org.apache.fory.platform.JdkVersion;
+import org.apache.fory.reflect.ObjectInstantiator;
+import org.apache.fory.reflect.ObjectInstantiators;
import org.apache.fory.reflect.ReflectionUtils;
import org.apache.fory.reflect.TypeRef;
import org.apache.fory.type.TypeUtils;
@@ -1498,14 +1501,31 @@ public ExprCode doGenCode(CodegenContext ctx) {
if (arguments.isEmpty() && !ReflectionUtils.hasPublicNoArgConstructor(rawType)) {
// janino doesn't generics, so we cast manually.
String instance = ctx.newName("instance");
+ String target;
+ String functionName;
+ String args;
+ if (JdkVersion.MAJOR_VERSION >= 25) {
+ String instantiator = ctx.newName("objectInstantiator");
+ codeBuilder
+ .append(
+ ExpressionUtils.callFunc(
+ ctx.type(ObjectInstantiator.class),
+ instantiator,
+ ctx.type(ObjectInstantiators.class),
+ "getObjectInstantiator",
+ clzName + ".class",
+ false))
+ .append('\n');
+ target = instantiator;
+ functionName = "newInstance";
+ args = "";
+ } else {
+ target = UnsafeCodegenSupport.unsafeInitCode();
+ functionName = "allocateInstance";
+ args = clzName + ".class";
+ }
String code =
- ExpressionUtils.callFunc(
- "Object",
- instance,
- ctx.type(UnsafeOps.class),
- "newInstance",
- clzName + ".class",
- false);
+ ExpressionUtils.callFunc("Object", instance, target, functionName, args, true);
codeBuilder.append(code).append('\n');
String cast =
StringUtils.format(
diff --git a/java/fory-core/src/main/java/org/apache/fory/codegen/ExpressionOptimizer.java b/java/fory-core/src/main/java/org/apache/fory/codegen/ExpressionOptimizer.java
index a9c3568e6f..68eae904bd 100644
--- a/java/fory-core/src/main/java/org/apache/fory/codegen/ExpressionOptimizer.java
+++ b/java/fory-core/src/main/java/org/apache/fory/codegen/ExpressionOptimizer.java
@@ -68,13 +68,12 @@ public static Expression invokeGenerated(
Expression groupExpressions,
String methodPrefix,
boolean inlineInvoke) {
+ // Janino lowers private instance helpers to static bridge methods with the original binary
+ // class as receiver. Hidden classes have a different runtime identity, so JDK25 hidden
+ // generated serializers must keep split helpers non-private.
+ String modifier = ctx.hasSamePackageAccess() ? "final" : "private";
return invokeGenerated(
- ctx,
- new LinkedHashSet<>(cutPoint),
- groupExpressions,
- "private",
- methodPrefix,
- inlineInvoke);
+ ctx, new LinkedHashSet<>(cutPoint), groupExpressions, modifier, methodPrefix, inlineInvoke);
}
/**
@@ -133,6 +132,7 @@ public static Expression invokeGenerated(
// instance field name.
CodegenContext codegenContext =
new CodegenContext(ctx.getPackage(), ctx.getValNames(), ctx.getImports());
+ codegenContext.setSamePackageAccess(ctx.hasSamePackageAccess());
for (Reference reference : cutExprMap.values()) {
Preconditions.checkArgument(codegenContext.containName(reference.name()));
}
diff --git a/java/fory-core/src/main/java/org/apache/fory/memory/ByteBufferUtil.java b/java/fory-core/src/main/java/org/apache/fory/memory/ByteBufferUtil.java
index 372699b8be..c43fc255e7 100644
--- a/java/fory-core/src/main/java/org/apache/fory/memory/ByteBufferUtil.java
+++ b/java/fory-core/src/main/java/org/apache/fory/memory/ByteBufferUtil.java
@@ -19,40 +19,13 @@
package org.apache.fory.memory;
-import java.lang.reflect.Field;
import java.nio.Buffer;
import java.nio.ByteBuffer;
-import org.apache.fory.platform.UnsafeOps;
-import org.apache.fory.util.Preconditions;
public class ByteBufferUtil {
public static final Class> HEAP_BYTE_BUFFER_CLASS = ByteBuffer.allocate(0).getClass();
public static final Class> DIRECT_BYTE_BUFFER_CLASS = ByteBuffer.allocateDirect(0).getClass();
- private static final class DirectBufferAccess {
- private static final long BUFFER_ADDRESS_FIELD_OFFSET;
-
- static {
- try {
- Field addressField = Buffer.class.getDeclaredField("address");
- BUFFER_ADDRESS_FIELD_OFFSET = UnsafeOps.objectFieldOffset(addressField);
- Preconditions.checkArgument(BUFFER_ADDRESS_FIELD_OFFSET != 0);
- } catch (NoSuchFieldException e) {
- throw new IllegalStateException(e);
- }
- }
- }
-
- static long getAddress(ByteBuffer buffer) {
- Preconditions.checkNotNull(buffer, "buffer is null");
- Preconditions.checkArgument(buffer.isDirect(), "Can't get address of a non-direct ByteBuffer.");
- try {
- return UnsafeOps.getLong(buffer, DirectBufferAccess.BUFFER_ADDRESS_FIELD_OFFSET);
- } catch (Throwable t) {
- throw new Error("Could not access direct byte buffer address field.", t);
- }
- }
-
public static void clearBuffer(Buffer buffer) {
buffer.clear();
}
diff --git a/java/fory-core/src/main/java/org/apache/fory/memory/LittleEndian.java b/java/fory-core/src/main/java/org/apache/fory/memory/LittleEndian.java
index fa0a831eff..4cb7b5ef6a 100644
--- a/java/fory-core/src/main/java/org/apache/fory/memory/LittleEndian.java
+++ b/java/fory-core/src/main/java/org/apache/fory/memory/LittleEndian.java
@@ -1,7 +1,8 @@
package org.apache.fory.memory;
import org.apache.fory.platform.AndroidSupport;
-import org.apache.fory.platform.UnsafeOps;
+import org.apache.fory.platform.internal._UnsafeUtils;
+import sun.misc.Unsafe;
/*
* Licensed to the Apache Software Foundation (ASF) under one
@@ -23,6 +24,18 @@
*/
public class LittleEndian {
+ private static final Unsafe UNSAFE = AndroidSupport.IS_ANDROID ? null : _UnsafeUtils.UNSAFE;
+ private static final int BYTE_ARRAY_OFFSET;
+
+ // Keep arrayBaseOffset as a direct static-field store for GraalVM native-image recomputation.
+ static {
+ if (AndroidSupport.IS_ANDROID) {
+ BYTE_ARRAY_OFFSET = 0;
+ } else {
+ BYTE_ARRAY_OFFSET = UNSAFE.arrayBaseOffset(byte[].class);
+ }
+ }
+
public static int putVarUint36Small(byte[] arr, int index, long v) {
if (v >>> 7 == 0) {
arr[index] = (byte) v;
@@ -58,28 +71,13 @@ private static int bigWriteUint36(byte[] arr, int index, long v) {
return 5;
}
- public static void putInt32(Object o, long pos, int value) {
- if (!NativeByteOrder.IS_LITTLE_ENDIAN) {
- value = Integer.reverseBytes(value);
- }
- UnsafeOps.putInt(o, pos, value);
- }
-
- public static int getInt32(Object o, long pos) {
- int i = UnsafeOps.getInt(o, pos);
- return NativeByteOrder.IS_LITTLE_ENDIAN ? i : Integer.reverseBytes(i);
- }
-
- public static long getInt64(Object o, long pos) {
- long v = UnsafeOps.getLong(o, pos);
- return NativeByteOrder.IS_LITTLE_ENDIAN ? v : Long.reverseBytes(v);
- }
-
public static long getInt64(byte[] o, int index) {
if (AndroidSupport.IS_ANDROID) {
return MemoryOps.getInt64(o, index);
}
- long v = UnsafeOps.getLong(o, UnsafeOps.BYTE_ARRAY_OFFSET + index);
+ // Unsafe object offsets are long. Keep the cast so JDK8-compiled bytecode calls
+ // getLong(Object, long) when the artifact runs on JDK9+.
+ long v = UNSAFE.getLong(o, (long) BYTE_ARRAY_OFFSET + index);
return NativeByteOrder.IS_LITTLE_ENDIAN ? v : Long.reverseBytes(v);
}
@@ -91,22 +89,7 @@ public static void putInt64(byte[] o, int index, long value) {
if (!NativeByteOrder.IS_LITTLE_ENDIAN) {
value = Long.reverseBytes(value);
}
- UnsafeOps.putLong(o, UnsafeOps.BYTE_ARRAY_OFFSET + index, value);
- }
-
- public static void putFloat32(Object o, long pos, float value) {
- int v = Float.floatToRawIntBits(value);
- if (!NativeByteOrder.IS_LITTLE_ENDIAN) {
- v = Integer.reverseBytes(v);
- }
- UnsafeOps.putInt(o, pos, v);
- }
-
- public static void putFloat64(Object o, long pos, double value) {
- long v = Double.doubleToRawLongBits(value);
- if (!NativeByteOrder.IS_LITTLE_ENDIAN) {
- v = Long.reverseBytes(v);
- }
- UnsafeOps.putLong(o, pos, v);
+ // See getInt64: the cast controls the Unsafe method descriptor in bytecode.
+ UNSAFE.putLong(o, (long) BYTE_ARRAY_OFFSET + index, value);
}
}
diff --git a/java/fory-core/src/main/java/org/apache/fory/memory/MemoryBuffer.java b/java/fory-core/src/main/java/org/apache/fory/memory/MemoryBuffer.java
index c6388f1041..0b273e3976 100644
--- a/java/fory-core/src/main/java/org/apache/fory/memory/MemoryBuffer.java
+++ b/java/fory-core/src/main/java/org/apache/fory/memory/MemoryBuffer.java
@@ -19,14 +19,18 @@
package org.apache.fory.memory;
import static org.apache.fory.util.Preconditions.checkArgument;
+import static org.apache.fory.util.Preconditions.checkNotNull;
+import java.lang.reflect.Field;
+import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.util.Arrays;
import org.apache.fory.annotation.CodegenInvoke;
import org.apache.fory.io.AbstractStreamReader;
import org.apache.fory.io.ForyStreamReader;
import org.apache.fory.platform.AndroidSupport;
-import org.apache.fory.platform.UnsafeOps;
+import org.apache.fory.platform.JdkVersion;
+import org.apache.fory.platform.internal._UnsafeUtils;
import sun.misc.Unsafe;
/**
@@ -62,11 +66,98 @@
*/
public final class MemoryBuffer {
public static final int BUFFER_GROW_STEP_THRESHOLD = 100 * 1024 * 1024;
- private static final Unsafe UNSAFE = AndroidSupport.IS_ANDROID ? null : UnsafeOps.UNSAFE;
+ private static final Unsafe UNSAFE = AndroidSupport.IS_ANDROID ? null : _UnsafeUtils.UNSAFE;
private static final boolean LITTLE_ENDIAN = NativeByteOrder.IS_LITTLE_ENDIAN;
+ private static final boolean UNALIGNED = !AndroidSupport.IS_ANDROID && unaligned();
+ private static final int BOOLEAN_ARRAY_OFFSET;
+ private static final int BYTE_ARRAY_OFFSET;
+ private static final int CHAR_ARRAY_OFFSET;
+ private static final int SHORT_ARRAY_OFFSET;
+ private static final int INT_ARRAY_OFFSET;
+ private static final int LONG_ARRAY_OFFSET;
+ private static final int FLOAT_ARRAY_OFFSET;
+ private static final int DOUBLE_ARRAY_OFFSET;
+
+ // GraalVM native-image recognizes arrayBaseOffset only when the call stores directly into the
+ // target static field. Keep these assignments in this shape so native images recompute heap array
+ // offsets for the image runtime instead of embedding build-time VM offsets.
+ static {
+ if (AndroidSupport.IS_ANDROID) {
+ BOOLEAN_ARRAY_OFFSET = 0;
+ BYTE_ARRAY_OFFSET = 0;
+ CHAR_ARRAY_OFFSET = 0;
+ SHORT_ARRAY_OFFSET = 0;
+ INT_ARRAY_OFFSET = 0;
+ LONG_ARRAY_OFFSET = 0;
+ FLOAT_ARRAY_OFFSET = 0;
+ DOUBLE_ARRAY_OFFSET = 0;
+ } else {
+ BOOLEAN_ARRAY_OFFSET = UNSAFE.arrayBaseOffset(boolean[].class);
+ BYTE_ARRAY_OFFSET = UNSAFE.arrayBaseOffset(byte[].class);
+ CHAR_ARRAY_OFFSET = UNSAFE.arrayBaseOffset(char[].class);
+ SHORT_ARRAY_OFFSET = UNSAFE.arrayBaseOffset(short[].class);
+ INT_ARRAY_OFFSET = UNSAFE.arrayBaseOffset(int[].class);
+ LONG_ARRAY_OFFSET = UNSAFE.arrayBaseOffset(long[].class);
+ FLOAT_ARRAY_OFFSET = UNSAFE.arrayBaseOffset(float[].class);
+ DOUBLE_ARRAY_OFFSET = UNSAFE.arrayBaseOffset(double[].class);
+ }
+ }
+
+ /** Limits each raw Unsafe copy to let large copies hit safepoint polls between chunks. */
+ private static final long UNSAFE_COPY_THRESHOLD = 1024L * 1024L;
+
+ private static final long BUFFER_ADDRESS_FIELD_OFFSET =
+ AndroidSupport.IS_ANDROID ? -1 : bufferAddressFieldOffset();
+
// Global allocator instance that can be customized
private static volatile MemoryAllocator globalAllocator = new DefaultMemoryAllocator();
+ private static long bufferAddressFieldOffset() {
+ try {
+ Field addressField = Buffer.class.getDeclaredField("address");
+ long offset = UNSAFE.objectFieldOffset(addressField);
+ checkArgument(offset != 0);
+ return offset;
+ } catch (NoSuchFieldException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private static boolean unaligned() {
+ String arch = System.getProperty("os.arch", "");
+ if ("ppc64le".equals(arch) || "ppc64".equals(arch) || "s390x".equals(arch)) {
+ return true;
+ }
+ try {
+ Class> bitsClass =
+ Class.forName("java.nio.Bits", false, ClassLoader.getSystemClassLoader());
+ if (JdkVersion.MAJOR_VERSION >= 9) {
+ Field unalignedField =
+ bitsClass.getDeclaredField(JdkVersion.MAJOR_VERSION >= 11 ? "UNALIGNED" : "unaligned");
+ return UNSAFE.getBoolean(
+ UNSAFE.staticFieldBase(unalignedField), UNSAFE.staticFieldOffset(unalignedField));
+ }
+ return Boolean.TRUE.equals(bitsClass.getDeclaredMethod("unaligned").invoke(null));
+ } catch (Throwable t) {
+ return arch.matches("^(i[3-6]86|x86(_64)?|x64|amd64|aarch64)$");
+ }
+ }
+
+ private static void copyMemory(
+ Object src, long srcOffset, Object dst, long dstOffset, long length) {
+ if (length < UNSAFE_COPY_THRESHOLD) {
+ UNSAFE.copyMemory(src, srcOffset, dst, dstOffset, length);
+ } else {
+ while (length > 0) {
+ long size = Math.min(length, UNSAFE_COPY_THRESHOLD);
+ UNSAFE.copyMemory(src, srcOffset, dst, dstOffset, size);
+ length -= size;
+ srcOffset += size;
+ dstOffset += size;
+ }
+ }
+ }
+
// If the data in on the heap, `heapMemory` will be non-null, and its' the object relative to
// which we access the memory.
// If we have this buffer, we must never void this reference, or the memory buffer will point
@@ -186,12 +277,22 @@ private void initOffHeapBuffer(long offHeapAddress, int size, ByteBuffer offHeap
this.size = size;
}
+ private static long getAddress(ByteBuffer buffer) {
+ checkNotNull(buffer, "buffer is null");
+ checkArgument(buffer.isDirect(), "Can't get address of a non-direct ByteBuffer.");
+ try {
+ return UNSAFE.getLong(buffer, BUFFER_ADDRESS_FIELD_OFFSET);
+ } catch (Throwable t) {
+ throw new Error("Could not access direct byte buffer address field.", t);
+ }
+ }
+
public void initByteBuffer(ByteBuffer buffer, int size) {
if (buffer.isDirect()) {
if (AndroidSupport.IS_ANDROID) {
MemoryOps.throwDirectByteBufferUnsupported();
} else {
- initOffHeapBuffer(ByteBufferUtil.getAddress(buffer), size, buffer);
+ initOffHeapBuffer(getAddress(buffer), size, buffer);
}
} else if (buffer.hasArray()) {
initHeapBuffer(buffer.array(), buffer.arrayOffset(), size);
@@ -240,7 +341,7 @@ public void initHeapBuffer(byte[] buffer, int offset, int length) {
}
this.heapMemory = buffer;
this.heapOffset = offset;
- final long startPos = UnsafeOps.BYTE_ARRAY_OFFSET + offset;
+ final long startPos = BYTE_ARRAY_OFFSET + offset;
this.address = startPos;
this.size = length;
this.addressLimit = startPos + length;
@@ -351,7 +452,7 @@ public void get(int index, byte[] dst, int offset, int length) {
< 0) {
throwOOBException();
}
- UnsafeOps.copyMemory(null, pos, dst, UnsafeOps.BYTE_ARRAY_OFFSET + offset, length);
+ copyMemory(null, pos, dst, BYTE_ARRAY_OFFSET + offset, length);
}
}
@@ -369,10 +470,10 @@ public void get(int offset, ByteBuffer target, int numBytes) {
if (AndroidSupport.IS_ANDROID) {
MemoryOps.get(this, offset, target, numBytes);
} else if (target.isDirect()) {
- final long targetAddr = ByteBufferUtil.getAddress(target) + targetPos;
+ final long targetAddr = getAddress(target) + targetPos;
final long sourceAddr = address + offset;
if (sourceAddr <= addressLimit - numBytes) {
- UnsafeOps.copyMemory(heapMemory, sourceAddr, null, targetAddr, numBytes);
+ copyMemory(heapMemory, sourceAddr, null, targetAddr, numBytes);
} else {
throwOOBException();
}
@@ -394,10 +495,10 @@ public void put(int offset, ByteBuffer source, int numBytes) {
if (AndroidSupport.IS_ANDROID) {
MemoryOps.put(this, offset, source, numBytes);
} else if (source.isDirect()) {
- final long sourceAddr = ByteBufferUtil.getAddress(source) + sourcePos;
+ final long sourceAddr = getAddress(source) + sourcePos;
final long targetAddr = address + offset;
if (targetAddr <= addressLimit - numBytes) {
- UnsafeOps.copyMemory(null, sourceAddr, heapMemory, targetAddr, numBytes);
+ copyMemory(null, sourceAddr, heapMemory, targetAddr, numBytes);
} else {
throwOOBException();
}
@@ -431,8 +532,8 @@ public void put(int index, byte[] src, int offset, int length) {
< 0) {
throwOOBException();
}
- final long arrayAddress = UnsafeOps.BYTE_ARRAY_OFFSET + offset;
- UnsafeOps.copyMemory(src, arrayAddress, null, pos, length);
+ final long arrayAddress = BYTE_ARRAY_OFFSET + offset;
+ copyMemory(src, arrayAddress, null, pos, length);
}
}
@@ -1660,12 +1761,8 @@ public void writeBooleans(boolean[] values, int offset, int numElements) {
final int writerIdx = writerIndex;
final int newIdx = writerIdx + numElements;
ensure(newIdx);
- UNSAFE.copyMemory(
- values,
- UnsafeOps.BOOLEAN_ARRAY_OFFSET + offset,
- heapMemory,
- address + writerIdx,
- numElements);
+ copyMemory(
+ values, BOOLEAN_ARRAY_OFFSET + offset, heapMemory, address + writerIdx, numElements);
writerIndex = newIdx;
}
}
@@ -1692,9 +1789,9 @@ public void writeChars(char[] values, int offset, int numElements) {
final int writerIdx = writerIndex;
final int newIdx = writerIdx + numBytes;
ensure(newIdx);
- UNSAFE.copyMemory(
+ copyMemory(
values,
- UnsafeOps.CHAR_ARRAY_OFFSET + ((long) offset << 1),
+ CHAR_ARRAY_OFFSET + ((long) offset << 1),
heapMemory,
address + writerIdx,
numBytes);
@@ -1724,9 +1821,9 @@ public void writeShorts(short[] values, int offset, int numElements) {
final int writerIdx = writerIndex;
final int newIdx = writerIdx + numBytes;
ensure(newIdx);
- UNSAFE.copyMemory(
+ copyMemory(
values,
- UnsafeOps.SHORT_ARRAY_OFFSET + ((long) offset << 1),
+ SHORT_ARRAY_OFFSET + ((long) offset << 1),
heapMemory,
address + writerIdx,
numBytes);
@@ -1756,9 +1853,9 @@ public void writeInts(int[] values, int offset, int numElements) {
final int writerIdx = writerIndex;
final int newIdx = writerIdx + numBytes;
ensure(newIdx);
- UNSAFE.copyMemory(
+ copyMemory(
values,
- UnsafeOps.INT_ARRAY_OFFSET + ((long) offset << 2),
+ INT_ARRAY_OFFSET + ((long) offset << 2),
heapMemory,
address + writerIdx,
numBytes);
@@ -1788,9 +1885,9 @@ public void writeLongs(long[] values, int offset, int numElements) {
final int writerIdx = writerIndex;
final int newIdx = writerIdx + numBytes;
ensure(newIdx);
- UNSAFE.copyMemory(
+ copyMemory(
values,
- UnsafeOps.LONG_ARRAY_OFFSET + ((long) offset << 3),
+ LONG_ARRAY_OFFSET + ((long) offset << 3),
heapMemory,
address + writerIdx,
numBytes);
@@ -1820,9 +1917,9 @@ public void writeFloats(float[] values, int offset, int numElements) {
final int writerIdx = writerIndex;
final int newIdx = writerIdx + numBytes;
ensure(newIdx);
- UNSAFE.copyMemory(
+ copyMemory(
values,
- UnsafeOps.FLOAT_ARRAY_OFFSET + ((long) offset << 2),
+ FLOAT_ARRAY_OFFSET + ((long) offset << 2),
heapMemory,
address + writerIdx,
numBytes);
@@ -1852,9 +1949,9 @@ public void writeDoubles(double[] values, int offset, int numElements) {
final int writerIdx = writerIndex;
final int newIdx = writerIdx + numBytes;
ensure(newIdx);
- UNSAFE.copyMemory(
+ copyMemory(
values,
- UnsafeOps.DOUBLE_ARRAY_OFFSET + ((long) offset << 3),
+ DOUBLE_ARRAY_OFFSET + ((long) offset << 3),
heapMemory,
address + writerIdx,
numBytes);
@@ -3028,7 +3125,7 @@ public byte[] readBytes(int length) {
// System.arraycopy faster for some jdk than Unsafe.
System.arraycopy(heapMemory, heapOffset + readerIdx, bytes, 0, length);
} else {
- UnsafeOps.copyMemory(null, address + readerIdx, bytes, UnsafeOps.BYTE_ARRAY_OFFSET, length);
+ copyMemory(null, address + readerIdx, bytes, BYTE_ARRAY_OFFSET, length);
}
readerIndex = readerIdx + length;
return bytes;
@@ -3198,8 +3295,7 @@ public byte[] readBytesAndSize() {
if (heapMemory != null) {
System.arraycopy(heapMemory, heapOffset + readerIdx, arr, 0, numBytes);
} else {
- UnsafeOps.UNSAFE.copyMemory(
- null, address + readerIdx, arr, UnsafeOps.BYTE_ARRAY_OFFSET, numBytes);
+ copyMemory(null, address + readerIdx, arr, BYTE_ARRAY_OFFSET, numBytes);
}
readerIndex = readerIdx + numBytes;
return arr;
@@ -3223,7 +3319,7 @@ public void readByteArrayPayload(byte[] values, int numBytes) {
if (heapMemory != null) {
System.arraycopy(heapMemory, heapOffset + readerIdx, values, 0, numBytes);
} else {
- UNSAFE.copyMemory(null, address + readerIdx, values, UnsafeOps.BYTE_ARRAY_OFFSET, numBytes);
+ copyMemory(null, address + readerIdx, values, BYTE_ARRAY_OFFSET, numBytes);
}
readerIndex = readerIdx + numBytes;
}
@@ -3243,8 +3339,7 @@ public void readBooleanArrayPayload(boolean[] values, int numBytes) {
streamReader.readBooleans(values, 0, numBytes);
return;
}
- UNSAFE.copyMemory(
- heapMemory, address + readerIdx, values, UnsafeOps.BOOLEAN_ARRAY_OFFSET, numBytes);
+ copyMemory(heapMemory, address + readerIdx, values, BOOLEAN_ARRAY_OFFSET, numBytes);
readerIndex = readerIdx + numBytes;
}
}
@@ -3263,8 +3358,7 @@ public void readCharArrayPayload(char[] values, int numBytes) {
streamReader.readChars(values, 0, numBytes >>> 1);
return;
}
- UNSAFE.copyMemory(
- heapMemory, address + readerIdx, values, UnsafeOps.CHAR_ARRAY_OFFSET, numBytes);
+ copyMemory(heapMemory, address + readerIdx, values, CHAR_ARRAY_OFFSET, numBytes);
readerIndex = readerIdx + numBytes;
}
}
@@ -3283,8 +3377,7 @@ public void readInt16ArrayPayload(short[] values, int numBytes) {
streamReader.readShorts(values, 0, numBytes >>> 1);
return;
}
- UNSAFE.copyMemory(
- heapMemory, address + readerIdx, values, UnsafeOps.SHORT_ARRAY_OFFSET, numBytes);
+ copyMemory(heapMemory, address + readerIdx, values, SHORT_ARRAY_OFFSET, numBytes);
readerIndex = readerIdx + numBytes;
}
}
@@ -3303,8 +3396,7 @@ public void readInt32ArrayPayload(int[] values, int numBytes) {
streamReader.readInts(values, 0, numBytes >>> 2);
return;
}
- UNSAFE.copyMemory(
- heapMemory, address + readerIdx, values, UnsafeOps.INT_ARRAY_OFFSET, numBytes);
+ copyMemory(heapMemory, address + readerIdx, values, INT_ARRAY_OFFSET, numBytes);
readerIndex = readerIdx + numBytes;
}
}
@@ -3323,8 +3415,7 @@ public void readInt64ArrayPayload(long[] values, int numBytes) {
streamReader.readLongs(values, 0, numBytes >>> 3);
return;
}
- UNSAFE.copyMemory(
- heapMemory, address + readerIdx, values, UnsafeOps.LONG_ARRAY_OFFSET, numBytes);
+ copyMemory(heapMemory, address + readerIdx, values, LONG_ARRAY_OFFSET, numBytes);
readerIndex = readerIdx + numBytes;
}
}
@@ -3343,8 +3434,7 @@ public void readFloat32ArrayPayload(float[] values, int numBytes) {
streamReader.readFloats(values, 0, numBytes >>> 2);
return;
}
- UNSAFE.copyMemory(
- heapMemory, address + readerIdx, values, UnsafeOps.FLOAT_ARRAY_OFFSET, numBytes);
+ copyMemory(heapMemory, address + readerIdx, values, FLOAT_ARRAY_OFFSET, numBytes);
readerIndex = readerIdx + numBytes;
}
}
@@ -3363,8 +3453,7 @@ public void readFloat64ArrayPayload(double[] values, int numBytes) {
streamReader.readDoubles(values, 0, numBytes >>> 3);
return;
}
- UNSAFE.copyMemory(
- heapMemory, address + readerIdx, values, UnsafeOps.DOUBLE_ARRAY_OFFSET, numBytes);
+ copyMemory(heapMemory, address + readerIdx, values, DOUBLE_ARRAY_OFFSET, numBytes);
readerIndex = readerIdx + numBytes;
}
}
@@ -3382,12 +3471,8 @@ public void readBooleans(boolean[] values, int offset, int numElements) {
return;
}
int readerIdx = readerIndex;
- UNSAFE.copyMemory(
- heapMemory,
- address + readerIdx,
- values,
- UnsafeOps.BOOLEAN_ARRAY_OFFSET + offset,
- numElements);
+ copyMemory(
+ heapMemory, address + readerIdx, values, BOOLEAN_ARRAY_OFFSET + offset, numElements);
readerIndex = readerIdx + numElements;
}
}
@@ -3410,11 +3495,11 @@ public void readChars(char[] chars, int offset, int numElements) {
return;
}
int readerIdx = readerIndex;
- UNSAFE.copyMemory(
+ copyMemory(
heapMemory,
address + readerIdx,
chars,
- UnsafeOps.CHAR_ARRAY_OFFSET + ((long) offset << 1),
+ CHAR_ARRAY_OFFSET + ((long) offset << 1),
numBytes);
readerIndex = readerIdx + numBytes;
}
@@ -3443,11 +3528,11 @@ public void readShorts(short[] values, int offset, int numElements) {
return;
}
int readerIdx = readerIndex;
- UNSAFE.copyMemory(
+ copyMemory(
heapMemory,
address + readerIdx,
values,
- UnsafeOps.SHORT_ARRAY_OFFSET + ((long) offset << 1),
+ SHORT_ARRAY_OFFSET + ((long) offset << 1),
numBytes);
readerIndex = readerIdx + numBytes;
}
@@ -3467,11 +3552,11 @@ public void readInts(int[] values, int offset, int numElements) {
return;
}
int readerIdx = readerIndex;
- UNSAFE.copyMemory(
+ copyMemory(
heapMemory,
address + readerIdx,
values,
- UnsafeOps.INT_ARRAY_OFFSET + ((long) offset << 2),
+ INT_ARRAY_OFFSET + ((long) offset << 2),
numBytes);
readerIndex = readerIdx + numBytes;
}
@@ -3491,11 +3576,11 @@ public void readLongs(long[] values, int offset, int numElements) {
return;
}
int readerIdx = readerIndex;
- UNSAFE.copyMemory(
+ copyMemory(
heapMemory,
address + readerIdx,
values,
- UnsafeOps.LONG_ARRAY_OFFSET + ((long) offset << 3),
+ LONG_ARRAY_OFFSET + ((long) offset << 3),
numBytes);
readerIndex = readerIdx + numBytes;
}
@@ -3515,11 +3600,11 @@ public void readFloats(float[] values, int offset, int numElements) {
return;
}
int readerIdx = readerIndex;
- UNSAFE.copyMemory(
+ copyMemory(
heapMemory,
address + readerIdx,
values,
- UnsafeOps.FLOAT_ARRAY_OFFSET + ((long) offset << 2),
+ FLOAT_ARRAY_OFFSET + ((long) offset << 2),
numBytes);
readerIndex = readerIdx + numBytes;
}
@@ -3539,11 +3624,11 @@ public void readDoubles(double[] values, int offset, int numElements) {
return;
}
int readerIdx = readerIndex;
- UNSAFE.copyMemory(
+ copyMemory(
heapMemory,
address + readerIdx,
values,
- UnsafeOps.DOUBLE_ARRAY_OFFSET + ((long) offset << 3),
+ DOUBLE_ARRAY_OFFSET + ((long) offset << 3),
numBytes);
readerIndex = readerIdx + numBytes;
}
@@ -3583,7 +3668,7 @@ public void copyTo(int offset, MemoryBuffer target, int targetOffset, int numByt
if ((numBytes | offset | targetOffset) >= 0
&& thisPointer <= this.addressLimit - numBytes
&& otherPointer <= target.addressLimit - numBytes) {
- UNSAFE.copyMemory(thisHeapRef, thisPointer, otherHeapRef, otherPointer, numBytes);
+ copyMemory(thisHeapRef, thisPointer, otherHeapRef, otherPointer, numBytes);
} else {
throw new IndexOutOfBoundsException(
String.format(
@@ -3597,34 +3682,230 @@ public void copyFrom(int offset, MemoryBuffer source, int sourcePointer, int num
source.copyTo(sourcePointer, this, offset, numBytes);
}
- /**
- * JVM-only bulk copy method. Copies {@code numBytes} bytes to target unsafe object and pointer.
- * Throws on Android before executing unsafe memory access.
- */
- public void copyToUnsafe(long offset, Object target, long targetPointer, int numBytes) {
+ public void copyToByteArray(int offset, byte[] target, int targetOffset, int numBytes) {
if (AndroidSupport.IS_ANDROID) {
- MemoryOps.throwRawUnsafeMemoryCopyUnsupported();
+ MemoryOps.copyToByteArray(this, offset, target, targetOffset, numBytes);
} else {
- final long thisPointer = this.address + offset;
- checkArgument(thisPointer + numBytes <= addressLimit);
- UNSAFE.copyMemory(this.heapMemory, thisPointer, target, targetPointer, numBytes);
+ checkArrayCopy(offset, targetOffset, target.length, numBytes, 0);
+ copyMemory(heapMemory, address + offset, target, BYTE_ARRAY_OFFSET + targetOffset, numBytes);
}
}
- /**
- * JVM-only bulk copy method. Copies {@code numBytes} bytes from source unsafe object and pointer.
- * Throws on Android before executing unsafe memory access.
- */
- public void copyFromUnsafe(long offset, Object source, long sourcePointer, long numBytes) {
+ public void copyToBooleanArray(int offset, boolean[] target, int targetOffset, int numBytes) {
if (AndroidSupport.IS_ANDROID) {
- MemoryOps.throwRawUnsafeMemoryCopyUnsupported();
+ MemoryOps.copyToBooleanArray(this, offset, target, targetOffset, numBytes);
} else {
- final long thisPointer = this.address + offset;
- checkArgument(thisPointer + numBytes <= addressLimit);
- UNSAFE.copyMemory(source, sourcePointer, heapMemory, thisPointer, numBytes);
+ checkArrayCopy(offset, targetOffset, target.length, numBytes, 0);
+ copyMemory(
+ heapMemory, address + offset, target, BOOLEAN_ARRAY_OFFSET + targetOffset, numBytes);
+ }
+ }
+
+ public void copyToCharArray(int offset, char[] target, int targetOffset, int numBytes) {
+ if (AndroidSupport.IS_ANDROID) {
+ MemoryOps.copyToCharArray(this, offset, target, targetOffset, numBytes);
+ } else {
+ checkArrayCopy(offset, targetOffset, target.length, numBytes, 1);
+ copyMemory(
+ heapMemory,
+ address + offset,
+ target,
+ CHAR_ARRAY_OFFSET + arrayCopyOffset(targetOffset, 1),
+ numBytes);
+ }
+ }
+
+ public void copyToShortArray(int offset, short[] target, int targetOffset, int numBytes) {
+ if (AndroidSupport.IS_ANDROID) {
+ MemoryOps.copyToShortArray(this, offset, target, targetOffset, numBytes);
+ } else {
+ checkArrayCopy(offset, targetOffset, target.length, numBytes, 1);
+ copyMemory(
+ heapMemory,
+ address + offset,
+ target,
+ SHORT_ARRAY_OFFSET + arrayCopyOffset(targetOffset, 1),
+ numBytes);
+ }
+ }
+
+ public void copyToIntArray(int offset, int[] target, int targetOffset, int numBytes) {
+ if (AndroidSupport.IS_ANDROID) {
+ MemoryOps.copyToIntArray(this, offset, target, targetOffset, numBytes);
+ } else {
+ checkArrayCopy(offset, targetOffset, target.length, numBytes, 2);
+ copyMemory(
+ heapMemory,
+ address + offset,
+ target,
+ INT_ARRAY_OFFSET + arrayCopyOffset(targetOffset, 2),
+ numBytes);
+ }
+ }
+
+ public void copyToLongArray(int offset, long[] target, int targetOffset, int numBytes) {
+ if (AndroidSupport.IS_ANDROID) {
+ MemoryOps.copyToLongArray(this, offset, target, targetOffset, numBytes);
+ } else {
+ checkArrayCopy(offset, targetOffset, target.length, numBytes, 3);
+ copyMemory(
+ heapMemory,
+ address + offset,
+ target,
+ LONG_ARRAY_OFFSET + arrayCopyOffset(targetOffset, 3),
+ numBytes);
+ }
+ }
+
+ public void copyToFloatArray(int offset, float[] target, int targetOffset, int numBytes) {
+ if (AndroidSupport.IS_ANDROID) {
+ MemoryOps.copyToFloatArray(this, offset, target, targetOffset, numBytes);
+ } else {
+ checkArrayCopy(offset, targetOffset, target.length, numBytes, 2);
+ copyMemory(
+ heapMemory,
+ address + offset,
+ target,
+ FLOAT_ARRAY_OFFSET + arrayCopyOffset(targetOffset, 2),
+ numBytes);
+ }
+ }
+
+ public void copyToDoubleArray(int offset, double[] target, int targetOffset, int numBytes) {
+ if (AndroidSupport.IS_ANDROID) {
+ MemoryOps.copyToDoubleArray(this, offset, target, targetOffset, numBytes);
+ } else {
+ checkArrayCopy(offset, targetOffset, target.length, numBytes, 3);
+ copyMemory(
+ heapMemory,
+ address + offset,
+ target,
+ DOUBLE_ARRAY_OFFSET + arrayCopyOffset(targetOffset, 3),
+ numBytes);
+ }
+ }
+
+ public void copyFromByteArray(int offset, byte[] source, int sourceOffset, int numBytes) {
+ if (AndroidSupport.IS_ANDROID) {
+ MemoryOps.copyFromByteArray(this, offset, source, sourceOffset, numBytes);
+ } else {
+ checkArrayCopy(offset, sourceOffset, source.length, numBytes, 0);
+ copyMemory(source, BYTE_ARRAY_OFFSET + sourceOffset, heapMemory, address + offset, numBytes);
}
}
+ public void copyFromBooleanArray(int offset, boolean[] source, int sourceOffset, int numBytes) {
+ if (AndroidSupport.IS_ANDROID) {
+ MemoryOps.copyFromBooleanArray(this, offset, source, sourceOffset, numBytes);
+ } else {
+ checkArrayCopy(offset, sourceOffset, source.length, numBytes, 0);
+ copyMemory(
+ source, BOOLEAN_ARRAY_OFFSET + sourceOffset, heapMemory, address + offset, numBytes);
+ }
+ }
+
+ public void copyFromCharArray(int offset, char[] source, int sourceOffset, int numBytes) {
+ if (AndroidSupport.IS_ANDROID) {
+ MemoryOps.copyFromCharArray(this, offset, source, sourceOffset, numBytes);
+ } else {
+ checkArrayCopy(offset, sourceOffset, source.length, numBytes, 1);
+ copyMemory(
+ source,
+ CHAR_ARRAY_OFFSET + arrayCopyOffset(sourceOffset, 1),
+ heapMemory,
+ address + offset,
+ numBytes);
+ }
+ }
+
+ public void copyFromShortArray(int offset, short[] source, int sourceOffset, int numBytes) {
+ if (AndroidSupport.IS_ANDROID) {
+ MemoryOps.copyFromShortArray(this, offset, source, sourceOffset, numBytes);
+ } else {
+ checkArrayCopy(offset, sourceOffset, source.length, numBytes, 1);
+ copyMemory(
+ source,
+ SHORT_ARRAY_OFFSET + arrayCopyOffset(sourceOffset, 1),
+ heapMemory,
+ address + offset,
+ numBytes);
+ }
+ }
+
+ public void copyFromIntArray(int offset, int[] source, int sourceOffset, int numBytes) {
+ if (AndroidSupport.IS_ANDROID) {
+ MemoryOps.copyFromIntArray(this, offset, source, sourceOffset, numBytes);
+ } else {
+ checkArrayCopy(offset, sourceOffset, source.length, numBytes, 2);
+ copyMemory(
+ source,
+ INT_ARRAY_OFFSET + arrayCopyOffset(sourceOffset, 2),
+ heapMemory,
+ address + offset,
+ numBytes);
+ }
+ }
+
+ public void copyFromLongArray(int offset, long[] source, int sourceOffset, int numBytes) {
+ if (AndroidSupport.IS_ANDROID) {
+ MemoryOps.copyFromLongArray(this, offset, source, sourceOffset, numBytes);
+ } else {
+ checkArrayCopy(offset, sourceOffset, source.length, numBytes, 3);
+ copyMemory(
+ source,
+ LONG_ARRAY_OFFSET + arrayCopyOffset(sourceOffset, 3),
+ heapMemory,
+ address + offset,
+ numBytes);
+ }
+ }
+
+ public void copyFromFloatArray(int offset, float[] source, int sourceOffset, int numBytes) {
+ if (AndroidSupport.IS_ANDROID) {
+ MemoryOps.copyFromFloatArray(this, offset, source, sourceOffset, numBytes);
+ } else {
+ checkArrayCopy(offset, sourceOffset, source.length, numBytes, 2);
+ copyMemory(
+ source,
+ FLOAT_ARRAY_OFFSET + arrayCopyOffset(sourceOffset, 2),
+ heapMemory,
+ address + offset,
+ numBytes);
+ }
+ }
+
+ public void copyFromDoubleArray(int offset, double[] source, int sourceOffset, int numBytes) {
+ if (AndroidSupport.IS_ANDROID) {
+ MemoryOps.copyFromDoubleArray(this, offset, source, sourceOffset, numBytes);
+ } else {
+ checkArrayCopy(offset, sourceOffset, source.length, numBytes, 3);
+ copyMemory(
+ source,
+ DOUBLE_ARRAY_OFFSET + arrayCopyOffset(sourceOffset, 3),
+ heapMemory,
+ address + offset,
+ numBytes);
+ }
+ }
+
+ private void checkArrayCopy(
+ int offset, int targetOffset, int targetLength, int numBytes, int elementShift) {
+ int elementMask = (1 << elementShift) - 1;
+ if ((numBytes & elementMask) != 0) {
+ throw new IllegalArgumentException("numBytes is not aligned to array element size");
+ }
+ int numElements = numBytes >> elementShift;
+ if ((offset | targetOffset | numBytes | numElements) < 0
+ || offset > size - numBytes
+ || targetOffset > targetLength - numElements) {
+ throwOOBException();
+ }
+ }
+
+ private static long arrayCopyOffset(int elementOffset, int elementShift) {
+ return (long) elementOffset << elementShift;
+ }
+
public byte[] getBytes(int index, int length) {
if (index == 0 && heapMemory != null && heapOffset == 0) {
// Arrays.copyOf is an intrinsics, which is faster
@@ -3677,7 +3958,7 @@ public ByteBuffer sliceAsByteBuffer(int offset, int length) {
ByteBuffer offHeapBuffer = this.offHeapBuffer;
if (offHeapBuffer != null) {
ByteBuffer duplicate = offHeapBuffer.duplicate();
- int start = (int) (address - ByteBufferUtil.getAddress(duplicate));
+ int start = (int) (address - getAddress(duplicate));
ByteBufferUtil.position(duplicate, start + offset);
duplicate.limit(start + offset + length);
return duplicate.slice();
@@ -3716,7 +3997,7 @@ public boolean equalTo(MemoryBuffer buf2, int offset1, int offset2, int len) {
final long pos2 = buf2.address + offset2;
checkArgument(pos1 < addressLimit);
checkArgument(pos2 < buf2.addressLimit);
- return UnsafeOps.arrayEquals(heapMemory, pos1, buf2.heapMemory, pos2, len);
+ return unsafeEqualTo(heapMemory, pos1, buf2.heapMemory, pos2, len);
}
/**
@@ -3740,8 +4021,37 @@ public boolean equalTo(byte[] bytes, int bytesOffset, int offset, int len) {
return MemoryOps.equalTo(this, bytes, bytesOffset, offset, len);
}
final long pos = address + offset;
- return UnsafeOps.arrayEquals(
- heapMemory, pos, bytes, UnsafeOps.BYTE_ARRAY_OFFSET + bytesOffset, len);
+ return unsafeEqualTo(heapMemory, pos, bytes, BYTE_ARRAY_OFFSET + bytesOffset, len);
+ }
+
+ private static boolean unsafeEqualTo(
+ Object leftBase, long leftOffset, Object rightBase, long rightOffset, int length) {
+ int i = 0;
+ if ((leftOffset % 8) == (rightOffset % 8)) {
+ while ((leftOffset + i) % 8 != 0 && i < length) {
+ if (UNSAFE.getByte(leftBase, leftOffset + i)
+ != UNSAFE.getByte(rightBase, rightOffset + i)) {
+ return false;
+ }
+ i += 1;
+ }
+ }
+ if (UNALIGNED || (((leftOffset + i) % 8 == 0) && ((rightOffset + i) % 8 == 0))) {
+ while (i <= length - 8) {
+ if (UNSAFE.getLong(leftBase, leftOffset + i)
+ != UNSAFE.getLong(rightBase, rightOffset + i)) {
+ return false;
+ }
+ i += 8;
+ }
+ }
+ while (i < length) {
+ if (UNSAFE.getByte(leftBase, leftOffset + i) != UNSAFE.getByte(rightBase, rightOffset + i)) {
+ return false;
+ }
+ i += 1;
+ }
+ return true;
}
@Override
@@ -3848,8 +4158,7 @@ public static MemoryBuffer fromByteBuffer(ByteBuffer buffer) {
if (AndroidSupport.IS_ANDROID) {
return MemoryOps.fromByteBuffer(buffer);
} else if (buffer.isDirect()) {
- return new MemoryBuffer(
- ByteBufferUtil.getAddress(buffer) + buffer.position(), buffer.remaining(), buffer);
+ return new MemoryBuffer(getAddress(buffer) + buffer.position(), buffer.remaining(), buffer);
} else if (buffer.hasArray()) {
int offset = buffer.arrayOffset() + buffer.position();
return new MemoryBuffer(buffer.array(), offset, buffer.remaining());
@@ -3866,7 +4175,7 @@ public static MemoryBuffer fromDirectByteBuffer(
if (AndroidSupport.IS_ANDROID) {
return MemoryOps.directByteBufferUnsupported();
}
- long offHeapAddress = ByteBufferUtil.getAddress(buffer) + buffer.position();
+ long offHeapAddress = getAddress(buffer) + buffer.position();
return new MemoryBuffer(offHeapAddress, size, buffer, streamReader);
}
diff --git a/java/fory-core/src/main/java/org/apache/fory/memory/MemoryOps.java b/java/fory-core/src/main/java/org/apache/fory/memory/MemoryOps.java
index 1b235c5b2f..02722024ac 100644
--- a/java/fory-core/src/main/java/org/apache/fory/memory/MemoryOps.java
+++ b/java/fory-core/src/main/java/org/apache/fory/memory/MemoryOps.java
@@ -70,10 +70,6 @@ static MemoryBuffer directByteBufferUnsupported() {
throw new UnsupportedOperationException("Direct ByteBuffer memory is not supported on Android");
}
- static void throwRawUnsafeMemoryCopyUnsupported() {
- throw new UnsupportedOperationException("Raw unsafe memory copy is not supported on Android");
- }
-
static MemoryBuffer fromByteBuffer(ByteBuffer buffer) {
ByteBuffer duplicate = buffer.duplicate();
byte[] bytes = new byte[duplicate.remaining()];
@@ -1259,6 +1255,190 @@ static void copyTo(
offset, targetOffset, numBytes, source.size, target.size));
}
+ static void copyToByteArray(
+ MemoryBuffer source, int offset, byte[] target, int targetOffset, int numBytes) {
+ checkArrayCopy(source, offset, targetOffset, target.length, numBytes, 0);
+ copy(source.heapMemory, heapIndex(source, offset), target, targetOffset, numBytes);
+ }
+
+ static void copyToBooleanArray(
+ MemoryBuffer source, int offset, boolean[] target, int targetOffset, int numBytes) {
+ checkArrayCopy(source, offset, targetOffset, target.length, numBytes, 0);
+ byte[] bytes = source.heapMemory;
+ int sourceIndex = heapIndex(source, offset);
+ for (int i = 0; i < numBytes; i++) {
+ target[targetOffset + i] = bytes[sourceIndex + i] != 0;
+ }
+ }
+
+ static void copyToCharArray(
+ MemoryBuffer source, int offset, char[] target, int targetOffset, int numBytes) {
+ checkArrayCopy(source, offset, targetOffset, target.length, numBytes, 1);
+ byte[] bytes = source.heapMemory;
+ int sourceIndex = heapIndex(source, offset);
+ int numElements = numBytes >>> 1;
+ for (int i = 0; i < numElements; i++, sourceIndex += 2) {
+ target[targetOffset + i] = (char) getInt16(bytes, sourceIndex);
+ }
+ }
+
+ static void copyToShortArray(
+ MemoryBuffer source, int offset, short[] target, int targetOffset, int numBytes) {
+ checkArrayCopy(source, offset, targetOffset, target.length, numBytes, 1);
+ byte[] bytes = source.heapMemory;
+ int sourceIndex = heapIndex(source, offset);
+ int numElements = numBytes >>> 1;
+ for (int i = 0; i < numElements; i++, sourceIndex += 2) {
+ target[targetOffset + i] = getInt16(bytes, sourceIndex);
+ }
+ }
+
+ static void copyToIntArray(
+ MemoryBuffer source, int offset, int[] target, int targetOffset, int numBytes) {
+ checkArrayCopy(source, offset, targetOffset, target.length, numBytes, 2);
+ byte[] bytes = source.heapMemory;
+ int sourceIndex = heapIndex(source, offset);
+ int numElements = numBytes >>> 2;
+ for (int i = 0; i < numElements; i++, sourceIndex += 4) {
+ target[targetOffset + i] = getInt32(bytes, sourceIndex);
+ }
+ }
+
+ static void copyToLongArray(
+ MemoryBuffer source, int offset, long[] target, int targetOffset, int numBytes) {
+ checkArrayCopy(source, offset, targetOffset, target.length, numBytes, 3);
+ byte[] bytes = source.heapMemory;
+ int sourceIndex = heapIndex(source, offset);
+ int numElements = numBytes >>> 3;
+ for (int i = 0; i < numElements; i++, sourceIndex += 8) {
+ target[targetOffset + i] = getInt64(bytes, sourceIndex);
+ }
+ }
+
+ static void copyToFloatArray(
+ MemoryBuffer source, int offset, float[] target, int targetOffset, int numBytes) {
+ checkArrayCopy(source, offset, targetOffset, target.length, numBytes, 2);
+ byte[] bytes = source.heapMemory;
+ int sourceIndex = heapIndex(source, offset);
+ int numElements = numBytes >>> 2;
+ for (int i = 0; i < numElements; i++, sourceIndex += 4) {
+ target[targetOffset + i] = getFloat32(bytes, sourceIndex);
+ }
+ }
+
+ static void copyToDoubleArray(
+ MemoryBuffer source, int offset, double[] target, int targetOffset, int numBytes) {
+ checkArrayCopy(source, offset, targetOffset, target.length, numBytes, 3);
+ byte[] bytes = source.heapMemory;
+ int sourceIndex = heapIndex(source, offset);
+ int numElements = numBytes >>> 3;
+ for (int i = 0; i < numElements; i++, sourceIndex += 8) {
+ target[targetOffset + i] = getFloat64(bytes, sourceIndex);
+ }
+ }
+
+ static void copyFromByteArray(
+ MemoryBuffer target, int offset, byte[] source, int sourceOffset, int numBytes) {
+ checkArrayCopy(target, offset, sourceOffset, source.length, numBytes, 0);
+ copy(source, sourceOffset, target.heapMemory, heapIndex(target, offset), numBytes);
+ }
+
+ static void copyFromBooleanArray(
+ MemoryBuffer target, int offset, boolean[] source, int sourceOffset, int numBytes) {
+ checkArrayCopy(target, offset, sourceOffset, source.length, numBytes, 0);
+ byte[] bytes = target.heapMemory;
+ int targetIndex = heapIndex(target, offset);
+ for (int i = 0; i < numBytes; i++) {
+ bytes[targetIndex + i] = source[sourceOffset + i] ? (byte) 1 : (byte) 0;
+ }
+ }
+
+ static void copyFromCharArray(
+ MemoryBuffer target, int offset, char[] source, int sourceOffset, int numBytes) {
+ checkArrayCopy(target, offset, sourceOffset, source.length, numBytes, 1);
+ byte[] bytes = target.heapMemory;
+ int targetIndex = heapIndex(target, offset);
+ int numElements = numBytes >>> 1;
+ for (int i = 0; i < numElements; i++, targetIndex += 2) {
+ putInt16(bytes, targetIndex, (short) source[sourceOffset + i]);
+ }
+ }
+
+ static void copyFromShortArray(
+ MemoryBuffer target, int offset, short[] source, int sourceOffset, int numBytes) {
+ checkArrayCopy(target, offset, sourceOffset, source.length, numBytes, 1);
+ byte[] bytes = target.heapMemory;
+ int targetIndex = heapIndex(target, offset);
+ int numElements = numBytes >>> 1;
+ for (int i = 0; i < numElements; i++, targetIndex += 2) {
+ putInt16(bytes, targetIndex, source[sourceOffset + i]);
+ }
+ }
+
+ static void copyFromIntArray(
+ MemoryBuffer target, int offset, int[] source, int sourceOffset, int numBytes) {
+ checkArrayCopy(target, offset, sourceOffset, source.length, numBytes, 2);
+ byte[] bytes = target.heapMemory;
+ int targetIndex = heapIndex(target, offset);
+ int numElements = numBytes >>> 2;
+ for (int i = 0; i < numElements; i++, targetIndex += 4) {
+ putInt32(bytes, targetIndex, source[sourceOffset + i]);
+ }
+ }
+
+ static void copyFromLongArray(
+ MemoryBuffer target, int offset, long[] source, int sourceOffset, int numBytes) {
+ checkArrayCopy(target, offset, sourceOffset, source.length, numBytes, 3);
+ byte[] bytes = target.heapMemory;
+ int targetIndex = heapIndex(target, offset);
+ int numElements = numBytes >>> 3;
+ for (int i = 0; i < numElements; i++, targetIndex += 8) {
+ putInt64(bytes, targetIndex, source[sourceOffset + i]);
+ }
+ }
+
+ static void copyFromFloatArray(
+ MemoryBuffer target, int offset, float[] source, int sourceOffset, int numBytes) {
+ checkArrayCopy(target, offset, sourceOffset, source.length, numBytes, 2);
+ byte[] bytes = target.heapMemory;
+ int targetIndex = heapIndex(target, offset);
+ int numElements = numBytes >>> 2;
+ for (int i = 0; i < numElements; i++, targetIndex += 4) {
+ putFloat32(bytes, targetIndex, source[sourceOffset + i]);
+ }
+ }
+
+ static void copyFromDoubleArray(
+ MemoryBuffer target, int offset, double[] source, int sourceOffset, int numBytes) {
+ checkArrayCopy(target, offset, sourceOffset, source.length, numBytes, 3);
+ byte[] bytes = target.heapMemory;
+ int targetIndex = heapIndex(target, offset);
+ int numElements = numBytes >>> 3;
+ for (int i = 0; i < numElements; i++, targetIndex += 8) {
+ putFloat64(bytes, targetIndex, source[sourceOffset + i]);
+ }
+ }
+
+ private static void checkArrayCopy(
+ MemoryBuffer source,
+ int offset,
+ int targetOffset,
+ int targetLength,
+ int numBytes,
+ int shift) {
+ checkHeap(source);
+ int mask = (1 << shift) - 1;
+ if ((numBytes & mask) != 0) {
+ throw new IllegalArgumentException("numBytes is not aligned to array element size");
+ }
+ int numElements = numBytes >>> shift;
+ if ((offset | targetOffset | numBytes | numElements) < 0
+ || offset > source.size - numBytes
+ || targetOffset > targetLength - numElements) {
+ throwOOBException(source);
+ }
+ }
+
static boolean equalTo(
MemoryBuffer buffer, MemoryBuffer other, int offset1, int offset2, int len) {
checkArgument(offset1 >= 0 && offset1 <= buffer.size - len);
diff --git a/java/fory-core/src/main/java/org/apache/fory/memory/MemoryUtils.java b/java/fory-core/src/main/java/org/apache/fory/memory/MemoryUtils.java
index bb23ca4994..7f7b06c27f 100644
--- a/java/fory-core/src/main/java/org/apache/fory/memory/MemoryUtils.java
+++ b/java/fory-core/src/main/java/org/apache/fory/memory/MemoryUtils.java
@@ -19,15 +19,24 @@
package org.apache.fory.memory;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import org.apache.fory.platform.AndroidSupport;
-import org.apache.fory.platform.UnsafeOps;
-import org.apache.fory.util.Preconditions;
+import org.apache.fory.platform.internal._JDKAccess;
/** Memory utils for fory. */
public class MemoryUtils {
+ // Android does not expose these private JDK fields. GraalVM native image support is decided by
+ // the accessor owner instead of being disabled globally here.
+ public static final boolean JDK_INTERNAL_FIELD_ACCESS =
+ !AndroidSupport.IS_ANDROID && _JDKAccess.JDK_INTERNAL_FIELD_ACCESS;
+ public static final boolean JDK_LANG_FIELD_ACCESS =
+ !AndroidSupport.IS_ANDROID && _JDKAccess.JDK_LANG_FIELD_ACCESS;
+ public static final boolean JDK_COLLECTION_FIELD_ACCESS =
+ !AndroidSupport.IS_ANDROID && _JDKAccess.JDK_COLLECTION_FIELD_ACCESS;
+ public static final boolean JDK_CONCURRENT_FIELD_ACCESS =
+ !AndroidSupport.IS_ANDROID && _JDKAccess.JDK_CONCURRENT_FIELD_ACCESS;
+ public static final boolean JDK_PROXY_FIELD_ACCESS =
+ !AndroidSupport.IS_ANDROID && _JDKAccess.JDK_PROXY_FIELD_ACCESS;
public static MemoryBuffer buffer(int size) {
return wrap(new byte[size]);
@@ -71,81 +80,6 @@ public static MemoryBuffer wrap(ByteBuffer buffer) {
}
}
- // Lazy load offset and also follow graalvm offset auto replace pattern.
- private static class Offset {
- private static final long BAS_BUF_BUF;
- private static final long BAS_BUF_COUNT;
- private static final long BIS_BUF_BUF;
- private static final long BIS_BUF_POS;
- private static final long BIS_BUF_COUNT;
-
- static {
- try {
- BAS_BUF_BUF =
- UnsafeOps.objectFieldOffset(ByteArrayOutputStream.class.getDeclaredField("buf"));
- BAS_BUF_COUNT =
- UnsafeOps.objectFieldOffset(ByteArrayOutputStream.class.getDeclaredField("count"));
- BIS_BUF_BUF =
- UnsafeOps.objectFieldOffset(ByteArrayInputStream.class.getDeclaredField("buf"));
- BIS_BUF_POS =
- UnsafeOps.objectFieldOffset(ByteArrayInputStream.class.getDeclaredField("pos"));
- BIS_BUF_COUNT =
- UnsafeOps.objectFieldOffset(ByteArrayInputStream.class.getDeclaredField("count"));
- } catch (NoSuchFieldException e) {
- throw new RuntimeException(e);
- }
- }
- }
-
- /**
- * Wrap a {@link ByteArrayOutputStream} into a {@link MemoryBuffer}. The writerIndex of buffer
- * will be the count of stream.
- */
- public static void wrap(ByteArrayOutputStream stream, MemoryBuffer buffer) {
- if (AndroidSupport.IS_ANDROID) {
- throw new UnsupportedOperationException(
- "ByteArrayOutputStream direct wrapping is not supported on Android");
- }
- Preconditions.checkNotNull(stream);
- byte[] buf = (byte[]) UnsafeOps.getObject(stream, Offset.BAS_BUF_BUF);
- int count = UnsafeOps.getInt(stream, Offset.BAS_BUF_COUNT);
- buffer.pointTo(buf, 0, buf.length);
- buffer.writerIndex(count);
- }
-
- /**
- * Wrap a @link MemoryBuffer} into a {@link ByteArrayOutputStream}. The count of stream will be
- * the writerIndex of buffer.
- */
- public static void wrap(MemoryBuffer buffer, ByteArrayOutputStream stream) {
- if (AndroidSupport.IS_ANDROID) {
- throw new UnsupportedOperationException(
- "ByteArrayOutputStream direct wrapping is not supported on Android");
- }
- Preconditions.checkNotNull(stream);
- byte[] bytes = buffer.getHeapMemory();
- Preconditions.checkNotNull(bytes);
- UnsafeOps.putObject(stream, Offset.BAS_BUF_BUF, bytes);
- UnsafeOps.putInt(stream, Offset.BAS_BUF_COUNT, buffer.writerIndex());
- }
-
- /**
- * Wrap a {@link ByteArrayInputStream} into a {@link MemoryBuffer}. The readerIndex of buffer will
- * be the pos of stream.
- */
- public static void wrap(ByteArrayInputStream stream, MemoryBuffer buffer) {
- if (AndroidSupport.IS_ANDROID) {
- throw new UnsupportedOperationException(
- "ByteArrayInputStream direct wrapping is not supported on Android");
- }
- Preconditions.checkNotNull(stream);
- byte[] buf = (byte[]) UnsafeOps.getObject(stream, Offset.BIS_BUF_BUF);
- int count = UnsafeOps.getInt(stream, Offset.BIS_BUF_COUNT);
- int pos = UnsafeOps.getInt(stream, Offset.BIS_BUF_POS);
- buffer.pointTo(buf, 0, count);
- buffer.readerIndex(pos);
- }
-
private static MemoryBuffer copyToHeapBuffer(ByteBuffer buffer) {
ByteBuffer duplicate = buffer.duplicate();
byte[] bytes = new byte[duplicate.remaining()];
diff --git a/java/fory-core/src/main/java/org/apache/fory/meta/MetaStringEncoder.java b/java/fory-core/src/main/java/org/apache/fory/meta/MetaStringEncoder.java
index e35460d8c5..ce4c976d71 100644
--- a/java/fory-core/src/main/java/org/apache/fory/meta/MetaStringEncoder.java
+++ b/java/fory-core/src/main/java/org/apache/fory/meta/MetaStringEncoder.java
@@ -21,8 +21,8 @@
import java.nio.charset.StandardCharsets;
import org.apache.fory.meta.MetaString.Encoding;
+import org.apache.fory.serializer.StringEncodingUtils;
import org.apache.fory.util.Preconditions;
-import org.apache.fory.util.StringUtils;
/** Encodes plain text strings into MetaString objects with specified encoding mechanisms. */
public class MetaStringEncoder {
@@ -78,7 +78,7 @@ public EncodedMetaString encodeBinary(String input, Encoding[] encodings) {
if (input.isEmpty()) {
return EncodedMetaString.EMPTY;
}
- if (!StringUtils.isLatin(input.toCharArray())) {
+ if (!StringEncodingUtils.isLatin(input.toCharArray())) {
return new EncodedMetaString(input.getBytes(StandardCharsets.UTF_8), Encoding.UTF_8);
}
Encoding encoding = computeEncoding(input, encodings);
@@ -88,7 +88,7 @@ public EncodedMetaString encodeBinary(String input, Encoding[] encodings) {
public EncodedMetaString encodeBinary(String input, Encoding encoding) {
Preconditions.checkArgument(
input.length() < Short.MAX_VALUE, "Long meta string than 32767 is not allowed");
- if (encoding != Encoding.UTF_8 && !StringUtils.isLatin(input.toCharArray())) {
+ if (encoding != Encoding.UTF_8 && !StringEncodingUtils.isLatin(input.toCharArray())) {
throw new IllegalArgumentException("Non-ASCII characters in meta string are not allowed");
}
if (input.isEmpty()) {
diff --git a/java/fory-core/src/main/java/org/apache/fory/meta/TypeDef.java b/java/fory-core/src/main/java/org/apache/fory/meta/TypeDef.java
index 4a72cb78d0..26865cb32e 100644
--- a/java/fory-core/src/main/java/org/apache/fory/meta/TypeDef.java
+++ b/java/fory-core/src/main/java/org/apache/fory/meta/TypeDef.java
@@ -19,14 +19,11 @@
package org.apache.fory.meta;
-import static org.apache.fory.meta.NativeTypeDefEncoder.buildDescriptors;
-
import java.io.ObjectStreamClass;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -38,8 +35,6 @@
import org.apache.fory.logging.Logger;
import org.apache.fory.logging.LoggerFactory;
import org.apache.fory.memory.MemoryBuffer;
-import org.apache.fory.platform.UnsafeOps;
-import org.apache.fory.reflect.ReflectionUtils;
import org.apache.fory.resolver.ClassResolver;
import org.apache.fory.resolver.SharedRegistry;
import org.apache.fory.resolver.TypeResolver;
@@ -67,7 +62,6 @@
* @see ForyBuilder#withCompatible(boolean)
* @see CompatibleSerializer
* @see ForyBuilder#withMetaShare
- * @see ReflectionUtils#getFieldOffset
*/
public class TypeDef implements Serializable {
private static final Logger LOG = LoggerFactory.getLogger(TypeDef.class);
@@ -78,30 +72,6 @@ public class TypeDef implements Serializable {
static final int META_SIZE_MASKS = 0xff;
static final int NUM_HASH_BITS = 52;
- // TODO use field offset to sort field, which will hit l1-cache more. Since
- // `objectFieldOffset` is not part of jvm-specification, it may change between different jdk
- // vendor. But the deserialization peer use the class definition to create deserializer, it's OK
- // even field offset or fields order change between jvm process.
- public static final Comparator FIELD_COMPARATOR =
- (f1, f2) -> {
- long offset1 = UnsafeOps.objectFieldOffset(f1);
- long offset2 = UnsafeOps.objectFieldOffset(f2);
- long diff = offset1 - offset2;
- if (diff != 0) {
- return (int) diff;
- } else {
- if (!f1.equals(f2)) {
- LOG.warn(
- "Field {} has same offset with {}, please an issue with jdk info to fory", f1, f2);
- }
- int compare = f1.getDeclaringClass().getName().compareTo(f2.getName());
- if (compare != 0) {
- return compare;
- }
- return f1.getName().compareTo(f2.getName());
- }
- };
-
private final ClassSpec classSpec;
private final List fieldsInfo;
// Unique id for class def. If class def are same between processes, then the id will
diff --git a/java/fory-core/src/main/java/org/apache/fory/platform/GraalvmSupport.java b/java/fory-core/src/main/java/org/apache/fory/platform/GraalvmSupport.java
index 573b379f83..de036f9759 100644
--- a/java/fory-core/src/main/java/org/apache/fory/platform/GraalvmSupport.java
+++ b/java/fory-core/src/main/java/org/apache/fory/platform/GraalvmSupport.java
@@ -105,6 +105,18 @@ public class GraalvmSupport {
registerDefaultSerializerClass(TimeSerializers.TimeZoneSerializer.class);
registerDefaultSerializerClass(BufferSerializers.ByteBufferSerializer.class);
registerDefaultSerializerClass(MapSerializers.StringKeyMapSerializer.class);
+ registerDefaultSerializerClassIfPresent(
+ "org.apache.fory.serializer.collection.GuavaCollectionSerializers"
+ + "$ImmutableIntArraySerializer");
+ registerDefaultSerializerClassIfPresent(
+ "org.apache.fory.serializer.collection.GuavaCollectionSerializers"
+ + "$ImmutableMapFormSerializer");
+ registerDefaultSerializerClassIfPresent(
+ "org.apache.fory.serializer.collection.GuavaCollectionSerializers"
+ + "$ImmutableBiMapFormSerializer");
+ registerDefaultSerializerClassIfPresent(
+ "org.apache.fory.serializer.collection.GuavaCollectionSerializers"
+ + "$HashBasedTableSerializer");
registerDefaultSerializerClass(ChildContainerSerializers.ChildArrayListSerializer.class);
registerDefaultSerializerClass(ChildContainerSerializers.ChildCollectionSerializer.class);
registerDefaultSerializerClass(ChildContainerSerializers.ChildMapSerializer.class);
@@ -118,6 +130,7 @@ public class GraalvmSupport {
registerDefaultSerializerClass(CollectionSerializers.DefaultJavaCollectionSerializer.class);
registerDefaultSerializerClass(CollectionSerializer.class);
registerDefaultSerializerClass(MapSerializers.JDKCompatibleMapSerializer.class);
+ registerDefaultSerializerClass(MapSerializers.IdentityHashMapSerializer.class);
registerDefaultSerializerClass(MapSerializers.DefaultJavaMapSerializer.class);
registerDefaultSerializerClass(MapSerializer.class);
registerDefaultSerializerClass(CodegenSerializer.LazyInitBeanSerializer.class);
@@ -296,6 +309,21 @@ private static void registerDefaultSerializerClass(Class extends Serializer> s
DEFAULT_SERIALIZER_CLASSES.add(serializerClass);
}
+ private static void registerDefaultSerializerClassIfPresent(String serializerClassName) {
+ try {
+ Class> serializerClass =
+ Class.forName(serializerClassName, false, GraalvmSupport.class.getClassLoader());
+ // Loading without initialization does not resolve member signatures. Optional serializers
+ // must prove their dependency types are present before entering the GraalVM metadata set.
+ serializerClass.getDeclaredConstructors();
+ serializerClass.getDeclaredMethods();
+ serializerClass.getDeclaredFields();
+ registerDefaultSerializerClass(serializerClass.asSubclass(Serializer.class));
+ } catch (ClassNotFoundException | LinkageError e) {
+ // Guava is optional at runtime; only available Guava serializers should be registered.
+ }
+ }
+
public static ForyException throwNoArgCtrException(Class> type) {
throw new ForyException("Please provide a no-arg constructor for " + type);
}
diff --git a/java/fory-core/src/main/java/org/apache/fory/platform/UnsafeOps.java b/java/fory-core/src/main/java/org/apache/fory/platform/UnsafeOps.java
deleted file mode 100644
index c8ddf1ae5a..0000000000
--- a/java/fory-core/src/main/java/org/apache/fory/platform/UnsafeOps.java
+++ /dev/null
@@ -1,287 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.fory.platform;
-
-import java.lang.reflect.Field;
-import java.lang.reflect.Method;
-import org.apache.fory.annotation.Internal;
-import org.apache.fory.util.ExceptionUtils;
-import org.apache.fory.util.unsafe._JDKAccess;
-import sun.misc.Unsafe;
-
-// Derived from Apache Spark's unsafe memory utility.
-
-/** A utility class for unsafe memory operations. */
-@Internal
-@SuppressWarnings("restriction")
-public final class UnsafeOps {
- @SuppressWarnings("restriction")
- public static final Unsafe UNSAFE = _JDKAccess.UNSAFE;
-
- public static final int BOOLEAN_ARRAY_OFFSET;
- public static final int BYTE_ARRAY_OFFSET;
- public static final int CHAR_ARRAY_OFFSET;
- public static final int SHORT_ARRAY_OFFSET;
- public static final int INT_ARRAY_OFFSET;
- public static final int LONG_ARRAY_OFFSET;
- public static final int FLOAT_ARRAY_OFFSET;
- public static final int DOUBLE_ARRAY_OFFSET;
- private static final boolean unaligned;
-
- /**
- * Limits the number of bytes to copy per {@link Unsafe#copyMemory(long, long, long)} to allow
- * safepoint polling during a large copy.
- */
- private static final long UNSAFE_COPY_THRESHOLD = 1024L * 1024L;
-
- private UnsafeOps() {}
-
- static {
- BOOLEAN_ARRAY_OFFSET = UNSAFE.arrayBaseOffset(boolean[].class);
- BYTE_ARRAY_OFFSET = UNSAFE.arrayBaseOffset(byte[].class);
- CHAR_ARRAY_OFFSET = UNSAFE.arrayBaseOffset(char[].class);
- SHORT_ARRAY_OFFSET = UNSAFE.arrayBaseOffset(short[].class);
- INT_ARRAY_OFFSET = UNSAFE.arrayBaseOffset(int[].class);
- LONG_ARRAY_OFFSET = UNSAFE.arrayBaseOffset(long[].class);
- FLOAT_ARRAY_OFFSET = UNSAFE.arrayBaseOffset(float[].class);
- DOUBLE_ARRAY_OFFSET = UNSAFE.arrayBaseOffset(double[].class);
- }
-
- // This requires `JdkVersion.MAJOR_VERSION` and `_UNSAFE`.
- static {
- boolean unalign;
- String arch = System.getProperty("os.arch", "");
- if ("ppc64le".equals(arch) || "ppc64".equals(arch) || "s390x".equals(arch)) {
- // Since java.nio.Bits.unaligned() doesn't return true on ppc (See JDK-8165231), but
- // ppc64 and ppc64le support it
- unalign = true;
- } else {
- try {
- Class> bitsClass =
- Class.forName("java.nio.Bits", false, ClassLoader.getSystemClassLoader());
- if (JdkVersion.MAJOR_VERSION >= 9) {
- // Java 9/10 and 11/12 have different field names.
- Field unalignedField =
- bitsClass.getDeclaredField(
- JdkVersion.MAJOR_VERSION >= 11 ? "UNALIGNED" : "unaligned");
- unalign =
- UNSAFE.getBoolean(
- UNSAFE.staticFieldBase(unalignedField), UNSAFE.staticFieldOffset(unalignedField));
- } else {
- Method unalignedMethod = bitsClass.getDeclaredMethod("unaligned");
- unalignedMethod.setAccessible(true);
- unalign = Boolean.TRUE.equals(unalignedMethod.invoke(null));
- }
- } catch (Throwable t) {
- // We at least know x86 and x64 support unaligned access.
- //noinspection DynamicRegexReplaceableByCompiledPattern
- unalign = arch.matches("^(i[3-6]86|x86(_64)?|x64|amd64|aarch64)$");
- }
- }
- unaligned = unalign;
- }
-
- /**
- * Returns true when running JVM is having sun's Unsafe package available in it and underlying
- * system having unaligned-access capability.
- */
- public static boolean unaligned() {
- return unaligned;
- }
-
- public static long objectFieldOffset(Field f) {
- return UNSAFE.objectFieldOffset(f);
- }
-
- public static int getInt(Object object, long offset) {
- return UNSAFE.getInt(object, offset);
- }
-
- public static void putInt(Object object, long offset, int value) {
- UNSAFE.putInt(object, offset, value);
- }
-
- public static boolean getBoolean(Object object, long offset) {
- return UNSAFE.getBoolean(object, offset);
- }
-
- public static void putBoolean(Object object, long offset, boolean value) {
- UNSAFE.putBoolean(object, offset, value);
- }
-
- public static byte getByte(Object object, long offset) {
- return UNSAFE.getByte(object, offset);
- }
-
- public static void putByte(Object object, long offset, byte value) {
- UNSAFE.putByte(object, offset, value);
- }
-
- public static short getShort(Object object, long offset) {
- return UNSAFE.getShort(object, offset);
- }
-
- public static void putShort(Object object, long offset, short value) {
- UNSAFE.putShort(object, offset, value);
- }
-
- public static char getChar(Object obj, long offset) {
- return UnsafeOps.UNSAFE.getChar(obj, offset);
- }
-
- public static void putChar(Object obj, long offset, char value) {
- UnsafeOps.UNSAFE.putChar(obj, offset, value);
- }
-
- public static long getLong(Object object, long offset) {
- return UNSAFE.getLong(object, offset);
- }
-
- public static void putLong(Object object, long offset, long value) {
- UNSAFE.putLong(object, offset, value);
- }
-
- public static float getFloat(Object object, long offset) {
- return UNSAFE.getFloat(object, offset);
- }
-
- public static void putFloat(Object object, long offset, float value) {
- UNSAFE.putFloat(object, offset, value);
- }
-
- public static double getDouble(Object object, long offset) {
- return UNSAFE.getDouble(object, offset);
- }
-
- public static void putDouble(Object object, long offset, double value) {
- UNSAFE.putDouble(object, offset, value);
- }
-
- public static Object getObject(Object o, long offset) {
- return UNSAFE.getObject(o, offset);
- }
-
- public static void putObject(Object object, long offset, Object value) {
- UNSAFE.putObject(object, offset, value);
- }
-
- public static Object getObjectVolatile(Object object, long offset) {
- return UNSAFE.getObjectVolatile(object, offset);
- }
-
- public static void putObjectVolatile(Object object, long offset, Object value) {
- UNSAFE.putObjectVolatile(object, offset, value);
- }
-
- public static long allocateMemory(long size) {
- return UNSAFE.allocateMemory(size);
- }
-
- public static void freeMemory(long address) {
- UNSAFE.freeMemory(address);
- }
-
- public static long reallocateMemory(long address, long oldSize, long newSize) {
- long newMemory = UNSAFE.allocateMemory(newSize);
- copyMemory(null, address, null, newMemory, oldSize);
- freeMemory(address);
- return newMemory;
- }
-
- public static void setMemory(Object object, long offset, long size, byte value) {
- UNSAFE.setMemory(object, offset, size, value);
- }
-
- public static void setMemory(long address, byte value, long size) {
- UNSAFE.setMemory(address, size, value);
- }
-
- public static void copyMemory(
- Object src, long srcOffset, Object dst, long dstOffset, long length) {
- if (length < UNSAFE_COPY_THRESHOLD) {
- UNSAFE.copyMemory(src, srcOffset, dst, dstOffset, length);
- } else {
- while (length > 0) {
- long size = Math.min(length, UNSAFE_COPY_THRESHOLD);
- UNSAFE.copyMemory(src, srcOffset, dst, dstOffset, size);
- length -= size;
- srcOffset += size;
- dstOffset += size;
- }
- }
- }
-
- public static Object[] copyObjectArray(Object[] arr) {
- Object[] objects = new Object[arr.length];
- System.arraycopy(arr, 0, objects, 0, arr.length);
- return objects;
- }
-
- /**
- * Optimized byte array equality check for byte arrays.
- *
- * @return true if the arrays are equal, false otherwise
- */
- public static boolean arrayEquals(
- Object leftBase, long leftOffset, Object rightBase, long rightOffset, final long length) {
- int i = 0;
-
- // check if stars align and we can get both offsets to be aligned
- if ((leftOffset % 8) == (rightOffset % 8)) {
- while ((leftOffset + i) % 8 != 0 && i < length) {
- if (UnsafeOps.getByte(leftBase, leftOffset + i)
- != UnsafeOps.getByte(rightBase, rightOffset + i)) {
- return false;
- }
- i += 1;
- }
- }
- // for architectures that support unaligned accesses, chew it up 8 bytes at a time
- if (unaligned || (((leftOffset + i) % 8 == 0) && ((rightOffset + i) % 8 == 0))) {
- while (i <= length - 8) {
- if (UnsafeOps.getLong(leftBase, leftOffset + i)
- != UnsafeOps.getLong(rightBase, rightOffset + i)) {
- return false;
- }
- i += 8;
- }
- }
- // this will finish off the unaligned comparisons, or do the entire aligned
- // comparison whichever is needed.
- while (i < length) {
- if (UnsafeOps.getByte(leftBase, leftOffset + i)
- != UnsafeOps.getByte(rightBase, rightOffset + i)) {
- return false;
- }
- i += 1;
- }
- return true;
- }
-
- /** Create an instance of type. This method don't call constructor. */
- public static T newInstance(Class type) {
- try {
- return type.cast(UNSAFE.allocateInstance(type));
- } catch (InstantiationException e) {
- ExceptionUtils.throwException(e);
- }
- throw new IllegalStateException("unreachable");
- }
-}
diff --git a/java/fory-core/src/main/java/org/apache/fory/platform/internal/DefineClass.java b/java/fory-core/src/main/java/org/apache/fory/platform/internal/DefineClass.java
new file mode 100644
index 0000000000..4ea20ae703
--- /dev/null
+++ b/java/fory-core/src/main/java/org/apache/fory/platform/internal/DefineClass.java
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fory.platform.internal;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodHandles.Lookup;
+import java.lang.invoke.MethodType;
+import java.lang.reflect.Array;
+import java.security.ProtectionDomain;
+import org.apache.fory.annotation.Internal;
+import org.apache.fory.platform.AndroidSupport;
+import org.apache.fory.platform.JdkVersion;
+import org.apache.fory.util.Preconditions;
+
+/** A class to define bytecode as a class. */
+@Internal
+public class DefineClass {
+ private static volatile MethodHandle classloaderDefineClassHandle;
+ private static volatile HiddenClassDefiner hiddenClassDefiner;
+
+ public static Class> defineClass(
+ String className,
+ Class> neighbor,
+ ClassLoader loader,
+ ProtectionDomain domain,
+ byte[] bytecodes) {
+ if (AndroidSupport.IS_ANDROID) {
+ throw new UnsupportedOperationException(
+ "Runtime bytecode loading is unsupported on Android.");
+ }
+ Preconditions.checkNotNull(loader);
+ Preconditions.checkArgument(JdkVersion.MAJOR_VERSION >= 8);
+ if (neighbor != null && JdkVersion.MAJOR_VERSION >= 9) {
+ // classes in bytecode must be in same package as lookup class.
+ MethodHandles.Lookup lookup = MethodHandles.lookup();
+ _JDKAccess.addReads(_JDKAccess.getModule(DefineClass.class), _JDKAccess.getModule(neighbor));
+ lookup = _Lookup.privateLookupIn(neighbor, lookup);
+ return _Lookup.defineClass(lookup, bytecodes);
+ }
+ if (classloaderDefineClassHandle == null) {
+ MethodHandles.Lookup lookup = _JDKAccess._trustedLookup(ClassLoader.class);
+ try {
+ classloaderDefineClassHandle =
+ lookup.findVirtual(
+ ClassLoader.class,
+ "defineClass",
+ MethodType.methodType(
+ Class.class,
+ String.class,
+ byte[].class,
+ int.class,
+ int.class,
+ ProtectionDomain.class));
+ } catch (NoSuchMethodException | IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ try {
+ return (Class>)
+ classloaderDefineClassHandle.invokeWithArguments(
+ loader, className, bytecodes, 0, bytecodes.length, domain);
+ } catch (Throwable e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static Class> defineHiddenNestmate(Class> neighbor, byte[] bytecodes) {
+ Preconditions.checkNotNull(neighbor);
+ Preconditions.checkNotNull(bytecodes);
+ if (JdkVersion.MAJOR_VERSION < 15) {
+ throw hiddenClassUnsupported(null);
+ }
+ try {
+ Lookup lookup = _Lookup.privateLookupIn(neighbor, MethodHandles.lookup());
+ return hiddenClassDefiner().define(lookup, bytecodes);
+ } catch (Throwable e) {
+ throw hiddenClassFailure(neighbor, e);
+ }
+ }
+
+ private static HiddenClassDefiner hiddenClassDefiner() {
+ HiddenClassDefiner definer = hiddenClassDefiner;
+ if (definer == null) {
+ synchronized (DefineClass.class) {
+ definer = hiddenClassDefiner;
+ if (definer == null) {
+ definer = loadHiddenClassDefiner();
+ hiddenClassDefiner = definer;
+ }
+ }
+ }
+ return definer;
+ }
+
+ private static HiddenClassDefiner loadHiddenClassDefiner() {
+ try {
+ // Keep JDK15+ hidden-class symbols out of the root constant pool so Java 8 through Java 14
+ // can load this class; defineHiddenNestmate is the only path that needs them.
+ Class> optionType = Class.forName("java.lang.invoke.MethodHandles$Lookup$ClassOption");
+ Object nestmate = Enum.valueOf(optionType.asSubclass(Enum.class), "NESTMATE");
+ Object options = Array.newInstance(optionType, 1);
+ Array.set(options, 0, nestmate);
+ MethodHandle defineHiddenClass =
+ MethodHandles.publicLookup()
+ .findVirtual(
+ Lookup.class,
+ "defineHiddenClass",
+ MethodType.methodType(
+ Lookup.class, byte[].class, boolean.class, options.getClass()))
+ .asFixedArity();
+ return new HiddenClassDefiner(defineHiddenClass, options);
+ } catch (ReflectiveOperationException | RuntimeException e) {
+ throw hiddenClassUnsupported(e);
+ }
+ }
+
+ private static UnsupportedOperationException hiddenClassUnsupported(Throwable cause) {
+ return new UnsupportedOperationException(
+ "Hidden nestmate class definition requires JDK15+ Lookup#defineHiddenClass", cause);
+ }
+
+ private static RuntimeException hiddenClassFailure(Class> neighbor, Throwable cause) {
+ if (cause instanceof RuntimeException) {
+ return (RuntimeException) cause;
+ }
+ if (cause instanceof Error) {
+ throw (Error) cause;
+ }
+ String message = "Cannot define hidden nestmate for " + neighbor.getName() + ".";
+ if (JdkVersion.MAJOR_VERSION >= 25) {
+ message += " " + _JDKAccess.jdk25AccessMessage();
+ }
+ return new IllegalStateException(message, cause);
+ }
+
+ private static final class HiddenClassDefiner {
+ private final MethodHandle defineHiddenClass;
+ private final Object nestmateOptions;
+
+ private HiddenClassDefiner(MethodHandle defineHiddenClass, Object nestmateOptions) {
+ this.defineHiddenClass = defineHiddenClass;
+ this.nestmateOptions = nestmateOptions;
+ }
+
+ private Class> define(Lookup lookup, byte[] bytecodes) throws Throwable {
+ return ((Lookup) defineHiddenClass.invoke(lookup, bytecodes, true, nestmateOptions))
+ .lookupClass();
+ }
+ }
+}
diff --git a/java/fory-core/src/main/java/org/apache/fory/util/unsafe/_JDKAccess.java b/java/fory-core/src/main/java/org/apache/fory/platform/internal/_JDKAccess.java
similarity index 78%
rename from java/fory-core/src/main/java/org/apache/fory/util/unsafe/_JDKAccess.java
rename to java/fory-core/src/main/java/org/apache/fory/platform/internal/_JDKAccess.java
index e9e8774f60..c989207e2c 100644
--- a/java/fory-core/src/main/java/org/apache/fory/util/unsafe/_JDKAccess.java
+++ b/java/fory-core/src/main/java/org/apache/fory/platform/internal/_JDKAccess.java
@@ -17,7 +17,7 @@
* under the License.
*/
-package org.apache.fory.util.unsafe;
+package org.apache.fory.platform.internal;
import java.lang.invoke.CallSite;
import java.lang.invoke.LambdaMetafactory;
@@ -25,7 +25,6 @@
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.invoke.MethodType;
-import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
@@ -39,6 +38,7 @@
import java.util.function.ToLongFunction;
import org.apache.fory.collection.ClassValueCache;
import org.apache.fory.collection.Tuple2;
+import org.apache.fory.platform.AndroidSupport;
import org.apache.fory.platform.GraalvmSupport;
import org.apache.fory.platform.JdkVersion;
import org.apache.fory.type.TypeUtils;
@@ -48,41 +48,41 @@
import org.apache.fory.util.function.ToCharFunction;
import org.apache.fory.util.function.ToFloatFunction;
import org.apache.fory.util.function.ToShortFunction;
-import sun.misc.Unsafe;
-/** Unsafe JDK utils. */
+/** JDK lookup, module, and lambda factory utils. */
// CHECKSTYLE.OFF:TypeName
public class _JDKAccess {
// CHECKSTYLE.ON:TypeName
public static final boolean IS_OPEN_J9;
- public static final Unsafe UNSAFE;
- public static final Class> _INNER_UNSAFE_CLASS;
- public static final Object _INNER_UNSAFE;
+ public static final boolean JDK_INTERNAL_FIELD_ACCESS;
+ public static final boolean JDK_LANG_FIELD_ACCESS;
+ public static final boolean JDK_COLLECTION_FIELD_ACCESS;
+ public static final boolean JDK_CONCURRENT_FIELD_ACCESS;
+ public static final boolean JDK_PROXY_FIELD_ACCESS;
static {
String jmvName = System.getProperty("java.vm.name", "");
IS_OPEN_J9 = jmvName.contains("OpenJ9");
- Unsafe unsafe;
- try {
- Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
- unsafeField.setAccessible(true);
- unsafe = (Unsafe) unsafeField.get(null);
- } catch (Throwable cause) {
- throw new UnsupportedOperationException("Unsafe is not supported in this platform.");
- }
- UNSAFE = unsafe;
- if (JdkVersion.MAJOR_VERSION >= 11) {
- try {
- Field theInternalUnsafeField = Unsafe.class.getDeclaredField("theInternalUnsafe");
- theInternalUnsafeField.setAccessible(true);
- _INNER_UNSAFE = theInternalUnsafeField.get(null);
- _INNER_UNSAFE_CLASS = _INNER_UNSAFE.getClass();
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
+ if (AndroidSupport.IS_ANDROID) {
+ JDK_INTERNAL_FIELD_ACCESS = false;
+ JDK_LANG_FIELD_ACCESS = false;
+ JDK_COLLECTION_FIELD_ACCESS = false;
+ JDK_CONCURRENT_FIELD_ACCESS = false;
+ JDK_PROXY_FIELD_ACCESS = false;
+ } else if (JdkVersion.MAJOR_VERSION >= 25) {
+ // JDK25+ zero-Unsafe mode requires java.base/java.lang.invoke to be opened to fory-core.
+ // Missing that open is an invalid runtime configuration, not a fallback signal.
+ JDK_INTERNAL_FIELD_ACCESS = true;
+ JDK_LANG_FIELD_ACCESS = true;
+ JDK_COLLECTION_FIELD_ACCESS = true;
+ JDK_CONCURRENT_FIELD_ACCESS = true;
+ JDK_PROXY_FIELD_ACCESS = true;
} else {
- _INNER_UNSAFE_CLASS = null;
- _INNER_UNSAFE = null;
+ JDK_INTERNAL_FIELD_ACCESS = true;
+ JDK_LANG_FIELD_ACCESS = true;
+ JDK_COLLECTION_FIELD_ACCESS = true;
+ JDK_CONCURRENT_FIELD_ACCESS = true;
+ JDK_PROXY_FIELD_ACCESS = true;
}
}
@@ -100,6 +100,13 @@ public static Lookup _trustedLookup(Class> objectClass) {
return lookupCache.get(objectClass, () -> _Lookup._trustedLookup(objectClass));
}
+ public static String jdk25AccessMessage() {
+ return "JDK25 zero-Unsafe mode requires java.base/java.lang.invoke to be open to Fory. "
+ + "Use --add-opens=java.base/java.lang.invoke=ALL-UNNAMED when Fory is on the "
+ + "classpath, or --add-opens=java.base/java.lang.invoke=org.apache.fory.core when "
+ + "Fory is on the module path.";
+ }
+
public static T tryMakeFunction(
Lookup lookup, MethodHandle handle, Class functionInterface) {
try {
@@ -132,8 +139,7 @@ public static Function makeJDKFunction(
boxedMethodType(handle.type()));
return (Function) callSite.getTarget().invokeExact();
} catch (Throwable e) {
- UNSAFE.throwException(e);
- throw new IllegalStateException(e);
+ throw ExceptionUtils.throwException(e);
}
}
@@ -153,8 +159,7 @@ public static Consumer makeJDKConsumer(Lookup lookup, MethodHandle handle
boxedMethodType(handle.type()));
return (Consumer) callSite.getTarget().invokeExact();
} catch (Throwable e) {
- UNSAFE.throwException(e);
- throw new IllegalStateException(e);
+ throw ExceptionUtils.throwException(e);
}
}
@@ -174,8 +179,7 @@ public static BiConsumer makeJDKBiConsumer(Lookup lookup, MethodHan
boxedMethodType(handle.type()));
return (BiConsumer) callSite.getTarget().invokeExact();
} catch (Throwable e) {
- UNSAFE.throwException(e);
- throw new IllegalStateException(e);
+ throw ExceptionUtils.throwException(e);
}
}
@@ -208,8 +212,7 @@ public static T makeFunction(Lookup lookup, MethodHandle handle, Method meth
instantiatedMethodType);
return (T) callSite.getTarget().invokeExact();
} catch (Throwable e) {
- UNSAFE.throwException(e);
- throw new IllegalStateException(e);
+ throw ExceptionUtils.throwException(e);
}
}
@@ -243,8 +246,7 @@ public static T makeFunction(Lookup lookup, MethodHandle handle, Class fu
// FIXME(chaokunyang) why use invokeExact will fail.
return (T) callSite.getTarget().invoke();
} catch (Throwable e) {
- UNSAFE.throwException(e);
- throw new IllegalStateException(e);
+ throw ExceptionUtils.throwException(e);
}
}
@@ -296,12 +298,12 @@ public static Object makeGetterFunction(
// represented by handle, then exception will be thrown.
return makeGetterFunction(lookup, handle, Object.class);
} catch (Throwable e) {
- UNSAFE.throwException(e);
- throw new IllegalStateException(e);
+ throw ExceptionUtils.throwException(e);
}
}
private static volatile Method getModuleMethod;
+ private static volatile Method isExportedMethod;
public static Object getModule(Class> cls) {
Preconditions.checkArgument(JdkVersion.MAJOR_VERSION >= 9);
@@ -315,7 +317,31 @@ public static Object getModule(Class> cls) {
try {
return getModuleMethod.invoke(cls);
} catch (IllegalAccessException | InvocationTargetException e) {
+ throw ExceptionUtils.throwException(e);
+ }
+ }
+
+ public static boolean isExported(Class> cls) {
+ if (JdkVersion.MAJOR_VERSION < 9) {
+ return true;
+ }
+ if (cls.isArray()) {
+ return isExported(cls.getComponentType());
+ }
+ if (cls.isPrimitive()) {
+ return true;
+ }
+ try {
+ if (isExportedMethod == null) {
+ Class> moduleClass = Class.forName("java.lang.Module");
+ isExportedMethod = moduleClass.getDeclaredMethod("isExported", String.class);
+ }
+ Package pkg = cls.getPackage();
+ return (Boolean) isExportedMethod.invoke(getModule(cls), pkg == null ? "" : pkg.getName());
+ } catch (ClassNotFoundException | NoSuchMethodException e) {
throw new RuntimeException(e);
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ throw ExceptionUtils.throwException(e);
}
}
@@ -332,7 +358,11 @@ public static Object addReads(Object thisModule, Object otherModule) {
}
return addReadsHandle.invoke(thisModule, otherModule);
} catch (Throwable e) {
- throw new RuntimeException(e);
+ throw ExceptionUtils.throwException(e);
}
}
+
+ public static Lookup privateLookupIn(Class> targetClass, Lookup caller) {
+ return _Lookup.privateLookupIn(targetClass, caller);
+ }
}
diff --git a/java/fory-core/src/main/java/org/apache/fory/util/unsafe/_Lookup.java b/java/fory-core/src/main/java/org/apache/fory/platform/internal/_Lookup.java
similarity index 95%
rename from java/fory-core/src/main/java/org/apache/fory/util/unsafe/_Lookup.java
rename to java/fory-core/src/main/java/org/apache/fory/platform/internal/_Lookup.java
index b719ac0afb..505e74549c 100644
--- a/java/fory-core/src/main/java/org/apache/fory/util/unsafe/_Lookup.java
+++ b/java/fory-core/src/main/java/org/apache/fory/platform/internal/_Lookup.java
@@ -17,7 +17,7 @@
* under the License.
*/
-package org.apache.fory.util.unsafe;
+package org.apache.fory.platform.internal;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
@@ -44,9 +44,9 @@ class _Lookup {
{
try {
Field implLookup = Lookup.class.getDeclaredField("IMPL_LOOKUP");
- long fieldOffset = _JDKAccess.UNSAFE.staticFieldOffset(implLookup);
- Object fieldBase = _JDKAccess.UNSAFE.staticFieldBase(implLookup);
- trustedLookup = (Lookup) _JDKAccess.UNSAFE.getObject(fieldBase, fieldOffset);
+ long fieldOffset = _UnsafeUtils.UNSAFE.staticFieldOffset(implLookup);
+ Object fieldBase = _UnsafeUtils.UNSAFE.staticFieldBase(implLookup);
+ trustedLookup = (Lookup) _UnsafeUtils.UNSAFE.getObject(fieldBase, fieldOffset);
} catch (Throwable ignored) {
// ignored
}
diff --git a/java/fory-core/src/main/java/org/apache/fory/platform/internal/_UnsafeUtils.java b/java/fory-core/src/main/java/org/apache/fory/platform/internal/_UnsafeUtils.java
new file mode 100644
index 0000000000..32555f842e
--- /dev/null
+++ b/java/fory-core/src/main/java/org/apache/fory/platform/internal/_UnsafeUtils.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fory.platform.internal;
+
+import java.lang.reflect.Field;
+import sun.misc.Unsafe;
+
+/** Root-runtime owner for {@link Unsafe}. Java25+ code must use overlay classes instead. */
+// CHECKSTYLE.OFF:TypeName
+public final class _UnsafeUtils {
+ // CHECKSTYLE.ON:TypeName
+ public static final Unsafe UNSAFE;
+
+ static {
+ try {
+ Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
+ unsafeField.setAccessible(true);
+ UNSAFE = (Unsafe) unsafeField.get(null);
+ } catch (Throwable cause) {
+ throw new UnsupportedOperationException("Unsafe is not supported in this platform.");
+ }
+ }
+
+ private _UnsafeUtils() {}
+}
diff --git a/java/fory-core/src/main/java/org/apache/fory/reflect/FieldAccessor.java b/java/fory-core/src/main/java/org/apache/fory/reflect/FieldAccessor.java
index 1690fc0918..0eeff11605 100644
--- a/java/fory-core/src/main/java/org/apache/fory/reflect/FieldAccessor.java
+++ b/java/fory-core/src/main/java/org/apache/fory/reflect/FieldAccessor.java
@@ -19,59 +19,52 @@
package org.apache.fory.reflect;
-import java.lang.invoke.MethodHandle;
-import java.lang.invoke.MethodHandles;
import java.lang.reflect.Field;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-import java.util.function.Function;
-import java.util.function.Predicate;
-import java.util.function.ToDoubleFunction;
-import java.util.function.ToIntFunction;
-import java.util.function.ToLongFunction;
-import org.apache.fory.collection.ClassValueCache;
-import org.apache.fory.collection.Tuple2;
-import org.apache.fory.exception.ForyException;
-import org.apache.fory.platform.AndroidSupport;
-import org.apache.fory.platform.GraalvmSupport;
-import org.apache.fory.platform.UnsafeOps;
-import org.apache.fory.type.TypeUtils;
+import java.lang.reflect.Modifier;
import org.apache.fory.util.Preconditions;
-import org.apache.fory.util.function.Functions;
-import org.apache.fory.util.function.ToByteFunction;
-import org.apache.fory.util.function.ToCharFunction;
-import org.apache.fory.util.function.ToFloatFunction;
-import org.apache.fory.util.function.ToShortFunction;
import org.apache.fory.util.record.RecordUtils;
-import org.apache.fory.util.unsafe._JDKAccess;
-/**
- * Field accessor for primitive types and object types.
- *
- *
Note for primitive types, there will be box/unbox overhead.
- */
-@SuppressWarnings({"unchecked", "rawtypes"})
+/** Field accessor for primitive types and object types. */
public abstract class FieldAccessor {
+ private static final int BOOLEAN_ACCESS = 1;
+ private static final int BYTE_ACCESS = 2;
+ private static final int CHAR_ACCESS = 3;
+ private static final int SHORT_ACCESS = 4;
+ private static final int INT_ACCESS = 5;
+ private static final int LONG_ACCESS = 6;
+ private static final int FLOAT_ACCESS = 7;
+ private static final int DOUBLE_ACCESS = 8;
+ private static final int OBJECT_ACCESS = 9;
+
protected final Field field;
- protected final long fieldOffset;
+ private final int accessKind;
public FieldAccessor(Field field) {
this.field = field;
Preconditions.checkNotNull(field);
- long fieldOffset;
- try {
- fieldOffset = ReflectionUtils.getFieldOffset(field);
- } catch (UnsupportedOperationException e) {
- fieldOffset = -1;
- }
- this.fieldOffset = fieldOffset;
- }
-
- protected FieldAccessor(Field field, long fieldOffset) {
- this.field = field;
- this.fieldOffset = fieldOffset;
+ accessKind = accessKind(field);
+ }
+
+ private static int accessKind(Field field) {
+ Class> fieldType = field.getType();
+ if (fieldType == boolean.class) {
+ return BOOLEAN_ACCESS;
+ } else if (fieldType == byte.class) {
+ return BYTE_ACCESS;
+ } else if (fieldType == char.class) {
+ return CHAR_ACCESS;
+ } else if (fieldType == short.class) {
+ return SHORT_ACCESS;
+ } else if (fieldType == int.class) {
+ return INT_ACCESS;
+ } else if (fieldType == long.class) {
+ return LONG_ACCESS;
+ } else if (fieldType == float.class) {
+ return FLOAT_ACCESS;
+ } else if (fieldType == double.class) {
+ return DOUBLE_ACCESS;
+ }
+ return OBJECT_ACCESS;
}
public abstract Object get(Object obj);
@@ -80,519 +73,150 @@ public void set(Object obj, Object value) {
throw new UnsupportedOperationException("Unsupported for field " + field);
}
+ public void copy(Object sourceObject, Object targetObject) {
+ switch (accessKind) {
+ case BOOLEAN_ACCESS:
+ putBoolean(targetObject, getBoolean(sourceObject));
+ return;
+ case BYTE_ACCESS:
+ putByte(targetObject, getByte(sourceObject));
+ return;
+ case CHAR_ACCESS:
+ putChar(targetObject, getChar(sourceObject));
+ return;
+ case SHORT_ACCESS:
+ putShort(targetObject, getShort(sourceObject));
+ return;
+ case INT_ACCESS:
+ putInt(targetObject, getInt(sourceObject));
+ return;
+ case LONG_ACCESS:
+ putLong(targetObject, getLong(sourceObject));
+ return;
+ case FLOAT_ACCESS:
+ putFloat(targetObject, getFloat(sourceObject));
+ return;
+ case DOUBLE_ACCESS:
+ putDouble(targetObject, getDouble(sourceObject));
+ return;
+ default:
+ putObject(targetObject, getObject(sourceObject));
+ }
+ }
+
+ public void copyObject(Object sourceObject, Object targetObject) {
+ putObject(targetObject, getObject(sourceObject));
+ }
+
public Field getField() {
return field;
}
- public final void putObject(Object targetObject, Object object) {
- // For primitive fields, we must use set() which calls the correct UnsafeOps.putXxx method.
- // UnsafeOps.putObject writes object references, not primitive values.
- if (fieldOffset != -1 && !field.getType().isPrimitive()) {
- UnsafeOps.putObject(targetObject, fieldOffset, object);
- } else {
- set(targetObject, object);
- }
+ public boolean getBoolean(Object targetObject) {
+ return (Boolean) get(targetObject);
}
- public final Object getObject(Object targetObject) {
- // For primitive fields, we must use get() which calls the correct UnsafeOps.getXxx method
- // and returns the boxed value. UnsafeOps.getObject interprets primitive bytes as object
- // refs.
- if (fieldOffset != -1 && !field.getType().isPrimitive()) {
- return UnsafeOps.getObject(targetObject, fieldOffset);
- } else {
- return get(targetObject);
- }
+ public void putBoolean(Object targetObject, boolean value) {
+ set(targetObject, value);
}
- public long getFieldOffset() {
- return fieldOffset;
+ public byte getByte(Object targetObject) {
+ return (Byte) get(targetObject);
}
- void checkObj(Object obj) {
- if (!this.field.getDeclaringClass().isAssignableFrom(obj.getClass())) {
- throw new IllegalArgumentException("Illegal class " + obj.getClass());
- }
+ public void putByte(Object targetObject, byte value) {
+ set(targetObject, value);
}
- @Override
- public String toString() {
- return field.toString();
+ public char getChar(Object targetObject) {
+ return (Character) get(targetObject);
}
- public abstract static class FieldGetter extends FieldAccessor {
-
- private final Object getter;
-
- protected FieldGetter(Field field, Object getter) {
- super(field, -1);
- this.getter = getter;
- }
-
- public Object getGetter() {
- return getter;
- }
+ public void putChar(Object targetObject, char value) {
+ set(targetObject, value);
}
- public static FieldAccessor createAccessor(Field field) {
- if (RecordUtils.isRecord(field.getDeclaringClass())) {
- if (AndroidSupport.IS_ANDROID || GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) {
- return new ReflectiveRecordFieldAccessor(field);
- }
- Object getter;
- try {
- Method getterMethod = field.getDeclaringClass().getDeclaredMethod(field.getName());
- getter = Functions.makeGetterFunction(getterMethod);
- } catch (NoSuchMethodException ex) {
- throw new RuntimeException(ex);
- }
- if (getter instanceof Predicate) {
- return new BooleanGetter(field, (Predicate) getter);
- } else if (getter instanceof ToByteFunction) {
- return new ByteGetter(field, (ToByteFunction) getter);
- } else if (getter instanceof ToCharFunction) {
- return new CharGetter(field, (ToCharFunction) getter);
- } else if (getter instanceof ToShortFunction) {
- return new ShortGetter(field, (ToShortFunction) getter);
- } else if (getter instanceof ToIntFunction) {
- return new IntGetter(field, (ToIntFunction) getter);
- } else if (getter instanceof ToLongFunction) {
- return new LongGetter(field, (ToLongFunction) getter);
- } else if (getter instanceof ToFloatFunction) {
- return new FloatGetter(field, (ToFloatFunction) getter);
- } else if (getter instanceof ToDoubleFunction) {
- return new DoubleGetter(field, (ToDoubleFunction) getter);
- } else {
- return new ObjectGetter(field, (Function) getter);
- }
- }
- if (AndroidSupport.IS_ANDROID) {
- // Android field access must stay reflection-owned: no Unsafe offsets, trusted lookups,
- // generated accessors, or primitive-specific reflection subclasses.
- return new ReflectionFieldAccessor(field);
- }
- if (GraalvmSupport.isGraalBuildTime()) {
- return new GeneratedAccessor(field);
- }
- if (field.getType() == boolean.class) {
- return new BooleanAccessor(field);
- } else if (field.getType() == byte.class) {
- return new ByteAccessor(field);
- } else if (field.getType() == char.class) {
- return new CharAccessor(field);
- } else if (field.getType() == short.class) {
- return new ShortAccessor(field);
- } else if (field.getType() == int.class) {
- return new IntAccessor(field);
- } else if (field.getType() == long.class) {
- return new LongAccessor(field);
- } else if (field.getType() == float.class) {
- return new FloatAccessor(field);
- } else if (field.getType() == double.class) {
- return new DoubleAccessor(field);
- } else {
- return new ObjectAccessor(field);
- }
+ public short getShort(Object targetObject) {
+ return (Short) get(targetObject);
}
- static final class ReflectiveRecordFieldAccessor extends FieldGetter {
- private final Method accessor;
-
- ReflectiveRecordFieldAccessor(Field field) {
- super(field, null);
- try {
- accessor = field.getDeclaringClass().getDeclaredMethod(field.getName());
- accessor.setAccessible(true);
- } catch (NoSuchMethodException | RuntimeException e) {
- throw new ForyException("Failed to create record field accessor for " + field, e);
- }
- }
-
- @Override
- public Object get(Object obj) {
- checkObj(obj);
- try {
- return accessor.invoke(obj);
- } catch (IllegalAccessException | IllegalArgumentException e) {
- throw new ForyException("Failed to read record field reflectively: " + field, e);
- } catch (InvocationTargetException e) {
- throw new ForyException(
- "Record accessor threw while reading field: " + field, e.getCause());
- }
- }
-
- @Override
- public void set(Object obj, Object value) {
- throw new UnsupportedOperationException("Record field is read-only: " + field);
- }
+ public void putShort(Object targetObject, short value) {
+ set(targetObject, value);
}
- /** Primitive boolean accessor. */
- public static class BooleanAccessor extends FieldAccessor {
- public BooleanAccessor(Field field) {
- super(field);
- Preconditions.checkArgument(field.getType() == boolean.class);
- }
-
- @Override
- public Object get(Object obj) {
- checkObj(obj);
- return UnsafeOps.getBoolean(obj, fieldOffset);
- }
-
- @Override
- public void set(Object obj, Object value) {
- checkObj(obj);
- UnsafeOps.putBoolean(obj, fieldOffset, (Boolean) value);
- }
+ public int getInt(Object targetObject) {
+ return (Integer) get(targetObject);
}
- public static class BooleanGetter extends FieldGetter {
- private final Predicate getter;
-
- public BooleanGetter(Field field, Predicate getter) {
- super(field, getter);
- this.getter = getter;
- Preconditions.checkArgument(field.getType() == boolean.class);
- }
-
- @Override
- public Boolean get(Object obj) {
- checkObj(obj);
- return getter.test(obj);
- }
+ public void putInt(Object targetObject, int value) {
+ set(targetObject, value);
}
- /** Primitive byte accessor. */
- public static class ByteAccessor extends FieldAccessor {
- public ByteAccessor(Field field) {
- super(field);
- Preconditions.checkArgument(field.getType() == byte.class);
- }
-
- @Override
- public Byte get(Object obj) {
- checkObj(obj);
- return UnsafeOps.getByte(obj, fieldOffset);
- }
-
- @Override
- public void set(Object obj, Object value) {
- checkObj(obj);
- UnsafeOps.putByte(obj, fieldOffset, (Byte) value);
- }
+ public long getLong(Object targetObject) {
+ return (Long) get(targetObject);
}
- public static class ByteGetter extends FieldGetter {
-
- private final ToByteFunction getter;
-
- public ByteGetter(Field field, ToByteFunction getter) {
- super(field, getter);
- this.getter = getter;
- Preconditions.checkArgument(field.getType() == byte.class);
- }
-
- @Override
- public Byte get(Object obj) {
- return getter.applyAsByte(obj);
- }
+ public void putLong(Object targetObject, long value) {
+ set(targetObject, value);
}
- /** Primitive char accessor. */
- public static class CharAccessor extends FieldAccessor {
- public CharAccessor(Field field) {
- super(field);
- Preconditions.checkArgument(field.getType() == char.class);
- }
-
- @Override
- public Character get(Object obj) {
- checkObj(obj);
- return UnsafeOps.getChar(obj, fieldOffset);
- }
-
- @Override
- public void set(Object obj, Object value) {
- checkObj(obj);
- UnsafeOps.putChar(obj, fieldOffset, (Character) value);
- }
+ public float getFloat(Object targetObject) {
+ return (Float) get(targetObject);
}
- public static class CharGetter extends FieldGetter {
- private final ToCharFunction getter;
-
- public CharGetter(Field field, ToCharFunction getter) {
- super(field, getter);
- this.getter = getter;
- Preconditions.checkArgument(field.getType() == char.class);
- }
-
- @Override
- public Character get(Object obj) {
- return getter.applyAsChar(obj);
- }
+ public void putFloat(Object targetObject, float value) {
+ set(targetObject, value);
}
- /** Primitive short accessor. */
- public static class ShortAccessor extends FieldAccessor {
- public ShortAccessor(Field field) {
- super(field);
- Preconditions.checkArgument(field.getType() == short.class);
- }
-
- @Override
- public Short get(Object obj) {
- checkObj(obj);
- return UnsafeOps.getShort(obj, fieldOffset);
- }
-
- @Override
- public void set(Object obj, Object value) {
- checkObj(obj);
- UnsafeOps.putShort(obj, fieldOffset, (Short) value);
- }
+ public double getDouble(Object targetObject) {
+ return (Double) get(targetObject);
}
- public static class ShortGetter extends FieldGetter {
- private final ToShortFunction getter;
-
- public ShortGetter(Field field, ToShortFunction getter) {
- super(field, getter);
- this.getter = getter;
- Preconditions.checkArgument(field.getType() == short.class);
- }
-
- @Override
- public Short get(Object obj) {
- return getter.applyAsShort(obj);
- }
- }
-
- /** Primitive int accessor. */
- public static class IntAccessor extends FieldAccessor {
- public IntAccessor(Field field) {
- super(field);
- Preconditions.checkArgument(field.getType() == int.class);
- }
-
- @Override
- public Integer get(Object obj) {
- checkObj(obj);
- return UnsafeOps.getInt(obj, fieldOffset);
- }
-
- @Override
- public void set(Object obj, Object value) {
- checkObj(obj);
- UnsafeOps.putInt(obj, fieldOffset, (Integer) value);
- }
- }
-
- public static class IntGetter extends FieldGetter {
- private final ToIntFunction getter;
-
- public IntGetter(Field field, ToIntFunction getter) {
- super(field, getter);
- this.getter = getter;
- Preconditions.checkArgument(field.getType() == int.class);
- }
-
- @Override
- public Integer get(Object obj) {
- return getter.applyAsInt(obj);
- }
+ public void putDouble(Object targetObject, double value) {
+ set(targetObject, value);
}
- /** Primitive long accessor. */
- public static class LongAccessor extends FieldAccessor {
- public LongAccessor(Field field) {
- super(field);
- Preconditions.checkArgument(field.getType() == long.class);
- }
-
- @Override
- public Long get(Object obj) {
- checkObj(obj);
- return UnsafeOps.getLong(obj, fieldOffset);
- }
-
- @Override
- public void set(Object obj, Object value) {
- checkObj(obj);
- UnsafeOps.putLong(obj, fieldOffset, (Long) value);
- }
+ public void putObject(Object targetObject, Object object) {
+ set(targetObject, object);
}
- public static class LongGetter extends FieldGetter {
- private final ToLongFunction getter;
-
- public LongGetter(Field field, ToLongFunction getter) {
- super(field, getter);
- this.getter = getter;
- Preconditions.checkArgument(field.getType() == long.class);
- }
-
- @Override
- public Long get(Object obj) {
- return getter.applyAsLong(obj);
- }
+ public Object getObject(Object targetObject) {
+ return get(targetObject);
}
- /** Primitive float accessor. */
- public static class FloatAccessor extends FieldAccessor {
- public FloatAccessor(Field field) {
- super(field);
- Preconditions.checkArgument(field.getType() == float.class);
- }
-
- @Override
- public Object get(Object obj) {
- checkObj(obj);
- return UnsafeOps.getFloat(obj, fieldOffset);
- }
-
- @Override
- public void set(Object obj, Object value) {
- checkObj(obj);
- UnsafeOps.putFloat(obj, fieldOffset, (Float) value);
- }
+ final void checkObj(Object obj) {
+ // Unsafe offset access does not validate the receiver. A wrong receiver is a Fory
+ // programming error, so keep this debug-only instead of adding production hot-path checks.
+ assert field.getDeclaringClass().isInstance(obj) : illegalObject(obj);
}
- public static class FloatGetter extends FieldGetter {
- private final ToFloatFunction getter;
-
- public FloatGetter(Field field, ToFloatFunction getter) {
- super(field, getter);
- this.getter = getter;
- Preconditions.checkArgument(field.getType() == float.class);
- }
-
- @Override
- public Float get(Object obj) {
- return getter.applyAsFloat(obj);
- }
+ private String illegalObject(Object obj) {
+ return "Illegal class " + (obj == null ? null : obj.getClass());
}
- /** Primitive double accessor. */
- public static class DoubleAccessor extends FieldAccessor {
- public DoubleAccessor(Field field) {
- super(field);
- Preconditions.checkArgument(field.getType() == double.class);
- }
-
- @Override
- public Object get(Object obj) {
- checkObj(obj);
- return UnsafeOps.getDouble(obj, fieldOffset);
- }
-
- @Override
- public void set(Object obj, Object value) {
- checkObj(obj);
- UnsafeOps.putDouble(obj, fieldOffset, (Double) value);
- }
+ @Override
+ public String toString() {
+ return field.toString();
}
- public static class DoubleGetter extends FieldGetter {
- private final ToDoubleFunction getter;
-
- public DoubleGetter(Field field, ToDoubleFunction getter) {
- super(field, getter);
- this.getter = getter;
- Preconditions.checkArgument(field.getType() == double.class);
- }
-
- @Override
- public Double get(Object obj) {
- return getter.applyAsDouble(obj);
- }
- }
+ public abstract static class FieldGetter extends FieldAccessor {
+ private final Object getter;
- /** Object accessor. */
- public static class ObjectAccessor extends FieldAccessor {
- public ObjectAccessor(Field field) {
+ protected FieldGetter(Field field, Object getter) {
super(field);
- Preconditions.checkArgument(!TypeUtils.isPrimitive(field.getType()));
- }
-
- @Override
- public Object get(Object obj) {
- checkObj(obj);
- return UnsafeOps.getObject(obj, fieldOffset);
- }
-
- @Override
- public void set(Object obj, Object value) {
- checkObj(obj);
- UnsafeOps.putObject(obj, fieldOffset, value);
- }
- }
-
- public static class ObjectGetter extends FieldGetter {
- private final Function getter;
-
- public ObjectGetter(Field field, Function getter) {
- super(field, getter);
this.getter = getter;
- Preconditions.checkArgument(!field.getType().isPrimitive(), field);
}
- @Override
- public Object get(Object obj) {
- return getter.apply(obj);
+ public Object getGetter() {
+ return getter;
}
}
- static final class GeneratedAccessor extends FieldAccessor {
- private static final ClassValueCache>>
- cache = ClassValueCache.newClassKeyCache(8);
-
- private final MethodHandle getter;
- private final MethodHandle setter;
-
- GeneratedAccessor(Field field) {
- super(field, -1);
- ConcurrentMap> map =
- cache.get(field.getDeclaringClass(), ConcurrentHashMap::new);
- ;
- MethodHandles.Lookup lookup = _JDKAccess._trustedLookup(field.getDeclaringClass());
- Tuple2 tuple2 =
- map.computeIfAbsent(
- field.getName(),
- k -> {
- try {
- MethodHandle getter =
- lookup.findGetter(
- field.getDeclaringClass(), field.getName(), field.getType());
- MethodHandle setter =
- lookup.findSetter(
- field.getDeclaringClass(), field.getName(), field.getType());
- return Tuple2.of(getter, setter);
- } catch (IllegalAccessException | NoSuchFieldException ex) {
- throw new RuntimeException(ex);
- }
- });
- getter = tuple2.f0;
- setter = tuple2.f1;
- }
-
- @Override
- public Object get(Object obj) {
- try {
- return getter.invoke(obj);
- } catch (Throwable e) {
- throw new RuntimeException(e);
- }
- }
-
- @Override
- public void set(Object obj, Object value) {
- try {
- setter.invoke(obj, value);
- } catch (Throwable e) {
- throw new RuntimeException(e);
- }
+ public static FieldAccessor createAccessor(Field field) {
+ Preconditions.checkArgument(!Modifier.isStatic(field.getModifiers()), field);
+ if (RecordUtils.isRecord(field.getDeclaringClass())) {
+ return RecordFieldAccessors.createAccessor(field);
}
+ return InstanceFieldAccessors.createAccessor(field);
}
}
diff --git a/java/fory-core/src/main/java/org/apache/fory/reflect/InstanceFieldAccessors.java b/java/fory-core/src/main/java/org/apache/fory/reflect/InstanceFieldAccessors.java
new file mode 100644
index 0000000000..cc664e32b7
--- /dev/null
+++ b/java/fory-core/src/main/java/org/apache/fory/reflect/InstanceFieldAccessors.java
@@ -0,0 +1,542 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fory.reflect;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import org.apache.fory.annotation.Internal;
+import org.apache.fory.collection.ClassValueCache;
+import org.apache.fory.collection.Tuple2;
+import org.apache.fory.exception.ForyException;
+import org.apache.fory.platform.AndroidSupport;
+import org.apache.fory.platform.GraalvmSupport;
+import org.apache.fory.platform.internal._JDKAccess;
+import org.apache.fory.platform.internal._UnsafeUtils;
+import org.apache.fory.util.Preconditions;
+import sun.misc.Unsafe;
+
+/**
+ * Non-record instance field accessor owner.
+ *
+ *
This class is public only so generated serializers can name {@link InstanceAccessor} as a
+ * concrete field type on JDK25+. Callers must still create accessors through {@link
+ * FieldAccessor#createAccessor(Field)} so platform dispatch stays centralized.
+ */
+@Internal
+public final class InstanceFieldAccessors {
+ private static final int BOOLEAN_ACCESS = 1;
+ private static final int BYTE_ACCESS = 2;
+ private static final int CHAR_ACCESS = 3;
+ private static final int SHORT_ACCESS = 4;
+ private static final int INT_ACCESS = 5;
+ private static final int LONG_ACCESS = 6;
+ private static final int FLOAT_ACCESS = 7;
+ private static final int DOUBLE_ACCESS = 8;
+ private static final int OBJECT_ACCESS = 9;
+
+ private InstanceFieldAccessors() {}
+
+ static FieldAccessor createAccessor(Field field) {
+ Preconditions.checkArgument(!Modifier.isStatic(field.getModifiers()), field);
+ if (AndroidSupport.IS_ANDROID) {
+ return new ReflectionAccessor(field);
+ }
+ if (GraalvmSupport.isGraalBuildTime()) {
+ return new GeneratedAccessor(field);
+ }
+ return new InstanceAccessor(field);
+ }
+
+ private static int accessKind(Field field) {
+ Class> fieldType = field.getType();
+ if (fieldType == boolean.class) {
+ return BOOLEAN_ACCESS;
+ } else if (fieldType == byte.class) {
+ return BYTE_ACCESS;
+ } else if (fieldType == char.class) {
+ return CHAR_ACCESS;
+ } else if (fieldType == short.class) {
+ return SHORT_ACCESS;
+ } else if (fieldType == int.class) {
+ return INT_ACCESS;
+ } else if (fieldType == long.class) {
+ return LONG_ACCESS;
+ } else if (fieldType == float.class) {
+ return FLOAT_ACCESS;
+ } else if (fieldType == double.class) {
+ return DOUBLE_ACCESS;
+ }
+ return OBJECT_ACCESS;
+ }
+
+ private static final class ReflectionAccessor extends FieldAccessor {
+ private ReflectionAccessor(Field field) {
+ super(field);
+ try {
+ field.setAccessible(true);
+ } catch (RuntimeException e) {
+ throw new ForyException("Failed to make field accessible: " + field, e);
+ }
+ }
+
+ @Override
+ public Object get(Object obj) {
+ try {
+ return field.get(obj);
+ } catch (IllegalAccessException | IllegalArgumentException e) {
+ throw new ForyException("Failed to read field reflectively: " + field, e);
+ }
+ }
+
+ @Override
+ public void set(Object obj, Object value) {
+ try {
+ field.set(obj, value);
+ } catch (IllegalAccessException | IllegalArgumentException e) {
+ throw new ForyException("Failed to write field reflectively: " + field, e);
+ }
+ }
+ }
+
+ /** Public only for generated serializers; use {@link FieldAccessor#createAccessor(Field)}. */
+ public static final class InstanceAccessor extends FieldAccessor {
+ private static final Unsafe UNSAFE = _UnsafeUtils.UNSAFE;
+
+ private final long fieldOffset;
+ private final int accessKind;
+
+ InstanceAccessor(Field field) {
+ super(field);
+ fieldOffset = fieldOffset(field);
+ accessKind = accessKind(field);
+ }
+
+ private static long fieldOffset(Field field) {
+ return UNSAFE.objectFieldOffset(field);
+ }
+
+ @Override
+ public Object get(Object obj) {
+ checkObj(obj);
+ switch (accessKind) {
+ case BOOLEAN_ACCESS:
+ return UNSAFE.getBoolean(obj, fieldOffset);
+ case BYTE_ACCESS:
+ return UNSAFE.getByte(obj, fieldOffset);
+ case CHAR_ACCESS:
+ return UNSAFE.getChar(obj, fieldOffset);
+ case SHORT_ACCESS:
+ return UNSAFE.getShort(obj, fieldOffset);
+ case INT_ACCESS:
+ return UNSAFE.getInt(obj, fieldOffset);
+ case LONG_ACCESS:
+ return UNSAFE.getLong(obj, fieldOffset);
+ case FLOAT_ACCESS:
+ return UNSAFE.getFloat(obj, fieldOffset);
+ case DOUBLE_ACCESS:
+ return UNSAFE.getDouble(obj, fieldOffset);
+ case OBJECT_ACCESS:
+ return UNSAFE.getObject(obj, fieldOffset);
+ default:
+ throw new IllegalStateException("Unsupported access kind " + accessKind);
+ }
+ }
+
+ @Override
+ public void set(Object obj, Object value) {
+ checkObj(obj);
+ switch (accessKind) {
+ case BOOLEAN_ACCESS:
+ UNSAFE.putBoolean(obj, fieldOffset, (Boolean) value);
+ return;
+ case BYTE_ACCESS:
+ UNSAFE.putByte(obj, fieldOffset, (Byte) value);
+ return;
+ case CHAR_ACCESS:
+ UNSAFE.putChar(obj, fieldOffset, (Character) value);
+ return;
+ case SHORT_ACCESS:
+ UNSAFE.putShort(obj, fieldOffset, (Short) value);
+ return;
+ case INT_ACCESS:
+ UNSAFE.putInt(obj, fieldOffset, (Integer) value);
+ return;
+ case LONG_ACCESS:
+ UNSAFE.putLong(obj, fieldOffset, (Long) value);
+ return;
+ case FLOAT_ACCESS:
+ UNSAFE.putFloat(obj, fieldOffset, (Float) value);
+ return;
+ case DOUBLE_ACCESS:
+ UNSAFE.putDouble(obj, fieldOffset, (Double) value);
+ return;
+ case OBJECT_ACCESS:
+ UNSAFE.putObject(obj, fieldOffset, value);
+ return;
+ default:
+ throw new IllegalStateException("Unsupported access kind " + accessKind);
+ }
+ }
+
+ @Override
+ public void copy(Object sourceObject, Object targetObject) {
+ checkObj(sourceObject);
+ checkObj(targetObject);
+ switch (accessKind) {
+ case BOOLEAN_ACCESS:
+ UNSAFE.putBoolean(
+ targetObject, fieldOffset, UNSAFE.getBoolean(sourceObject, fieldOffset));
+ return;
+ case BYTE_ACCESS:
+ UNSAFE.putByte(targetObject, fieldOffset, UNSAFE.getByte(sourceObject, fieldOffset));
+ return;
+ case CHAR_ACCESS:
+ UNSAFE.putChar(targetObject, fieldOffset, UNSAFE.getChar(sourceObject, fieldOffset));
+ return;
+ case SHORT_ACCESS:
+ UNSAFE.putShort(targetObject, fieldOffset, UNSAFE.getShort(sourceObject, fieldOffset));
+ return;
+ case INT_ACCESS:
+ UNSAFE.putInt(targetObject, fieldOffset, UNSAFE.getInt(sourceObject, fieldOffset));
+ return;
+ case LONG_ACCESS:
+ UNSAFE.putLong(targetObject, fieldOffset, UNSAFE.getLong(sourceObject, fieldOffset));
+ return;
+ case FLOAT_ACCESS:
+ UNSAFE.putFloat(targetObject, fieldOffset, UNSAFE.getFloat(sourceObject, fieldOffset));
+ return;
+ case DOUBLE_ACCESS:
+ UNSAFE.putDouble(targetObject, fieldOffset, UNSAFE.getDouble(sourceObject, fieldOffset));
+ return;
+ case OBJECT_ACCESS:
+ UNSAFE.putObject(targetObject, fieldOffset, UNSAFE.getObject(sourceObject, fieldOffset));
+ return;
+ default:
+ super.copy(sourceObject, targetObject);
+ }
+ }
+
+ @Override
+ public void copyObject(Object sourceObject, Object targetObject) {
+ checkObj(sourceObject);
+ checkObj(targetObject);
+ if (accessKind == OBJECT_ACCESS) {
+ UNSAFE.putObject(targetObject, fieldOffset, UNSAFE.getObject(sourceObject, fieldOffset));
+ } else {
+ super.copyObject(sourceObject, targetObject);
+ }
+ }
+
+ @Override
+ public boolean getBoolean(Object obj) {
+ checkObj(obj);
+ return UNSAFE.getBoolean(obj, fieldOffset);
+ }
+
+ @Override
+ public void putBoolean(Object obj, boolean value) {
+ checkObj(obj);
+ UNSAFE.putBoolean(obj, fieldOffset, value);
+ }
+
+ @Override
+ public byte getByte(Object obj) {
+ checkObj(obj);
+ return UNSAFE.getByte(obj, fieldOffset);
+ }
+
+ @Override
+ public void putByte(Object obj, byte value) {
+ checkObj(obj);
+ UNSAFE.putByte(obj, fieldOffset, value);
+ }
+
+ @Override
+ public char getChar(Object obj) {
+ checkObj(obj);
+ return UNSAFE.getChar(obj, fieldOffset);
+ }
+
+ @Override
+ public void putChar(Object obj, char value) {
+ checkObj(obj);
+ UNSAFE.putChar(obj, fieldOffset, value);
+ }
+
+ @Override
+ public short getShort(Object obj) {
+ checkObj(obj);
+ return UNSAFE.getShort(obj, fieldOffset);
+ }
+
+ @Override
+ public void putShort(Object obj, short value) {
+ checkObj(obj);
+ UNSAFE.putShort(obj, fieldOffset, value);
+ }
+
+ @Override
+ public int getInt(Object obj) {
+ checkObj(obj);
+ return UNSAFE.getInt(obj, fieldOffset);
+ }
+
+ @Override
+ public void putInt(Object obj, int value) {
+ checkObj(obj);
+ UNSAFE.putInt(obj, fieldOffset, value);
+ }
+
+ @Override
+ public long getLong(Object obj) {
+ checkObj(obj);
+ return UNSAFE.getLong(obj, fieldOffset);
+ }
+
+ @Override
+ public void putLong(Object obj, long value) {
+ checkObj(obj);
+ UNSAFE.putLong(obj, fieldOffset, value);
+ }
+
+ @Override
+ public float getFloat(Object obj) {
+ checkObj(obj);
+ return UNSAFE.getFloat(obj, fieldOffset);
+ }
+
+ @Override
+ public void putFloat(Object obj, float value) {
+ checkObj(obj);
+ UNSAFE.putFloat(obj, fieldOffset, value);
+ }
+
+ @Override
+ public double getDouble(Object obj) {
+ checkObj(obj);
+ return UNSAFE.getDouble(obj, fieldOffset);
+ }
+
+ @Override
+ public void putDouble(Object obj, double value) {
+ checkObj(obj);
+ UNSAFE.putDouble(obj, fieldOffset, value);
+ }
+ }
+
+ static final class GeneratedAccessor extends FieldAccessor {
+ private static final ClassValueCache>>
+ cache = ClassValueCache.newClassKeyCache(8);
+
+ private final MethodHandle getter;
+ private final MethodHandle setter;
+
+ GeneratedAccessor(Field field) {
+ super(field);
+ ConcurrentMap> map =
+ cache.get(field.getDeclaringClass(), ConcurrentHashMap::new);
+ MethodHandles.Lookup lookup = _JDKAccess._trustedLookup(field.getDeclaringClass());
+ Tuple2 tuple2 =
+ map.computeIfAbsent(
+ field.getName(),
+ k -> {
+ try {
+ MethodHandle getter =
+ lookup.findGetter(
+ field.getDeclaringClass(), field.getName(), field.getType());
+ MethodHandle setter =
+ lookup.findSetter(
+ field.getDeclaringClass(), field.getName(), field.getType());
+ return Tuple2.of(getter, setter);
+ } catch (IllegalAccessException | NoSuchFieldException ex) {
+ throw new RuntimeException(ex);
+ }
+ });
+ getter = tuple2.f0;
+ setter = tuple2.f1;
+ }
+
+ @Override
+ public Object get(Object obj) {
+ try {
+ return getter.invoke(obj);
+ } catch (Throwable e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void set(Object obj, Object value) {
+ try {
+ setter.invoke(obj, value);
+ } catch (Throwable e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public boolean getBoolean(Object obj) {
+ try {
+ return (boolean) getter.invoke(obj);
+ } catch (Throwable e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void putBoolean(Object obj, boolean value) {
+ try {
+ setter.invoke(obj, value);
+ } catch (Throwable e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public byte getByte(Object obj) {
+ try {
+ return (byte) getter.invoke(obj);
+ } catch (Throwable e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void putByte(Object obj, byte value) {
+ try {
+ setter.invoke(obj, value);
+ } catch (Throwable e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public char getChar(Object obj) {
+ try {
+ return (char) getter.invoke(obj);
+ } catch (Throwable e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void putChar(Object obj, char value) {
+ try {
+ setter.invoke(obj, value);
+ } catch (Throwable e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public short getShort(Object obj) {
+ try {
+ return (short) getter.invoke(obj);
+ } catch (Throwable e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void putShort(Object obj, short value) {
+ try {
+ setter.invoke(obj, value);
+ } catch (Throwable e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public int getInt(Object obj) {
+ try {
+ return (int) getter.invoke(obj);
+ } catch (Throwable e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void putInt(Object obj, int value) {
+ try {
+ setter.invoke(obj, value);
+ } catch (Throwable e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public long getLong(Object obj) {
+ try {
+ return (long) getter.invoke(obj);
+ } catch (Throwable e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void putLong(Object obj, long value) {
+ try {
+ setter.invoke(obj, value);
+ } catch (Throwable e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public float getFloat(Object obj) {
+ try {
+ return (float) getter.invoke(obj);
+ } catch (Throwable e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void putFloat(Object obj, float value) {
+ try {
+ setter.invoke(obj, value);
+ } catch (Throwable e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public double getDouble(Object obj) {
+ try {
+ return (double) getter.invoke(obj);
+ } catch (Throwable e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void putDouble(Object obj, double value) {
+ try {
+ setter.invoke(obj, value);
+ } catch (Throwable e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+}
diff --git a/java/fory-core/src/main/java/org/apache/fory/reflect/ObjectCreators.java b/java/fory-core/src/main/java/org/apache/fory/reflect/ObjectCreators.java
deleted file mode 100644
index 00fc668539..0000000000
--- a/java/fory-core/src/main/java/org/apache/fory/reflect/ObjectCreators.java
+++ /dev/null
@@ -1,323 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.fory.reflect;
-
-import java.lang.invoke.MethodHandle;
-import java.lang.invoke.MethodHandles.Lookup;
-import java.lang.invoke.MethodType;
-import java.lang.reflect.Constructor;
-import org.apache.fory.collection.ClassValueCache;
-import org.apache.fory.collection.Tuple2;
-import org.apache.fory.exception.ForyException;
-import org.apache.fory.platform.AndroidSupport;
-import org.apache.fory.platform.GraalvmSupport;
-import org.apache.fory.platform.JdkVersion;
-import org.apache.fory.platform.UnsafeOps;
-import org.apache.fory.util.record.RecordUtils;
-import org.apache.fory.util.unsafe._JDKAccess;
-
-/**
- * Factory class for creating and caching {@link ObjectCreator} instances.
- *
- *
This class provides a centralized way to obtain optimized object creators for different types.
- * It automatically selects the most appropriate creation strategy based on the target type and
- * runtime environment:
- *
- *
- *
Record types: Uses {@link RecordObjectCreator} with MethodHandle for
- * parameterized constructor invocation
- *
Classes with no-arg constructors: Uses {@link
- * DeclaredNoArgCtrObjectCreator} with MethodHandle for fast invocation
- *
Classes without accessible constructors: Uses {@link UnsafeObjectCreator}
- * with platform-specific unsafe allocation
- *
GraalVM native image compatibility: Uses {@link
- * ParentNoArgCtrObjectCreator} for constructor generate-based creation when needed
- *
Android compatibility: Uses reflection for records and no-arg
- * constructors, and throws when no supported reflective construction path exists
- *
- *
- *
All created ObjectCreator instances are cached using a soft reference cache to improve
- * performance on repeated requests for the same type.
- *
- *
Thread Safety: This class and all returned ObjectCreator instances are
- * thread-safe and can be safely used across multiple threads concurrently.
- */
-@SuppressWarnings("unchecked")
-public class ObjectCreators {
- private static final ClassValueCache> cache =
- ClassValueCache.newClassKeySoftCache(8);
-
- /**
- * Returns an optimized ObjectCreator for the given type.
- *
- *
This method automatically selects the most appropriate creation strategy based on the type
- * characteristics and caches the result for future use. The selection logic prioritizes
- * performance and platform compatibility.
- *
- * @param the type for which to create an ObjectCreator
- * @param type the Class object representing the target type
- * @return a cached ObjectCreator instance optimized for the given type
- * @throws ForyException if the type cannot be instantiated (e.g., missing no-arg constructor in
- * GraalVM native image)
- */
- public static ObjectCreator getObjectCreator(Class type) {
- return (ObjectCreator) cache.get(type, () -> creategetObjectCreator(type));
- }
-
- private static ObjectCreator creategetObjectCreator(Class type) {
- if (RecordUtils.isRecord(type)) {
- return new RecordObjectCreator<>(type);
- }
- Constructor noArgConstructor = ReflectionUtils.getNoArgConstructor(type);
- if (AndroidSupport.IS_ANDROID) {
- if (noArgConstructor != null) {
- return new ReflectiveNoArgCtrObjectCreator<>(type, noArgConstructor);
- }
- return new UnsupportedObjectCreator<>(type);
- }
- if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) {
- if (noArgConstructor != null) {
- return new DeclaredNoArgCtrObjectCreator<>(type);
- } else {
- return new UnsafeObjectCreator<>(type);
- }
- }
- if (noArgConstructor == null) {
- return new UnsafeObjectCreator<>(type);
- }
- return new DeclaredNoArgCtrObjectCreator<>(type);
- }
-
- private static final class ReflectiveNoArgCtrObjectCreator extends ObjectCreator {
- private final Constructor constructor;
-
- private ReflectiveNoArgCtrObjectCreator(Class type, Constructor constructor) {
- super(type);
- this.constructor = constructor;
- try {
- constructor.setAccessible(true);
- } catch (RuntimeException e) {
- throw new ForyException("Failed to make no-arg constructor accessible for " + type, e);
- }
- }
-
- @Override
- public T newInstance() {
- try {
- return constructor.newInstance();
- } catch (Exception e) {
- throw new ForyException("Failed to create instance using no-arg constructor: " + type, e);
- }
- }
-
- @Override
- public T newInstanceWithArguments(Object... arguments) {
- throw new UnsupportedOperationException();
- }
- }
-
- private static final class UnsupportedObjectCreator extends ObjectCreator {
- private UnsupportedObjectCreator(Class type) {
- super(type);
- }
-
- @Override
- public T newInstance() {
- throw new ForyException(
- "Android cannot create " + type + " without an accessible no-arg constructor");
- }
-
- @Override
- public T newInstanceWithArguments(Object... arguments) {
- throw new ForyException(
- "Android cannot create " + type + " without a supported constructor path");
- }
- }
-
- public static final class UnsafeObjectCreator extends ObjectCreator {
-
- public UnsafeObjectCreator(Class type) {
- super(type);
- }
-
- @Override
- public T newInstance() {
- return UnsafeOps.newInstance(type);
- }
-
- @Override
- public T newInstanceWithArguments(Object... arguments) {
- throw new UnsupportedOperationException();
- }
- }
-
- public static final class DeclaredNoArgCtrObjectCreator extends ObjectCreator {
- private final MethodHandle handle;
-
- public DeclaredNoArgCtrObjectCreator(Class type) {
- super(type);
- handle = ReflectionUtils.getCtrHandle(type, true);
- }
-
- @Override
- public T newInstance() {
- try {
- return (T) handle.invoke();
- } catch (Throwable e) {
- throw new RuntimeException(e);
- }
- }
-
- @Override
- public T newInstanceWithArguments(Object... arguments) {
- throw new UnsupportedOperationException();
- }
- }
-
- public static final class RecordObjectCreator extends ObjectCreator {
- private final MethodHandle handle;
- private final Constructor> constructor;
-
- public RecordObjectCreator(Class type) {
- super(type);
- Tuple2 tuple2 = RecordUtils.getRecordConstructor(type);
- constructor = tuple2.f0;
- handle = tuple2.f1;
- if (AndroidSupport.IS_ANDROID
- || (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE && JdkVersion.MAJOR_VERSION >= 25)) {
- try {
- constructor.setAccessible(true);
- } catch (Throwable t) {
- throw new ForyException(
- "Failed to create instance, please provide a public constructor for " + type, t);
- }
- }
- }
-
- @Override
- public T newInstance() {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public T newInstanceWithArguments(Object... arguments) {
- try {
- // compile-time constant is eligible for dead code elimination.
- if (AndroidSupport.IS_ANDROID
- || handle == null
- || (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE && JdkVersion.MAJOR_VERSION >= 25)) {
- return (T) constructor.newInstance(arguments);
- } else {
- // Regular path: use method handle
- return (T) handle.invokeWithArguments(arguments);
- }
- } catch (Throwable e) {
- throw new ForyException("Failed to create record instance: " + type, e);
- }
- }
- }
-
- public static final class ParentNoArgCtrObjectCreator extends ObjectCreator {
- private static volatile Object reflectionFactory;
- private static volatile MethodHandle newConstructorForSerializationMethod;
-
- private final Constructor constructor;
-
- public ParentNoArgCtrObjectCreator(Class type) {
- super(type);
- this.constructor = createSerializationConstructor(type);
- }
-
- private static Constructor createSerializationConstructor(Class type) {
- try {
- // Get ReflectionFactory instance
- if (reflectionFactory == null) {
- Class> reflectionFactoryClass;
- if (JdkVersion.MAJOR_VERSION >= 9) {
- reflectionFactoryClass = Class.forName("jdk.internal.reflect.ReflectionFactory");
- } else {
- reflectionFactoryClass = Class.forName("sun.reflect.ReflectionFactory");
- }
- Lookup lookup = _JDKAccess._trustedLookup(reflectionFactoryClass);
- MethodHandle handle =
- lookup.findStatic(
- reflectionFactoryClass,
- "getReflectionFactory",
- MethodType.methodType(reflectionFactoryClass));
- reflectionFactory = handle.invoke();
- newConstructorForSerializationMethod =
- lookup.findVirtual(
- reflectionFactoryClass,
- "newConstructorForSerialization",
- MethodType.methodType(Constructor.class, Class.class, Constructor.class));
- }
- // Find a public no-arg constructor in parent classes that we can use as a template
- Constructor> parentConstructor = findPublicNoArgConstructor(type);
- if (parentConstructor == null) {
- // Use Object's constructor as fallback
- parentConstructor = Object.class.getDeclaredConstructor();
- } else {
- try {
- parentConstructor.newInstance();
- } catch (Throwable ignored) {
- parentConstructor = Object.class.getDeclaredConstructor();
- }
- }
- // Create serialization constructor using ReflectionFactory
- return (Constructor)
- newConstructorForSerializationMethod.invoke(reflectionFactory, type, parentConstructor);
- } catch (Throwable e) {
- throw new ForyException(
- "Failed to create instance, please provide a no-arg constructor for " + type, e);
- }
- }
-
- private static Constructor> findPublicNoArgConstructor(Class> type) {
- Class> current = type.getSuperclass();
- while (current != null && current != Object.class) {
- try {
- Constructor> constructor = current.getDeclaredConstructor();
- if (constructor.getModifiers() == java.lang.reflect.Modifier.PUBLIC) {
- return constructor;
- }
- } catch (NoSuchMethodException ignored) {
- // Continue searching
- }
- current = current.getSuperclass();
- }
- return null;
- }
-
- @Override
- public T newInstance() {
- try {
- return constructor.newInstance();
- } catch (Exception e) {
- throw new ForyException(
- "Failed to create instance, please provide a no-arg constructor for " + type, e);
- }
- }
-
- @Override
- public T newInstanceWithArguments(Object... arguments) {
- throw new UnsupportedOperationException();
- }
- }
-}
diff --git a/java/fory-core/src/main/java/org/apache/fory/reflect/ObjectCreator.java b/java/fory-core/src/main/java/org/apache/fory/reflect/ObjectInstantiator.java
similarity index 75%
rename from java/fory-core/src/main/java/org/apache/fory/reflect/ObjectCreator.java
rename to java/fory-core/src/main/java/org/apache/fory/reflect/ObjectInstantiator.java
index cc6d0c7d70..971dcf4542 100644
--- a/java/fory-core/src/main/java/org/apache/fory/reflect/ObjectCreator.java
+++ b/java/fory-core/src/main/java/org/apache/fory/reflect/ObjectInstantiator.java
@@ -22,24 +22,24 @@
import javax.annotation.concurrent.ThreadSafe;
/**
- * Abstract base class for creating instances of a given type.
+ * Abstract base class for instantiating instances of a given type.
*
*
This class provides a unified interface for object instantiation across different creation
* strategies such as constructor invocation, unsafe allocation, and record creation.
* Implementations handle various scenarios including no-arg constructors, parameterized
* constructors for records, and platform-specific optimizations.
*
- *
Thread Safety: All implementations of ObjectCreator are thread-safe and can
- * be safely used across multiple threads concurrently. The underlying creation mechanisms
- * (MethodHandle, Constructor, UnsafeOps.newInstance) are all thread-safe.
+ *
Thread Safety: All implementations of ObjectInstantiator are thread-safe and
+ * can be safely used across multiple threads concurrently. The underlying instantiation mechanisms
+ * (MethodHandle, Constructor, and supported constructor-bypassing allocation) are all thread-safe.
*
- * @param the type of objects this creator can instantiate
+ * @param the type of objects this instantiator can instantiate
*/
@ThreadSafe
-public abstract class ObjectCreator {
+public abstract class ObjectInstantiator {
protected final Class type;
- protected ObjectCreator(Class type) {
+ protected ObjectInstantiator(Class type) {
this.type = type;
}
@@ -48,7 +48,8 @@ protected ObjectCreator(Class type) {
*
* @return a new instance of type T
* @throws RuntimeException if instance creation fails
- * @throws UnsupportedOperationException if this creator doesn't support parameterless creation
+ * @throws UnsupportedOperationException if this instantiator doesn't support parameterless
+ * creation
*/
public abstract T newInstance();
@@ -61,7 +62,8 @@ protected ObjectCreator(Class type) {
* @param arguments the arguments to pass to the constructor
* @return a new instance of type T
* @throws RuntimeException if instance creation fails
- * @throws UnsupportedOperationException if this creator doesn't support parameterized creation
+ * @throws UnsupportedOperationException if this instantiator doesn't support parameterized
+ * creation
*/
public abstract T newInstanceWithArguments(Object... arguments);
}
diff --git a/java/fory-core/src/main/java/org/apache/fory/reflect/ObjectInstantiators.java b/java/fory-core/src/main/java/org/apache/fory/reflect/ObjectInstantiators.java
new file mode 100644
index 0000000000..b40f4d85ad
--- /dev/null
+++ b/java/fory-core/src/main/java/org/apache/fory/reflect/ObjectInstantiators.java
@@ -0,0 +1,513 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fory.reflect;
+
+import java.io.ObjectStreamClass;
+import java.io.Serializable;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Modifier;
+import org.apache.fory.annotation.Internal;
+import org.apache.fory.collection.ClassValueCache;
+import org.apache.fory.collection.Tuple2;
+import org.apache.fory.exception.ForyException;
+import org.apache.fory.platform.AndroidSupport;
+import org.apache.fory.platform.GraalvmSupport;
+import org.apache.fory.platform.JdkVersion;
+import org.apache.fory.platform.internal._JDKAccess;
+import org.apache.fory.util.record.RecordUtils;
+
+/**
+ * Factory class for creating {@link ObjectInstantiator} instances.
+ *
+ *
This class provides a centralized way to obtain optimized object instantiators for different
+ * types. It automatically selects the most appropriate creation strategy based on the target type
+ * and runtime environment:
+ *
+ *
+ *
Record types: Uses {@link RecordObjectInstantiator} with MethodHandle for
+ * parameterized constructor invocation
+ *
Classes with no-arg constructors: Uses {@link
+ * DeclaredNoArgCtrInstantiator} with MethodHandle for fast invocation
+ *
Classes without accessible constructors: Uses JDK8-24 Unsafe allocation or
+ * serialization constructor creation through the runtime ReflectionFactory owner
+ *
Android compatibility: Uses reflection for records and no-arg
+ * constructors, and throws when no supported reflective construction path exists
+ *
+ *
+ *
The static {@link #getObjectInstantiator(Class)} method keeps the process-global construction
+ * cache. Runtime-owned paths should use {@link
+ * org.apache.fory.resolver.TypeResolver#getObjectInstantiator(Class)} so ObjectStream-compatible
+ * instantiators stay scoped to the Fory runtime.
+ *
+ *
Thread Safety: This class and all returned ObjectInstantiator instances are
+ * thread-safe and can be safely used across multiple threads concurrently.
+ */
+@SuppressWarnings("unchecked")
+public class ObjectInstantiators {
+ private static final ClassValueCache> cache =
+ ClassValueCache.newClassKeySoftCache(8);
+
+ /**
+ * Returns an optimized ObjectInstantiator for the given type.
+ *
+ *
This method automatically selects the most appropriate creation strategy based on the type
+ * characteristics and caches the result for future use. The selection logic prioritizes
+ * performance and platform compatibility.
+ *
+ * @param the type for which to create an ObjectInstantiator
+ * @param type the Class object representing the target type
+ * @return a cached ObjectInstantiator instance optimized for the given type
+ * @throws ForyException if the type cannot be instantiated (e.g., missing no-arg constructor in
+ * GraalVM native image)
+ */
+ public static ObjectInstantiator getObjectInstantiator(Class type) {
+ return (ObjectInstantiator) cache.get(type, () -> createObjectInstantiator(type));
+ }
+
+ /** Creates an uncached object instantiator for runtime-scoped registries. */
+ @Internal
+ public static ObjectInstantiator createObjectInstantiator(Class type) {
+ if (RecordUtils.isRecord(type)) {
+ return new RecordObjectInstantiator<>(type);
+ }
+ Constructor noArgConstructor = ReflectionUtils.getNoArgConstructor(type);
+ if (AndroidSupport.IS_ANDROID) {
+ if (noArgConstructor != null) {
+ return new ReflectiveNoArgCtrInstantiator<>(type, noArgConstructor);
+ }
+ return new UnsupportedObjectInstantiator<>(
+ type, "Android cannot create " + type + " without an accessible no-arg constructor");
+ }
+ if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) {
+ if (noArgConstructor != null) {
+ return new DeclaredNoArgCtrInstantiator<>(type);
+ } else if (JdkVersion.MAJOR_VERSION >= 25) {
+ if (Serializable.class.isAssignableFrom(type)
+ && serializationConstructorClass(type) == Object.class) {
+ return new GraalvmObjectInstantiator<>(type);
+ }
+ return new UnsupportedObjectInstantiator<>(
+ type,
+ "GraalVM native image on JDK25+ cannot create "
+ + type
+ + " without an accessible no-arg constructor because ObjectStream construction "
+ + "would change ordinary Fory object-creation semantics");
+ } else {
+ return new UnsafeObjectInstantiator<>(type);
+ }
+ }
+ if (noArgConstructor == null) {
+ if (JdkVersion.MAJOR_VERSION >= 25) {
+ return new ReflectionFactoryInstantiator<>(type);
+ }
+ return new UnsafeObjectInstantiator<>(type);
+ }
+ return new DeclaredNoArgCtrInstantiator<>(type);
+ }
+
+ /**
+ * Creates an uncached empty-instance instantiator for Java ObjectStream-compatible serializers.
+ */
+ @Internal
+ public static ObjectInstantiator createObjectStreamInstantiator(Class type) {
+ if (AndroidSupport.IS_ANDROID) {
+ Constructor noArgConstructor = ReflectionUtils.getNoArgConstructor(type);
+ if (noArgConstructor != null) {
+ return new ReflectiveNoArgCtrInstantiator<>(type, noArgConstructor);
+ }
+ return new UnsupportedObjectInstantiator<>(
+ type, "Android cannot create " + type + " without an accessible no-arg constructor");
+ }
+ if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE && JdkVersion.MAJOR_VERSION < 25) {
+ return new UnsafeObjectInstantiator<>(type);
+ }
+ if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE && JdkVersion.MAJOR_VERSION >= 25) {
+ return new ObjectStreamInstantiator<>(type);
+ }
+ return new ParentNoArgCtrInstantiator<>(type);
+ }
+
+ private static RuntimeException makeException(Class> type, Throwable cause) {
+ Throwable target = unwrapConstructorFailure(cause);
+ // Keep constructor invocation failures outside ForyException so top-level deserialization can
+ // attach the read-object trail before surfacing the error to users.
+ return new RuntimeException("Failed to create instance for " + type, target);
+ }
+
+ private static Throwable unwrapConstructorFailure(Throwable cause) {
+ if (cause instanceof InvocationTargetException) {
+ Throwable target = ((InvocationTargetException) cause).getTargetException();
+ if (target != null) {
+ return target;
+ }
+ }
+ return cause;
+ }
+
+ private static final class ReflectiveNoArgCtrInstantiator extends ObjectInstantiator {
+ private final Constructor constructor;
+
+ private ReflectiveNoArgCtrInstantiator(Class type, Constructor constructor) {
+ super(type);
+ this.constructor = constructor;
+ try {
+ constructor.setAccessible(true);
+ } catch (RuntimeException e) {
+ throw new ForyException("Failed to make no-arg constructor accessible for " + type, e);
+ }
+ }
+
+ @Override
+ public T newInstance() {
+ try {
+ return constructor.newInstance();
+ } catch (Exception e) {
+ throw makeException(type, e);
+ }
+ }
+
+ @Override
+ public T newInstanceWithArguments(Object... arguments) {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ private static final class UnsupportedObjectInstantiator extends ObjectInstantiator {
+ private final String message;
+
+ private UnsupportedObjectInstantiator(Class type, String message) {
+ super(type);
+ this.message = message;
+ }
+
+ @Override
+ public T newInstance() {
+ throw new ForyException(message);
+ }
+
+ @Override
+ public T newInstanceWithArguments(Object... arguments) {
+ throw new ForyException(message);
+ }
+ }
+
+ public static final class DeclaredNoArgCtrInstantiator extends ObjectInstantiator {
+ private final MethodHandle handle;
+
+ public DeclaredNoArgCtrInstantiator(Class type) {
+ super(type);
+ handle = ReflectionUtils.getCtrHandle(type, true);
+ }
+
+ @Override
+ public T newInstance() {
+ try {
+ return (T) handle.invoke();
+ } catch (Throwable e) {
+ throw makeException(type, e);
+ }
+ }
+
+ @Override
+ public T newInstanceWithArguments(Object... arguments) {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ public static final class RecordObjectInstantiator extends ObjectInstantiator {
+ private final MethodHandle handle;
+ private final Constructor> constructor;
+
+ public RecordObjectInstantiator(Class type) {
+ super(type);
+ Tuple2 tuple2 = RecordUtils.getRecordConstructor(type);
+ constructor = tuple2.f0;
+ handle =
+ tuple2.f1 == null
+ ? null
+ : tuple2.f1.asSpreader(Object[].class, constructor.getParameterCount());
+ if (AndroidSupport.IS_ANDROID
+ || (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE && JdkVersion.MAJOR_VERSION >= 25)) {
+ try {
+ constructor.setAccessible(true);
+ } catch (Throwable t) {
+ throw new ForyException(
+ "Failed to create instance, please provide a public constructor for " + type, t);
+ }
+ }
+ }
+
+ @Override
+ public T newInstance() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public T newInstanceWithArguments(Object... arguments) {
+ try {
+ // compile-time constant is eligible for dead code elimination.
+ if (AndroidSupport.IS_ANDROID
+ || handle == null
+ || (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE && JdkVersion.MAJOR_VERSION >= 25)) {
+ return (T) constructor.newInstance(arguments);
+ } else {
+ // Regular path: use method handle
+ return (T) handle.invoke(arguments);
+ }
+ } catch (Throwable e) {
+ throw makeException(type, e);
+ }
+ }
+ }
+
+ private abstract static class ObjectStreamClassInstantiator extends ObjectInstantiator {
+ private volatile ObjectStreamClass objectStreamClass;
+
+ private ObjectStreamClassInstantiator(Class type) {
+ super(type);
+ if (!GraalvmSupport.isGraalBuildTime()) {
+ objectStreamClass = ObjectStreamClass.lookupAny(type);
+ }
+ }
+
+ @Override
+ public T newInstance() {
+ try {
+ return type.cast(ObjectStreamAccess.newInstance(objectStreamClass()));
+ } catch (Throwable e) {
+ throw makeException(type, e);
+ }
+ }
+
+ @Override
+ public T newInstanceWithArguments(Object... arguments) {
+ throw new UnsupportedOperationException();
+ }
+
+ private ObjectStreamClass objectStreamClass() {
+ ObjectStreamClass localObjectStreamClass = objectStreamClass;
+ if (localObjectStreamClass == null) {
+ // Static Fory instances can be built into GraalVM native images. A build-time
+ // ObjectStreamClass can retain GraalVM's bad ReflectionFactory constructor, so cache the
+ // descriptor only after the image reaches runtime.
+ localObjectStreamClass = ObjectStreamClass.lookupAny(type);
+ objectStreamClass = localObjectStreamClass;
+ }
+ return localObjectStreamClass;
+ }
+ }
+
+ public static final class ObjectStreamInstantiator extends ObjectStreamClassInstantiator {
+ public ObjectStreamInstantiator(Class type) {
+ super(type);
+ }
+ }
+
+ static final class GraalvmObjectInstantiator extends ObjectStreamClassInstantiator {
+ GraalvmObjectInstantiator(Class type) {
+ super(type);
+ if (!Serializable.class.isAssignableFrom(type)
+ || serializationConstructorClass(type) != Object.class) {
+ throw new ForyException(
+ "GraalVM JDK25+ ObjectStreamClass-backed object creation is only valid when "
+ + "the serialization constructor class is java.lang.Object for "
+ + type);
+ }
+ }
+ }
+
+ private static final class ObjectStreamAccess {
+ private static final MethodHandle NEW_INSTANCE = newInstanceHandle();
+
+ private static Object newInstance(ObjectStreamClass objectStreamClass) throws Throwable {
+ return NEW_INSTANCE.invoke(objectStreamClass);
+ }
+
+ private static MethodHandle newInstanceHandle() {
+ try {
+ // GraalVM JDK25+ native image handles ObjectStreamClass allocation through its
+ // serialization metadata. Direct ReflectionFactory serialization constructors can produce
+ // Object instances there, so Serializable empty-instance creation must use this owner path.
+ return _JDKAccess._trustedLookup(ObjectStreamClass.class)
+ .findVirtual(
+ ObjectStreamClass.class, "newInstance", MethodType.methodType(Object.class));
+ } catch (Throwable e) {
+ throw new ExceptionInInitializerError(e);
+ }
+ }
+ }
+
+ static final class ReflectionFactoryInstantiator extends ObjectInstantiator {
+ private static final Constructor> OBJECT_CONSTRUCTOR = objectConstructor();
+ private final Constructor constructor;
+
+ ReflectionFactoryInstantiator(Class type) {
+ super(type);
+ constructor = createBypassConstructor(type);
+ }
+
+ private static Constructor> objectConstructor() {
+ try {
+ return Object.class.getDeclaredConstructor();
+ } catch (NoSuchMethodException e) {
+ throw new ExceptionInInitializerError(e);
+ }
+ }
+
+ private static Constructor createBypassConstructor(Class type) {
+ try {
+ return (Constructor)
+ ReflectionFactoryAccess.newConstructorForSerialization(type, OBJECT_CONSTRUCTOR);
+ } catch (Throwable e) {
+ throw new ForyException(
+ "Failed to create constructor-bypassing instantiator for " + type, e);
+ }
+ }
+
+ @Override
+ public T newInstance() {
+ try {
+ return constructor.newInstance();
+ } catch (Exception e) {
+ throw makeException(type, e);
+ }
+ }
+
+ @Override
+ public T newInstanceWithArguments(Object... arguments) {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ public static final class ParentNoArgCtrInstantiator extends ObjectInstantiator {
+ private final Constructor constructor;
+
+ public ParentNoArgCtrInstantiator(Class type) {
+ super(type);
+ this.constructor = createSerializationConstructor(type);
+ }
+
+ private static Constructor createSerializationConstructor(Class type) {
+ try {
+ Constructor> parentConstructor = findSerializationConstructor(type);
+ return (Constructor)
+ ReflectionFactoryAccess.newConstructorForSerialization(type, parentConstructor);
+ } catch (Throwable e) {
+ throw new ForyException(
+ "Failed to create instance, please provide a no-arg constructor for " + type, e);
+ }
+ }
+
+ private static Constructor> findSerializationConstructor(Class> type)
+ throws NoSuchMethodException {
+ if (!Serializable.class.isAssignableFrom(type)) {
+ throw new ForyException("ObjectStream instantiation requires Serializable type " + type);
+ }
+ Class> current = serializationConstructorClass(type);
+ Constructor> constructor = current.getDeclaredConstructor();
+ if (!validSerializationConstructor(type, current, constructor)) {
+ throw new ForyException(
+ "First non-Serializable superclass "
+ + current.getName()
+ + " does not expose a valid no-arg constructor for "
+ + type);
+ }
+ return constructor;
+ }
+
+ private static boolean validSerializationConstructor(
+ Class> type, Class> constructorClass, Constructor> constructor) {
+ int modifiers = constructor.getModifiers();
+ if (Modifier.isPublic(modifiers) || Modifier.isProtected(modifiers)) {
+ return true;
+ }
+ if (Modifier.isPrivate(modifiers)) {
+ return false;
+ }
+ return ReflectionUtils.getPackage(type).equals(ReflectionUtils.getPackage(constructorClass));
+ }
+
+ @Override
+ public T newInstance() {
+ try {
+ return constructor.newInstance();
+ } catch (Exception e) {
+ throw makeException(type, e);
+ }
+ }
+
+ @Override
+ public T newInstanceWithArguments(Object... arguments) {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ private static Class> serializationConstructorClass(Class> type) {
+ Class> current = type.getSuperclass();
+ // Java ObjectStream reconstruction skips every Serializable class constructor and invokes only
+ // the first non-Serializable superclass no-arg constructor.
+ while (current != null && Serializable.class.isAssignableFrom(current)) {
+ current = current.getSuperclass();
+ }
+ return current == null ? Object.class : current;
+ }
+
+ private static final class ReflectionFactoryAccess {
+ private static final Object REFLECTION_FACTORY;
+ private static final MethodHandle NEW_CONSTRUCTOR_FOR_SERIALIZATION;
+
+ static {
+ try {
+ Class> reflectionFactoryClass = reflectionFactoryClass();
+ MethodHandles.Lookup lookup = _JDKAccess._trustedLookup(reflectionFactoryClass);
+ MethodHandle getReflectionFactory =
+ lookup.findStatic(
+ reflectionFactoryClass,
+ "getReflectionFactory",
+ MethodType.methodType(reflectionFactoryClass));
+ REFLECTION_FACTORY = getReflectionFactory.invoke();
+ NEW_CONSTRUCTOR_FOR_SERIALIZATION =
+ lookup.findVirtual(
+ reflectionFactoryClass,
+ "newConstructorForSerialization",
+ MethodType.methodType(Constructor.class, Class.class, Constructor.class));
+ } catch (Throwable e) {
+ throw new ExceptionInInitializerError(e);
+ }
+ }
+
+ private static Class> reflectionFactoryClass() throws ClassNotFoundException {
+ if (JdkVersion.MAJOR_VERSION >= 25) {
+ return Class.forName("jdk.internal.reflect.ReflectionFactory");
+ }
+ return Class.forName("sun.reflect.ReflectionFactory");
+ }
+
+ private static Constructor> newConstructorForSerialization(
+ Class> type, Constructor> parentConstructor) throws Throwable {
+ return (Constructor>)
+ NEW_CONSTRUCTOR_FOR_SERIALIZATION.invoke(REFLECTION_FACTORY, type, parentConstructor);
+ }
+ }
+}
diff --git a/java/fory-core/src/main/java/org/apache/fory/reflect/RecordFieldAccessors.java b/java/fory-core/src/main/java/org/apache/fory/reflect/RecordFieldAccessors.java
new file mode 100644
index 0000000000..bc841a9c4a
--- /dev/null
+++ b/java/fory-core/src/main/java/org/apache/fory/reflect/RecordFieldAccessors.java
@@ -0,0 +1,286 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fory.reflect;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.ToDoubleFunction;
+import java.util.function.ToIntFunction;
+import java.util.function.ToLongFunction;
+import org.apache.fory.exception.ForyException;
+import org.apache.fory.platform.AndroidSupport;
+import org.apache.fory.platform.GraalvmSupport;
+import org.apache.fory.reflect.FieldAccessor.FieldGetter;
+import org.apache.fory.util.Preconditions;
+import org.apache.fory.util.function.Functions;
+import org.apache.fory.util.function.ToByteFunction;
+import org.apache.fory.util.function.ToCharFunction;
+import org.apache.fory.util.function.ToFloatFunction;
+import org.apache.fory.util.function.ToShortFunction;
+
+final class RecordFieldAccessors {
+ private RecordFieldAccessors() {}
+
+ static FieldAccessor createAccessor(Field field) {
+ if (AndroidSupport.IS_ANDROID) {
+ return new ReflectiveRecordFieldAccessor(field);
+ }
+ if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) {
+ return new ReflectiveRecordFieldAccessor(field);
+ }
+ Object getter;
+ try {
+ Method getterMethod = field.getDeclaringClass().getDeclaredMethod(field.getName());
+ getter = Functions.makeGetterFunction(getterMethod);
+ } catch (NoSuchMethodException ex) {
+ throw new RuntimeException(ex);
+ }
+ if (getter instanceof Predicate) {
+ return new BooleanGetter(field, (Predicate) getter);
+ } else if (getter instanceof ToByteFunction) {
+ return new ByteGetter(field, (ToByteFunction) getter);
+ } else if (getter instanceof ToCharFunction) {
+ return new CharGetter(field, (ToCharFunction) getter);
+ } else if (getter instanceof ToShortFunction) {
+ return new ShortGetter(field, (ToShortFunction) getter);
+ } else if (getter instanceof ToIntFunction) {
+ return new IntGetter(field, (ToIntFunction) getter);
+ } else if (getter instanceof ToLongFunction) {
+ return new LongGetter(field, (ToLongFunction) getter);
+ } else if (getter instanceof ToFloatFunction) {
+ return new FloatGetter(field, (ToFloatFunction) getter);
+ } else if (getter instanceof ToDoubleFunction) {
+ return new DoubleGetter(field, (ToDoubleFunction) getter);
+ } else {
+ return new ObjectGetter(field, (Function) getter);
+ }
+ }
+
+ static final class ReflectiveRecordFieldAccessor extends FieldGetter {
+ private final Method accessor;
+
+ ReflectiveRecordFieldAccessor(Field field) {
+ super(field, null);
+ try {
+ accessor = field.getDeclaringClass().getDeclaredMethod(field.getName());
+ accessor.setAccessible(true);
+ } catch (NoSuchMethodException | RuntimeException e) {
+ throw new ForyException("Failed to create record field accessor for " + field, e);
+ }
+ }
+
+ @Override
+ public Object get(Object obj) {
+ checkObj(obj);
+ try {
+ return accessor.invoke(obj);
+ } catch (IllegalAccessException | IllegalArgumentException e) {
+ throw new ForyException("Failed to read record field reflectively: " + field, e);
+ } catch (InvocationTargetException e) {
+ throw new ForyException(
+ "Record accessor threw while reading field: " + field, e.getCause());
+ }
+ }
+
+ @Override
+ public void set(Object obj, Object value) {
+ throw new UnsupportedOperationException("Record field is read-only: " + field);
+ }
+ }
+
+ private static class BooleanGetter extends FieldGetter {
+ private final Predicate getter;
+
+ public BooleanGetter(Field field, Predicate getter) {
+ super(field, getter);
+ this.getter = getter;
+ Preconditions.checkArgument(field.getType() == boolean.class);
+ }
+
+ @Override
+ public Boolean get(Object obj) {
+ return getBoolean(obj);
+ }
+
+ @Override
+ public boolean getBoolean(Object obj) {
+ checkObj(obj);
+ return getter.test(obj);
+ }
+ }
+
+ private static class ByteGetter extends FieldGetter {
+ private final ToByteFunction getter;
+
+ public ByteGetter(Field field, ToByteFunction getter) {
+ super(field, getter);
+ this.getter = getter;
+ Preconditions.checkArgument(field.getType() == byte.class);
+ }
+
+ @Override
+ public Byte get(Object obj) {
+ return getByte(obj);
+ }
+
+ @Override
+ public byte getByte(Object obj) {
+ return getter.applyAsByte(obj);
+ }
+ }
+
+ private static class CharGetter extends FieldGetter {
+ private final ToCharFunction getter;
+
+ public CharGetter(Field field, ToCharFunction getter) {
+ super(field, getter);
+ this.getter = getter;
+ Preconditions.checkArgument(field.getType() == char.class);
+ }
+
+ @Override
+ public Character get(Object obj) {
+ return getChar(obj);
+ }
+
+ @Override
+ public char getChar(Object obj) {
+ return getter.applyAsChar(obj);
+ }
+ }
+
+ private static class ShortGetter extends FieldGetter {
+ private final ToShortFunction getter;
+
+ public ShortGetter(Field field, ToShortFunction getter) {
+ super(field, getter);
+ this.getter = getter;
+ Preconditions.checkArgument(field.getType() == short.class);
+ }
+
+ @Override
+ public Short get(Object obj) {
+ return getShort(obj);
+ }
+
+ @Override
+ public short getShort(Object obj) {
+ return getter.applyAsShort(obj);
+ }
+ }
+
+ private static class IntGetter extends FieldGetter {
+ private final ToIntFunction getter;
+
+ public IntGetter(Field field, ToIntFunction getter) {
+ super(field, getter);
+ this.getter = getter;
+ Preconditions.checkArgument(field.getType() == int.class);
+ }
+
+ @Override
+ public Integer get(Object obj) {
+ return getInt(obj);
+ }
+
+ @Override
+ public int getInt(Object obj) {
+ return getter.applyAsInt(obj);
+ }
+ }
+
+ private static class LongGetter extends FieldGetter {
+ private final ToLongFunction getter;
+
+ public LongGetter(Field field, ToLongFunction getter) {
+ super(field, getter);
+ this.getter = getter;
+ Preconditions.checkArgument(field.getType() == long.class);
+ }
+
+ @Override
+ public Long get(Object obj) {
+ return getLong(obj);
+ }
+
+ @Override
+ public long getLong(Object obj) {
+ return getter.applyAsLong(obj);
+ }
+ }
+
+ private static class FloatGetter extends FieldGetter {
+ private final ToFloatFunction getter;
+
+ public FloatGetter(Field field, ToFloatFunction getter) {
+ super(field, getter);
+ this.getter = getter;
+ Preconditions.checkArgument(field.getType() == float.class);
+ }
+
+ @Override
+ public Float get(Object obj) {
+ return getFloat(obj);
+ }
+
+ @Override
+ public float getFloat(Object obj) {
+ return getter.applyAsFloat(obj);
+ }
+ }
+
+ private static class DoubleGetter extends FieldGetter {
+ private final ToDoubleFunction getter;
+
+ public DoubleGetter(Field field, ToDoubleFunction getter) {
+ super(field, getter);
+ this.getter = getter;
+ Preconditions.checkArgument(field.getType() == double.class);
+ }
+
+ @Override
+ public Double get(Object obj) {
+ return getDouble(obj);
+ }
+
+ @Override
+ public double getDouble(Object obj) {
+ return getter.applyAsDouble(obj);
+ }
+ }
+
+ private static class ObjectGetter extends FieldGetter {
+ private final Function getter;
+
+ public ObjectGetter(Field field, Function getter) {
+ super(field, getter);
+ this.getter = getter;
+ Preconditions.checkArgument(!field.getType().isPrimitive(), field);
+ }
+
+ @Override
+ public Object get(Object obj) {
+ return getter.apply(obj);
+ }
+ }
+}
diff --git a/java/fory-core/src/main/java/org/apache/fory/reflect/ReflectionUtils.java b/java/fory-core/src/main/java/org/apache/fory/reflect/ReflectionUtils.java
index 2af7dd0d0b..82647de3ae 100644
--- a/java/fory-core/src/main/java/org/apache/fory/reflect/ReflectionUtils.java
+++ b/java/fory-core/src/main/java/org/apache/fory/reflect/ReflectionUtils.java
@@ -51,13 +51,11 @@
import org.apache.fory.collection.ClassValueCache;
import org.apache.fory.exception.ForyException;
import org.apache.fory.platform.AndroidSupport;
-import org.apache.fory.platform.GraalvmSupport;
-import org.apache.fory.platform.UnsafeOps;
+import org.apache.fory.platform.internal._JDKAccess;
import org.apache.fory.util.ExceptionUtils;
import org.apache.fory.util.Preconditions;
import org.apache.fory.util.StringUtils;
import org.apache.fory.util.function.Functions;
-import org.apache.fory.util.unsafe._JDKAccess;
/** Reflection util. */
@Internal
@@ -456,42 +454,13 @@ public static List getSortedFields(Class> cls, boolean searchParent) {
public static List