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 @@ + true org.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: * *

{@code
  * 
@@ -56,7 +56,7 @@
  *   --add-modules=jdk.incubator.vector
  * 
  *
- * 
+ * 
  * 
  *   org.openjdk.jmh
  *   jmh-core
@@ -68,11 +68,6 @@
  *   ${jmh.version}
  *   provided
  * 
- * 
- *   org.apache.fory
- *   fory-simd
- *   ${project.version}
- * 
  * }
* *

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.testng testng @@ -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 @@ 11 11 UTF-8 + - - org.apache.fory - benchmark - ${project.version} - org.apache.fory fory-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 [![Maven Version](https://img.shields.io/maven-central/v/org.apache.fory/fory-core?style=for-the-badge)](https://search.maven.org/#search|gav|1|g:"org.apache.fory"%20AND%20a:"fory-core") -[![Java Version](https://img.shields.io/badge/Java-8%20to%2025-blue?style=for-the-badge)](https://www.oracle.com/java/) +[![Java Version](https://img.shields.io/badge/Java-8%2B-blue?style=for-the-badge)](https://www.oracle.com/java/) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg?style=for-the-badge)](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 @@ 8 8 ${basedir}/.. + ${project.build.directory}/multi-release-classes + ${project.build.directory}/jdk25-test-classes @@ -93,6 +95,7 @@ shade + false true @@ -157,11 +160,11 @@ - + - inject-java9-module-info + inject-multi-release-classes package run @@ -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.plugins maven-antrun-plugin 3.1.0 - compile-java16-sources + compile-jdk25-classes + process-classes + + run + + + + + + + + + + + + + + + + + + + + + compile-jdk25-module-info process-classes run - - - - + - + - + - + - clean-java16-package-classes - prepare-package + prepare-jdk25-test-classes + process-test-classes run - + + + + + + + + + + + + + + + + + + - inject-java16-classes - package + verify-jdk25-multi-release-jar + verify run - - - + + + + + + - - - - 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 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 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 loadOrGenCompatibleLayerCodecClass @SuppressWarnings("unchecked") static Class 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 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) + 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) 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 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: - * - *

- * - *

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: + * + *

+ * + *

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 getFieldValues(Collection fields, Object o) { List results = new ArrayList<>(fields.size()); for (Field field : fields) { - // UnsafeOps.objectFieldOffset(field) can't handle primitive field. + // Unsafe.objectFieldOffset(field) can't handle primitive field. Object fieldValue = FieldAccessor.createAccessor(field).get(o); results.add(fieldValue); } return results; } - public static long getFieldOffset(Field field) { - if (AndroidSupport.IS_ANDROID) { - throw new UnsupportedOperationException( - "Field offsets are not supported on Android: " + field); - } - if (GraalvmSupport.isGraalBuildTime()) { - // See more details at - // https://www.graalvm.org/latest/reference-manual/native-image/metadata/Compatibility/#unsafe-memory-access - throw new IllegalStateException( - "Field offset will change between graalvm build time and runtime, " - + "should bye accessed by following graalvm auto rewrite pattern."); - } - if (field == null) { - return -1; - } - return UnsafeOps.objectFieldOffset(field); - } - - public static long getFieldOffset(Class cls, String fieldName) { - Field field = getFieldNullable(cls, fieldName); - return getFieldOffset(field); - } - - public static long getFieldOffsetChecked(Class cls, String fieldName) { - long offset = getFieldOffset(cls, fieldName); - Preconditions.checkArgument(offset != -1); - return offset; - } - public static void setObjectFieldValue(Object obj, String fieldName, Object value) { setObjectFieldValue(obj, getField(obj.getClass(), fieldName), value); } @@ -499,30 +468,13 @@ public static void setObjectFieldValue(Object obj, String fieldName, Object valu public static void setObjectFieldValue(Object obj, Field field, Object value) { Preconditions.checkArgument( !field.getType().isPrimitive(), "Field %s is primitive type", field); - if (AndroidSupport.IS_ANDROID) { - try { - field.setAccessible(true); - field.set(obj, value); - return; - } catch (IllegalAccessException | IllegalArgumentException e) { - throw new ForyException("Failed to write object field reflectively: " + field, e); - } - } - UnsafeOps.putObject(obj, UnsafeOps.objectFieldOffset(field), value); + FieldAccessor.createAccessor(field).putObject(obj, value); } public static T getObjectFieldValue(Object obj, Field field) { Preconditions.checkArgument( !field.getType().isPrimitive(), "Field %s is primitive type", field); - if (AndroidSupport.IS_ANDROID) { - try { - field.setAccessible(true); - return (T) field.get(obj); - } catch (IllegalAccessException | IllegalArgumentException e) { - throw new ForyException("Failed to read object field reflectively: " + field, e); - } - } - return (T) UnsafeOps.getObject(obj, UnsafeOps.objectFieldOffset(field)); + return (T) FieldAccessor.createAccessor(field).getObject(obj); } /** @@ -538,16 +490,7 @@ public static Object getObjectFieldValue(Object obj, String fieldName) { Field field = cls.getDeclaredField(fieldName); Preconditions.checkArgument( !field.getType().isPrimitive(), "Field %s is primitive type", field); - if (AndroidSupport.IS_ANDROID) { - try { - field.setAccessible(true); - return field.get(obj); - } catch (IllegalAccessException | IllegalArgumentException e) { - throw new ForyException("Failed to read object field reflectively: " + field, e); - } - } - long fieldOffset = UnsafeOps.objectFieldOffset(field); - return UnsafeOps.getObject(obj, fieldOffset); + return FieldAccessor.createAccessor(field).getObject(obj); // CHECKSTYLE.OFF:EmptyCatchBlock } catch (NoSuchFieldException ignored) { } diff --git a/java/fory-core/src/main/java/org/apache/fory/reflect/UnsafeObjectInstantiator.java b/java/fory-core/src/main/java/org/apache/fory/reflect/UnsafeObjectInstantiator.java new file mode 100644 index 0000000000..85503d32b2 --- /dev/null +++ b/java/fory-core/src/main/java/org/apache/fory/reflect/UnsafeObjectInstantiator.java @@ -0,0 +1,66 @@ +/* + * 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 org.apache.fory.annotation.Internal; +import org.apache.fory.exception.ForyException; +import org.apache.fory.platform.AndroidSupport; +import org.apache.fory.platform.JdkVersion; +import org.apache.fory.platform.internal._UnsafeUtils; +import sun.misc.Unsafe; + +/** JDK8-24 Unsafe-backed instantiator for classes without an invocable constructor. */ +@SuppressWarnings("unchecked") +@Internal +final class UnsafeObjectInstantiator extends ObjectInstantiator { + private static final Unsafe UNSAFE = AndroidSupport.IS_ANDROID ? null : _UnsafeUtils.UNSAFE; + private static final boolean UNSAFE_ALLOCATION_AVAILABLE = + UNSAFE != null && JdkVersion.MAJOR_VERSION < 25; + + UnsafeObjectInstantiator(Class type) { + super(type); + } + + @Override + public T newInstance() { + if (!UNSAFE_ALLOCATION_AVAILABLE) { + throw unsupported(type); + } + try { + return (T) UNSAFE.allocateInstance(type); + } catch (InstantiationException e) { + throw allocationFailed(type, e); + } + } + + @Override + public T newInstanceWithArguments(Object... arguments) { + throw new UnsupportedOperationException(); + } + + private static ForyException unsupported(Class type) { + return new ForyException( + "Constructor-bypassing Unsafe allocation is unsupported in this runtime for " + type); + } + + private static ForyException allocationFailed(Class type, InstantiationException cause) { + return new ForyException("Failed to allocate instance for " + type, cause); + } +} diff --git a/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java b/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java index 3a94bc805b..d5b45fab05 100644 --- a/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java +++ b/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java @@ -50,6 +50,7 @@ import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; +import java.util.IdentityHashMap; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; @@ -112,11 +113,11 @@ import org.apache.fory.meta.TypeExtMeta; import org.apache.fory.platform.AndroidSupport; import org.apache.fory.platform.GraalvmSupport; -import org.apache.fory.reflect.ObjectCreators; import org.apache.fory.reflect.ReflectionUtils; import org.apache.fory.serializer.ArraySerializers; import org.apache.fory.serializer.BufferSerializers; import org.apache.fory.serializer.CodegenSerializer.LazyInitBeanSerializer; +import org.apache.fory.serializer.CompatibleSerializer; import org.apache.fory.serializer.CopyOnlyObjectSerializer; import org.apache.fory.serializer.EnumSerializer; import org.apache.fory.serializer.ExceptionSerializers; @@ -1481,6 +1482,12 @@ public Class getSerializerClass(Class cls, boolean code if (serializerClass != null) { return serializerClass; } + if (config.registerGuavaTypes()) { + serializerClass = GuavaCollectionSerializers.getSerializerClass(cls); + if (serializerClass != null) { + return serializerClass; + } + } if (config.checkJdkClassSerializable()) { if (cls.getName().startsWith("java") && !(Serializable.class.isAssignableFrom(cls))) { throw new UnsupportedOperationException( @@ -1519,6 +1526,9 @@ public Class getSerializerClass(Class cls, boolean code if (serializerClass != null) { return serializerClass; } + if (!isCrossLanguage() && cls == IdentityHashMap.class) { + return MapSerializers.IdentityHashMapSerializer.class; + } if (Externalizable.class.isAssignableFrom(cls) || requireJavaSerialization(cls) || useReplaceResolveSerializer(cls)) { @@ -1583,6 +1593,9 @@ public Class getObjectSerializerClass( return serializerClass; } } + if (ReflectionUtils.isJdkProxy(cls)) { + return JdkProxySerializer.class; + } Class staticSerializerClass = getStaticGeneratedStructSerializerClass(cls); if (staticSerializerClass != null && shouldPreferStaticGeneratedSerializer(cls)) { @@ -1864,7 +1877,21 @@ private void registerGraalvmSerializerClass(Class cls) { RecordUtils.getRecordConstructor(cls); RecordUtils.getRecordComponents(cls); } - ObjectCreators.getObjectCreator(cls); + if (needsGraalvmObjectInstantiator(cls, serializerClass)) { + getObjectInstantiator(cls); + } + } + + private boolean needsGraalvmObjectInstantiator( + Class cls, Class serializerClass) { + if (cls.isArray()) { + return false; + } + return serializerClass == ObjectSerializer.class + || serializerClass == CompatibleSerializer.class + || serializerClass == ReplaceResolveSerializer.class + || serializerClass == CollectionSerializers.DefaultJavaCollectionSerializer.class + || serializerClass == MapSerializers.DefaultJavaMapSerializer.class; } private void createSerializer0(Class cls) { diff --git a/java/fory-core/src/main/java/org/apache/fory/resolver/SharedRegistry.java b/java/fory-core/src/main/java/org/apache/fory/resolver/SharedRegistry.java index 2eb57fd705..a3f573afdd 100644 --- a/java/fory-core/src/main/java/org/apache/fory/resolver/SharedRegistry.java +++ b/java/fory-core/src/main/java/org/apache/fory/resolver/SharedRegistry.java @@ -39,6 +39,8 @@ import org.apache.fory.meta.MetaStringEncoder; import org.apache.fory.meta.TypeDef; import org.apache.fory.platform.GraalvmSupport; +import org.apache.fory.reflect.ObjectInstantiator; +import org.apache.fory.reflect.ObjectInstantiators; import org.apache.fory.serializer.Serializer; import org.apache.fory.type.Descriptor; import org.apache.fory.type.DescriptorGrouper; @@ -81,6 +83,10 @@ public final class SharedRegistry { new ConcurrentIdentityMap<>(); final ConcurrentIdentityMap, Serializer> registeredSerializerCache = new ConcurrentIdentityMap<>(); + private final ConcurrentHashMap, ObjectInstantiator> objectInstantiatorCache = + new ConcurrentHashMap<>(); + private final ConcurrentHashMap, ObjectInstantiator> objectStreamInstantiatorCache = + new ConcurrentHashMap<>(); final StaticGeneratedSerializerRegistry staticGeneratedSerializerRegistry = new StaticGeneratedSerializerRegistry(); private final Object metaStringCacheLock = new Object(); @@ -125,6 +131,21 @@ Serializer cacheRegisteredSerializer(Class type, Serializer serializer) return existing; } + @SuppressWarnings("unchecked") + public ObjectInstantiator getObjectInstantiator(Class type) { + return (ObjectInstantiator) + objectInstantiatorCache.computeIfAbsent( + type, ObjectInstantiators::createObjectInstantiator); + } + + /** Returns the runtime-scoped instantiator used by Java ObjectStream-compatible serializers. */ + @SuppressWarnings("unchecked") + public ObjectInstantiator getObjectStreamInstantiator(Class type) { + return (ObjectInstantiator) + objectStreamInstantiatorCache.computeIfAbsent( + type, ObjectInstantiators::createObjectStreamInstantiator); + } + TypeInfo cacheRegisteredTypeInfo(Class type, TypeInfo typeInfo) { TypeInfo existing = registeredTypeInfoCache.putIfAbsent(type, typeInfo); return existing == null ? typeInfo : existing; diff --git a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java index d60a72ce3c..a1a4377a27 100644 --- a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java +++ b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java @@ -77,6 +77,7 @@ import org.apache.fory.meta.TypeExtMeta; import org.apache.fory.platform.AndroidSupport; import org.apache.fory.platform.GraalvmSupport; +import org.apache.fory.reflect.ObjectInstantiator; import org.apache.fory.reflect.ReflectionUtils; import org.apache.fory.reflect.TypeRef; import org.apache.fory.serializer.CodegenSerializer; @@ -341,6 +342,18 @@ public abstract void registerUnion( public abstract void registerEnum( Class type, String namespace, String typeName, Serializer serializer); + /** + * Returns the runtime-scoped object instantiator for {@code type}. + * + *

The instantiator follows the normal Java object model used by Fory serializers. Records use + * their canonical constructor, while ordinary classes use supported empty construction followed + * by field restoration. + */ + @Internal + public final ObjectInstantiator getObjectInstantiator(Class type) { + return sharedRegistry.getObjectInstantiator(type); + } + /** * Registers a custom serializer for a type. * diff --git a/java/fory-core/src/main/java/org/apache/fory/resolver/XtypeResolver.java b/java/fory-core/src/main/java/org/apache/fory/resolver/XtypeResolver.java index 9972145d3c..d17bbc1078 100644 --- a/java/fory-core/src/main/java/org/apache/fory/resolver/XtypeResolver.java +++ b/java/fory-core/src/main/java/org/apache/fory/resolver/XtypeResolver.java @@ -41,9 +41,17 @@ import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -103,11 +111,20 @@ import org.apache.fory.serializer.collection.CollectionLikeSerializer; import org.apache.fory.serializer.collection.CollectionSerializer; import org.apache.fory.serializer.collection.CollectionSerializers.ArrayListSerializer; +import org.apache.fory.serializer.collection.CollectionSerializers.ConcurrentSkipListSetSerializer; +import org.apache.fory.serializer.collection.CollectionSerializers.CopyOnWriteArrayListSerializer; +import org.apache.fory.serializer.collection.CollectionSerializers.CopyOnWriteArraySetSerializer; import org.apache.fory.serializer.collection.CollectionSerializers.HashSetSerializer; +import org.apache.fory.serializer.collection.CollectionSerializers.LinkedHashSetSerializer; +import org.apache.fory.serializer.collection.CollectionSerializers.SortedSetSerializer; import org.apache.fory.serializer.collection.CollectionSerializers.XlangListDefaultSerializer; import org.apache.fory.serializer.collection.CollectionSerializers.XlangSetDefaultSerializer; import org.apache.fory.serializer.collection.MapLikeSerializer; import org.apache.fory.serializer.collection.MapSerializer; +import org.apache.fory.serializer.collection.MapSerializers.ConcurrentHashMapSerializer; +import org.apache.fory.serializer.collection.MapSerializers.ConcurrentSkipListMapSerializer; +import org.apache.fory.serializer.collection.MapSerializers.LinkedHashMapSerializer; +import org.apache.fory.serializer.collection.MapSerializers.SortedMapSerializer; import org.apache.fory.serializer.collection.MapSerializers.XlangMapSerializer; import org.apache.fory.type.BFloat16; import org.apache.fory.type.BFloat16Array; @@ -1100,6 +1117,11 @@ private void registerDefaultTypes() { new PrimitiveArraySerializers.DoubleArraySerializer(this)); // Collections registerType(Types.LIST, ArrayList.class, new ArrayListSerializer(this)); + registerType(Types.LIST, LinkedList.class, new CollectionSerializer(this, LinkedList.class)); + registerType( + Types.LIST, + CopyOnWriteArrayList.class, + new CopyOnWriteArrayListSerializer(this, CopyOnWriteArrayList.class)); registerType( Types.LIST, Object[].class, @@ -1127,22 +1149,30 @@ private void registerDefaultTypes() { // Sets registerType(Types.SET, HashSet.class, new HashSetSerializer(this)); + registerType(Types.SET, LinkedHashSet.class, new LinkedHashSetSerializer(this)); + registerType(Types.SET, TreeSet.class, new SortedSetSerializer<>(this, TreeSet.class)); + registerType( + Types.SET, + ConcurrentSkipListSet.class, + new ConcurrentSkipListSetSerializer(this, ConcurrentSkipListSet.class)); registerType( Types.SET, - LinkedHashSet.class, - new org.apache.fory.serializer.collection.CollectionSerializers.LinkedHashSetSerializer( - this)); + CopyOnWriteArraySet.class, + new CopyOnWriteArraySetSerializer(this, CopyOnWriteArraySet.class)); registerType(Types.SET, Set.class, new XlangSetDefaultSerializer(this, Set.class)); // Maps + registerType(Types.MAP, HashMap.class, new HashMapSerializer(this)); + registerType(Types.MAP, LinkedHashMap.class, new LinkedHashMapSerializer(this)); + registerType(Types.MAP, TreeMap.class, new SortedMapSerializer<>(this, TreeMap.class)); registerType( Types.MAP, - HashMap.class, - new org.apache.fory.serializer.collection.MapSerializers.HashMapSerializer(this)); + ConcurrentHashMap.class, + new ConcurrentHashMapSerializer(this, ConcurrentHashMap.class)); registerType( Types.MAP, - LinkedHashMap.class, - new org.apache.fory.serializer.collection.MapSerializers.LinkedHashMapSerializer(this)); + ConcurrentSkipListMap.class, + new ConcurrentSkipListMapSerializer(this, ConcurrentSkipListMap.class)); registerType(Types.MAP, Map.class, new XlangMapSerializer(this, Map.class)); registerUnionTypes(); diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/AbstractObjectSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/AbstractObjectSerializer.java index 4ac9e2ab5f..5312540d41 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/AbstractObjectSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/AbstractObjectSerializer.java @@ -19,26 +19,31 @@ package org.apache.fory.serializer; +import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; +import java.util.IdentityHashMap; import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import org.apache.fory.Fory; +import org.apache.fory.collection.IntArray; import org.apache.fory.config.Config; import org.apache.fory.context.CopyContext; import org.apache.fory.context.ReadContext; import org.apache.fory.context.RefReader; import org.apache.fory.context.RefWriter; import org.apache.fory.context.WriteContext; +import org.apache.fory.exception.ForyException; 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.FieldAccessor; -import org.apache.fory.reflect.ObjectCreator; -import org.apache.fory.reflect.ObjectCreators; +import org.apache.fory.reflect.ObjectInstantiator; import org.apache.fory.reflect.ReflectionUtils; import org.apache.fory.reflect.TypeRef; import org.apache.fory.resolver.RefMode; @@ -63,10 +68,17 @@ public abstract class AbstractObjectSerializer extends Serializer { private static final Logger LOG = LoggerFactory.getLogger(AbstractObjectSerializer.class); + private static final Object SELF_REFERENCE = new Object(); + // Constructor-bound objects reserve a ref id before constructor arguments are read, but the + // object cannot be referenced semantically until the constructor returns. Generated constructor + // serializers call the tracker before reading ref-tracking constructor-phase fields so nested + // collection/map/array elements cannot hide unresolved self-references. + private static final Object CONSTRUCTOR_REF_IDS = new Object(); + private static final Object UNRESOLVED_CONSTRUCTOR_REF_IDS = new Object(); protected final Config config; protected final TypeResolver typeResolver; protected final boolean isRecord; - protected final ObjectCreator objectCreator; + protected final ObjectInstantiator objectInstantiator; private SerializationFieldInfo[] fieldInfos; private RecordInfo copyRecordInfo; @@ -75,20 +87,20 @@ protected AbstractObjectSerializer() { this.config = null; this.typeResolver = null; this.isRecord = false; - this.objectCreator = null; + this.objectInstantiator = null; } public AbstractObjectSerializer(TypeResolver typeResolver, Class type) { - this(typeResolver, type, ObjectCreators.getObjectCreator(type)); + this(typeResolver, type, typeResolver.getObjectInstantiator(type)); } public AbstractObjectSerializer( - TypeResolver typeResolver, Class type, ObjectCreator objectCreator) { + TypeResolver typeResolver, Class type, ObjectInstantiator objectInstantiator) { super(typeResolver.getConfig(), type); this.config = typeResolver.getConfig(); this.typeResolver = typeResolver; this.isRecord = RecordUtils.isRecord(type); - this.objectCreator = objectCreator; + this.objectInstantiator = objectInstantiator; } static void writeField( @@ -169,7 +181,7 @@ static Object readField( return null; } if (refMode == RefMode.TRACKING) { - int nextReadRefId = refReader.tryPreserveRefId(buffer); + int nextReadRefId = readContext.tryPreserveRefId(); if (nextReadRefId >= Fory.NOT_NULL_VALUE_FLAG) { Object value = typeResolver @@ -243,75 +255,6 @@ static void writeBuildInFieldValue( } } - /** - * Write a primitive field value to buffer using direct memory offset access. - * - * @param buffer the buffer to write to - * @param targetObject the object containing the field - * @param fieldOffset the memory offset of the field - * @param dispatchId the class ID of the primitive type - * @return true if dispatchId is not a primitive type and needs further write handling - */ - private static boolean writePrimitiveFieldValue( - MemoryBuffer buffer, Object targetObject, long fieldOffset, int dispatchId) { - switch (dispatchId) { - case DispatchId.BOOL: - buffer.writeBoolean(UnsafeOps.getBoolean(targetObject, fieldOffset)); - return false; - case DispatchId.INT8: - buffer.writeByte(UnsafeOps.getByte(targetObject, fieldOffset)); - return false; - case DispatchId.UINT8: - buffer.writeByte(UnsafeOps.getInt(targetObject, fieldOffset)); - return false; - case DispatchId.CHAR: - buffer.writeChar(UnsafeOps.getChar(targetObject, fieldOffset)); - return false; - case DispatchId.INT16: - buffer.writeInt16(UnsafeOps.getShort(targetObject, fieldOffset)); - return false; - case DispatchId.UINT16: - buffer.writeInt16((short) UnsafeOps.getInt(targetObject, fieldOffset)); - return false; - case DispatchId.INT32: - buffer.writeInt32(UnsafeOps.getInt(targetObject, fieldOffset)); - return false; - case DispatchId.UINT32: - buffer.writeInt32((int) UnsafeOps.getLong(targetObject, fieldOffset)); - return false; - case DispatchId.VARINT32: - buffer.writeVarInt32(UnsafeOps.getInt(targetObject, fieldOffset)); - return false; - case DispatchId.VAR_UINT32: - buffer.writeVarUInt32((int) UnsafeOps.getLong(targetObject, fieldOffset)); - return false; - case DispatchId.FLOAT32: - buffer.writeFloat32(UnsafeOps.getFloat(targetObject, fieldOffset)); - return false; - case DispatchId.INT64: - case DispatchId.UINT64: - buffer.writeInt64(UnsafeOps.getLong(targetObject, fieldOffset)); - return false; - case DispatchId.VARINT64: - buffer.writeVarInt64(UnsafeOps.getLong(targetObject, fieldOffset)); - return false; - case DispatchId.TAGGED_INT64: - buffer.writeTaggedInt64(UnsafeOps.getLong(targetObject, fieldOffset)); - return false; - case DispatchId.VAR_UINT64: - buffer.writeVarUInt64(UnsafeOps.getLong(targetObject, fieldOffset)); - return false; - case DispatchId.TAGGED_UINT64: - buffer.writeTaggedUInt64(UnsafeOps.getLong(targetObject, fieldOffset)); - return false; - case DispatchId.FLOAT64: - buffer.writeFloat64(UnsafeOps.getDouble(targetObject, fieldOffset)); - return false; - default: - return true; - } - } - /** * Write a primitive field value to buffer using the field accessor. * @@ -323,65 +266,58 @@ private static boolean writePrimitiveFieldValue( */ static boolean writePrimitiveFieldValue( MemoryBuffer buffer, Object targetObject, FieldAccessor fieldAccessor, int dispatchId) { - long fieldOffset = fieldAccessor.getFieldOffset(); - if (fieldOffset != -1) { - return writePrimitiveFieldValue(buffer, targetObject, fieldOffset, dispatchId); - } - // graalvm use GeneratedAccessor, which will be this code path. switch (dispatchId) { case DispatchId.BOOL: - buffer.writeBoolean((Boolean) fieldAccessor.get(targetObject)); + buffer.writeBoolean(fieldAccessor.getBoolean(targetObject)); return false; case DispatchId.INT8: - buffer.writeByte((Byte) fieldAccessor.get(targetObject)); + buffer.writeByte(fieldAccessor.getByte(targetObject)); return false; case DispatchId.UINT8: - buffer.writeByte((Integer) fieldAccessor.get(targetObject)); + buffer.writeByte(fieldAccessor.getInt(targetObject)); return false; case DispatchId.CHAR: - buffer.writeChar((Character) fieldAccessor.get(targetObject)); + buffer.writeChar(fieldAccessor.getChar(targetObject)); return false; case DispatchId.INT16: - buffer.writeInt16((Short) fieldAccessor.get(targetObject)); + buffer.writeInt16(fieldAccessor.getShort(targetObject)); return false; case DispatchId.UINT16: - buffer.writeInt16(((Integer) fieldAccessor.get(targetObject)).shortValue()); + buffer.writeInt16((short) fieldAccessor.getInt(targetObject)); return false; case DispatchId.INT32: - buffer.writeInt32((Integer) fieldAccessor.get(targetObject)); + buffer.writeInt32(fieldAccessor.getInt(targetObject)); return false; case DispatchId.UINT32: - buffer.writeInt32(((Long) fieldAccessor.get(targetObject)).intValue()); + buffer.writeInt32((int) fieldAccessor.getLong(targetObject)); return false; case DispatchId.VARINT32: - buffer.writeVarInt32((Integer) fieldAccessor.get(targetObject)); + buffer.writeVarInt32(fieldAccessor.getInt(targetObject)); return false; case DispatchId.VAR_UINT32: - buffer.writeVarUInt32(((Long) fieldAccessor.get(targetObject)).intValue()); + buffer.writeVarUInt32((int) fieldAccessor.getLong(targetObject)); return false; case DispatchId.FLOAT32: - buffer.writeFloat32((Float) fieldAccessor.get(targetObject)); + buffer.writeFloat32(fieldAccessor.getFloat(targetObject)); return false; case DispatchId.INT64: - buffer.writeInt64((Long) fieldAccessor.get(targetObject)); - return false; case DispatchId.UINT64: - buffer.writeInt64((Long) fieldAccessor.get(targetObject)); + buffer.writeInt64(fieldAccessor.getLong(targetObject)); return false; case DispatchId.VARINT64: - buffer.writeVarInt64((Long) fieldAccessor.get(targetObject)); + buffer.writeVarInt64(fieldAccessor.getLong(targetObject)); return false; case DispatchId.TAGGED_INT64: - buffer.writeTaggedInt64((Long) fieldAccessor.get(targetObject)); + buffer.writeTaggedInt64(fieldAccessor.getLong(targetObject)); return false; case DispatchId.VAR_UINT64: - buffer.writeVarUInt64((Long) fieldAccessor.get(targetObject)); + buffer.writeVarUInt64(fieldAccessor.getLong(targetObject)); return false; case DispatchId.TAGGED_UINT64: - buffer.writeTaggedUInt64((Long) fieldAccessor.get(targetObject)); + buffer.writeTaggedUInt64(fieldAccessor.getLong(targetObject)); return false; case DispatchId.FLOAT64: - buffer.writeFloat64((Double) fieldAccessor.get(targetObject)); + buffer.writeFloat64(fieldAccessor.getDouble(targetObject)); return false; default: return true; @@ -570,8 +506,7 @@ static Object readContainerFieldValue( break; case TRACKING: generics.pushGenericType(fieldInfo.genericType, readContext.getDepth()); - fieldValue = - readContainerFieldValueRef(readContext, typeResolver, refReader, fieldInfo, buffer); + fieldValue = readContainerFieldValueRef(readContext, typeResolver, refReader, fieldInfo); generics.popGenericType(readContext.getDepth()); break; default: @@ -595,9 +530,8 @@ private static Object readContainerFieldValueRef( ReadContext readContext, TypeResolver typeResolver, RefReader refReader, - SerializationFieldInfo fieldInfo, - MemoryBuffer buffer) { - int nextReadRefId = refReader.tryPreserveRefId(buffer); + SerializationFieldInfo fieldInfo) { + int nextReadRefId = readContext.tryPreserveRefId(); if (nextReadRefId >= Fory.NOT_NULL_VALUE_FLAG) { Object value; if (fieldInfo.containerSerializerOverride != null) { @@ -770,134 +704,59 @@ private static Object readNotNullBuildInFieldValue( */ private static void readPrimitiveFieldValue( MemoryBuffer buffer, Object targetObject, FieldAccessor fieldAccessor, int dispatchId) { - long fieldOffset = fieldAccessor.getFieldOffset(); - if (fieldOffset != -1) { - readPrimitiveFieldValue(buffer, targetObject, fieldOffset, dispatchId); - return; - } - // graalvm use GeneratedAccessor, which will be this code path. // we still need `PRIMITIVE` cases since peer may send switch (dispatchId) { case DispatchId.BOOL: - fieldAccessor.set(targetObject, buffer.readBoolean()); + fieldAccessor.putBoolean(targetObject, buffer.readBoolean()); return; case DispatchId.INT8: - fieldAccessor.set(targetObject, buffer.readByte()); + fieldAccessor.putByte(targetObject, buffer.readByte()); return; case DispatchId.UINT8: - fieldAccessor.set(targetObject, buffer.readByte() & 0xFF); + fieldAccessor.putInt(targetObject, buffer.readByte() & 0xFF); return; case DispatchId.CHAR: - fieldAccessor.set(targetObject, buffer.readChar()); + fieldAccessor.putChar(targetObject, buffer.readChar()); return; case DispatchId.INT16: - fieldAccessor.set(targetObject, buffer.readInt16()); + fieldAccessor.putShort(targetObject, buffer.readInt16()); return; case DispatchId.UINT16: - fieldAccessor.set(targetObject, buffer.readInt16() & 0xFFFF); + fieldAccessor.putInt(targetObject, buffer.readInt16() & 0xFFFF); return; case DispatchId.INT32: - fieldAccessor.set(targetObject, buffer.readInt32()); + fieldAccessor.putInt(targetObject, buffer.readInt32()); return; case DispatchId.UINT32: - fieldAccessor.set(targetObject, Integer.toUnsignedLong(buffer.readInt32())); + fieldAccessor.putLong(targetObject, Integer.toUnsignedLong(buffer.readInt32())); return; case DispatchId.VARINT32: - fieldAccessor.set(targetObject, buffer.readVarInt32()); + fieldAccessor.putInt(targetObject, buffer.readVarInt32()); return; case DispatchId.VAR_UINT32: - fieldAccessor.set(targetObject, Integer.toUnsignedLong(buffer.readVarUInt32())); + fieldAccessor.putLong(targetObject, Integer.toUnsignedLong(buffer.readVarUInt32())); return; case DispatchId.FLOAT32: - fieldAccessor.set(targetObject, buffer.readFloat32()); + fieldAccessor.putFloat(targetObject, buffer.readFloat32()); return; case DispatchId.INT64: case DispatchId.UINT64: - fieldAccessor.set(targetObject, buffer.readInt64()); + fieldAccessor.putLong(targetObject, buffer.readInt64()); return; case DispatchId.VARINT64: - fieldAccessor.set(targetObject, buffer.readVarInt64()); + fieldAccessor.putLong(targetObject, buffer.readVarInt64()); return; case DispatchId.TAGGED_INT64: - fieldAccessor.set(targetObject, buffer.readTaggedInt64()); + fieldAccessor.putLong(targetObject, buffer.readTaggedInt64()); return; case DispatchId.VAR_UINT64: - fieldAccessor.set(targetObject, buffer.readVarUInt64()); + fieldAccessor.putLong(targetObject, buffer.readVarUInt64()); return; case DispatchId.TAGGED_UINT64: - fieldAccessor.set(targetObject, buffer.readTaggedUInt64()); + fieldAccessor.putLong(targetObject, buffer.readTaggedUInt64()); return; case DispatchId.FLOAT64: - fieldAccessor.set(targetObject, buffer.readFloat64()); - return; - default: - throw new IllegalArgumentException("Unsupported dispatch id " + dispatchId); - } - } - - /** - * Read a primitive field value from buffer and set it using direct memory offset access. - * - * @param buffer the buffer to read from - * @param targetObject the object to set the field value on - * @param fieldOffset the memory offset of the field - * @param dispatchId the dispatch ID of the primitive type - */ - private static void readPrimitiveFieldValue( - MemoryBuffer buffer, Object targetObject, long fieldOffset, int dispatchId) { - switch (dispatchId) { - case DispatchId.BOOL: - UnsafeOps.putBoolean(targetObject, fieldOffset, buffer.readBoolean()); - return; - case DispatchId.INT8: - UnsafeOps.putByte(targetObject, fieldOffset, buffer.readByte()); - return; - case DispatchId.UINT8: - UnsafeOps.putInt(targetObject, fieldOffset, buffer.readByte() & 0xFF); - return; - case DispatchId.CHAR: - UnsafeOps.putChar(targetObject, fieldOffset, buffer.readChar()); - return; - case DispatchId.INT16: - UnsafeOps.putShort(targetObject, fieldOffset, buffer.readInt16()); - return; - case DispatchId.UINT16: - UnsafeOps.putInt(targetObject, fieldOffset, buffer.readInt16() & 0xFFFF); - return; - case DispatchId.INT32: - UnsafeOps.putInt(targetObject, fieldOffset, buffer.readInt32()); - return; - case DispatchId.UINT32: - UnsafeOps.putLong(targetObject, fieldOffset, Integer.toUnsignedLong(buffer.readInt32())); - return; - case DispatchId.VARINT32: - UnsafeOps.putInt(targetObject, fieldOffset, buffer.readVarInt32()); - return; - case DispatchId.VAR_UINT32: - UnsafeOps.putLong( - targetObject, fieldOffset, Integer.toUnsignedLong(buffer.readVarUInt32())); - return; - case DispatchId.FLOAT32: - UnsafeOps.putFloat(targetObject, fieldOffset, buffer.readFloat32()); - return; - case DispatchId.INT64: - case DispatchId.UINT64: - UnsafeOps.putLong(targetObject, fieldOffset, buffer.readInt64()); - return; - case DispatchId.VARINT64: - UnsafeOps.putLong(targetObject, fieldOffset, buffer.readVarInt64()); - return; - case DispatchId.TAGGED_INT64: - UnsafeOps.putLong(targetObject, fieldOffset, buffer.readTaggedInt64()); - return; - case DispatchId.VAR_UINT64: - UnsafeOps.putLong(targetObject, fieldOffset, buffer.readVarUInt64()); - return; - case DispatchId.TAGGED_UINT64: - UnsafeOps.putLong(targetObject, fieldOffset, buffer.readTaggedUInt64()); - return; - case DispatchId.FLOAT64: - UnsafeOps.putDouble(targetObject, fieldOffset, buffer.readFloat64()); + fieldAccessor.putDouble(targetObject, buffer.readFloat64()); return; default: throw new IllegalArgumentException("Unsupported dispatch id " + dispatchId); @@ -1022,9 +881,10 @@ public T copy(CopyContext copyContext, T originObj) { } private T copyRecord(CopyContext copyContext, T originObj) { - Object[] fieldValues = copyFields(copyContext, originObj); + Object[] fieldValues = copyFieldValues(copyContext, originObj); + fieldValues = RecordUtils.remapping(copyRecordInfo, fieldValues); try { - T t = objectCreator.newInstanceWithArguments(fieldValues); + T t = objectInstantiator.newInstanceWithArguments(fieldValues); Arrays.fill(copyRecordInfo.getRecordComponents(), null); copyContext.reference(originObj, t); return t; @@ -1034,7 +894,7 @@ private T copyRecord(CopyContext copyContext, T originObj) { return originObj; } - private Object[] copyFields(CopyContext copyContext, T originObj) { + private Object[] copyFieldValues(CopyContext copyContext, T originObj) { SerializationFieldInfo[] fieldInfos = this.fieldInfos; if (fieldInfos == null) { fieldInfos = buildFieldsInfo(); @@ -1043,21 +903,14 @@ private Object[] copyFields(CopyContext copyContext, T originObj) { for (int i = 0; i < fieldInfos.length; i++) { SerializationFieldInfo fieldInfo = fieldInfos[i]; FieldAccessor fieldAccessor = fieldInfo.fieldAccessor; - long fieldOffset = fieldAccessor.getFieldOffset(); - if (fieldOffset != -1) { - if (fieldInfo.isPrimitiveField) { - fieldValues[i] = copyPrimitiveField(originObj, fieldOffset, fieldInfo.dispatchId); - } else { - fieldValues[i] = - copyNotPrimitiveField(copyContext, originObj, fieldOffset, fieldInfo.dispatchId); - } + if (fieldInfo.isPrimitiveField) { + fieldValues[i] = copyPrimitiveField(originObj, fieldAccessor, fieldInfo.dispatchId); } else { - // field in record class has offset -1 - Object fieldValue = fieldAccessor.get(originObj); - fieldValues[i] = copyContext.copyObject(fieldValue, fieldInfo.dispatchId); + fieldValues[i] = + copyNotPrimitiveField(copyContext, originObj, fieldAccessor, fieldInfo.dispatchId); } } - return RecordUtils.remapping(copyRecordInfo, fieldValues); + return fieldValues; } private void copyFields(CopyContext copyContext, T originObj, T newObj) { @@ -1075,17 +928,11 @@ public static void copyFields( Object newObj) { for (SerializationFieldInfo fieldInfo : fieldInfos) { FieldAccessor fieldAccessor = fieldInfo.fieldAccessor; - long fieldOffset = fieldAccessor.getFieldOffset(); - if (fieldOffset == -1) { - Object fieldValue = fieldAccessor.getObject(originObj); - fieldAccessor.putObject( - newObj, copyFieldValue(copyContext, fieldValue, fieldInfo.dispatchId)); - continue; - } if (fieldInfo.isPrimitiveField) { - copySetPrimitiveField(originObj, newObj, fieldOffset, fieldInfo.dispatchId); + copySetPrimitiveField(originObj, newObj, fieldAccessor, fieldInfo.dispatchId); } else { - copySetNotPrimitiveField(copyContext, originObj, newObj, fieldOffset, fieldInfo.dispatchId); + copySetNotPrimitiveField( + copyContext, originObj, newObj, fieldAccessor, fieldInfo.dispatchId); } } } @@ -1094,6 +941,10 @@ private static Object copyFieldValue(CopyContext copyContext, Object fieldValue, if (fieldValue == null) { return null; } + return isCopyByReference(dispatchId) ? fieldValue : copyContext.copyObject(fieldValue); + } + + private static boolean isCopyByReference(int dispatchId) { switch (dispatchId) { case DispatchId.BOOL: case DispatchId.INT8: @@ -1122,164 +973,70 @@ private static Object copyFieldValue(CopyContext copyContext, Object fieldValue, case DispatchId.FLOAT16: case DispatchId.BFLOAT16: case DispatchId.STRING: - return fieldValue; + return true; default: - return copyContext.copyObject(fieldValue); + return false; } } private static void copySetPrimitiveField( - Object originObj, Object newObj, long fieldOffset, int typeId) { - switch (typeId) { - case DispatchId.BOOL: - UnsafeOps.putBoolean(newObj, fieldOffset, UnsafeOps.getBoolean(originObj, fieldOffset)); - break; - case DispatchId.INT8: - UnsafeOps.putByte(newObj, fieldOffset, UnsafeOps.getByte(originObj, fieldOffset)); - break; - case DispatchId.UINT8: - UnsafeOps.putInt(newObj, fieldOffset, UnsafeOps.getInt(originObj, fieldOffset)); - break; - case DispatchId.CHAR: - UnsafeOps.putChar(newObj, fieldOffset, UnsafeOps.getChar(originObj, fieldOffset)); - break; - case DispatchId.INT16: - UnsafeOps.putShort(newObj, fieldOffset, UnsafeOps.getShort(originObj, fieldOffset)); - break; - case DispatchId.UINT16: - UnsafeOps.putInt(newObj, fieldOffset, UnsafeOps.getInt(originObj, fieldOffset)); - break; - case DispatchId.INT32: - case DispatchId.VARINT32: - UnsafeOps.putInt(newObj, fieldOffset, UnsafeOps.getInt(originObj, fieldOffset)); - break; - case DispatchId.UINT32: - case DispatchId.VAR_UINT32: - UnsafeOps.putLong(newObj, fieldOffset, UnsafeOps.getLong(originObj, fieldOffset)); - break; - case DispatchId.INT64: - case DispatchId.VARINT64: - case DispatchId.TAGGED_INT64: - case DispatchId.UINT64: - case DispatchId.VAR_UINT64: - case DispatchId.TAGGED_UINT64: - UnsafeOps.putLong(newObj, fieldOffset, UnsafeOps.getLong(originObj, fieldOffset)); - break; - case DispatchId.FLOAT32: - UnsafeOps.putFloat(newObj, fieldOffset, UnsafeOps.getFloat(originObj, fieldOffset)); - break; - case DispatchId.FLOAT64: - UnsafeOps.putDouble(newObj, fieldOffset, UnsafeOps.getDouble(originObj, fieldOffset)); - break; - default: - throw new RuntimeException("Unknown primitive type: " + typeId); - } + Object originObj, Object newObj, FieldAccessor fieldAccessor, int typeId) { + fieldAccessor.copy(originObj, newObj); } private static void copySetNotPrimitiveField( - CopyContext copyContext, Object originObj, Object newObj, long fieldOffset, int typeId) { - switch (typeId) { - case DispatchId.BOOL: - case DispatchId.INT8: - case DispatchId.UINT8: - case DispatchId.EXT_UINT8: - case DispatchId.CHAR: - case DispatchId.INT16: - case DispatchId.UINT16: - case DispatchId.EXT_UINT16: - case DispatchId.INT32: - case DispatchId.VARINT32: - case DispatchId.UINT32: - case DispatchId.EXT_UINT32: - case DispatchId.VAR_UINT32: - case DispatchId.EXT_VAR_UINT32: - case DispatchId.INT64: - case DispatchId.VARINT64: - case DispatchId.TAGGED_INT64: - case DispatchId.UINT64: - case DispatchId.EXT_UINT64: - case DispatchId.VAR_UINT64: - case DispatchId.EXT_VAR_UINT64: - case DispatchId.TAGGED_UINT64: - case DispatchId.FLOAT32: - case DispatchId.FLOAT64: - case DispatchId.FLOAT16: - case DispatchId.BFLOAT16: - case DispatchId.STRING: - UnsafeOps.putObject(newObj, fieldOffset, UnsafeOps.getObject(originObj, fieldOffset)); - break; - default: - UnsafeOps.putObject( - newObj, - fieldOffset, - copyContext.copyObject(UnsafeOps.getObject(originObj, fieldOffset))); + CopyContext copyContext, + Object originObj, + Object newObj, + FieldAccessor fieldAccessor, + int typeId) { + if (isCopyByReference(typeId)) { + fieldAccessor.copyObject(originObj, newObj); + return; } + Object fieldValue = fieldAccessor.getObject(originObj); + fieldAccessor.putObject(newObj, fieldValue == null ? null : copyContext.copyObject(fieldValue)); } - private Object copyPrimitiveField(Object targetObject, long fieldOffset, int typeId) { + private Object copyPrimitiveField(Object targetObject, FieldAccessor fieldAccessor, int typeId) { switch (typeId) { case DispatchId.BOOL: - return UnsafeOps.getBoolean(targetObject, fieldOffset); + return fieldAccessor.getBoolean(targetObject); case DispatchId.INT8: - return UnsafeOps.getByte(targetObject, fieldOffset); + return fieldAccessor.getByte(targetObject); case DispatchId.UINT8: - return UnsafeOps.getInt(targetObject, fieldOffset); + return fieldAccessor.getInt(targetObject); case DispatchId.CHAR: - return UnsafeOps.getChar(targetObject, fieldOffset); + return fieldAccessor.getChar(targetObject); case DispatchId.INT16: - return UnsafeOps.getShort(targetObject, fieldOffset); + return fieldAccessor.getShort(targetObject); case DispatchId.UINT16: - return UnsafeOps.getInt(targetObject, fieldOffset); + return fieldAccessor.getInt(targetObject); case DispatchId.INT32: case DispatchId.VARINT32: - return UnsafeOps.getInt(targetObject, fieldOffset); + return fieldAccessor.getInt(targetObject); case DispatchId.UINT32: case DispatchId.VAR_UINT32: - return UnsafeOps.getLong(targetObject, fieldOffset); + return fieldAccessor.getLong(targetObject); case DispatchId.FLOAT32: - return UnsafeOps.getFloat(targetObject, fieldOffset); + return fieldAccessor.getFloat(targetObject); case DispatchId.INT64: case DispatchId.VARINT64: case DispatchId.TAGGED_INT64: case DispatchId.UINT64: case DispatchId.VAR_UINT64: case DispatchId.TAGGED_UINT64: - return UnsafeOps.getLong(targetObject, fieldOffset); + return fieldAccessor.getLong(targetObject); case DispatchId.FLOAT64: - return UnsafeOps.getDouble(targetObject, fieldOffset); + return fieldAccessor.getDouble(targetObject); default: throw new RuntimeException("Unknown primitive type: " + typeId); } } private Object copyNotPrimitiveField( - CopyContext copyContext, Object targetObject, long fieldOffset, int typeId) { - switch (typeId) { - case DispatchId.BOOL: - case DispatchId.INT8: - case DispatchId.UINT8: - case DispatchId.CHAR: - case DispatchId.INT16: - case DispatchId.UINT16: - case DispatchId.INT32: - case DispatchId.VARINT32: - case DispatchId.UINT32: - case DispatchId.VAR_UINT32: - case DispatchId.FLOAT32: - case DispatchId.INT64: - case DispatchId.VARINT64: - case DispatchId.TAGGED_INT64: - case DispatchId.UINT64: - case DispatchId.VAR_UINT64: - case DispatchId.TAGGED_UINT64: - case DispatchId.FLOAT64: - case DispatchId.FLOAT16: - case DispatchId.BFLOAT16: - case DispatchId.STRING: - return UnsafeOps.getObject(targetObject, fieldOffset); - default: - return copyContext.copyObject(UnsafeOps.getObject(targetObject, fieldOffset)); - } + CopyContext copyContext, Object targetObject, FieldAccessor fieldAccessor, int typeId) { + return copyFieldValue(copyContext, fieldAccessor.getObject(targetObject), typeId); } private SerializationFieldInfo[] buildFieldsInfo() { @@ -1326,6 +1083,272 @@ private SerializationFieldInfo[] buildFieldsInfo() { } protected T newBean() { - return objectCreator.newInstance(); + ObjectInstantiator instantiator = objectInstantiator; + if (instantiator == null) { + throw objectCreationUnsupported(); + } + return instantiator.newInstance(); + } + + private ForyException objectCreationUnsupported() { + return new ForyException("Serializer for " + type.getName() + " does not create objects"); + } + + protected final void checkNoUnresolvedReadRef(ReadContext readContext) { + checkNoUnresolvedReadRef(readContext, type); + } + + public static void checkNoUnresolvedReadRef(ReadContext readContext, Class type) { + if (consumeSelfRef(readContext)) { + throwConstructorCycle(type); + } + } + + protected final Object beginConstructorCopy(CopyContext copyContext, Object originObj) { + if (!copyContext.copyTrackingRef()) { + return null; + } + Object pendingMarker = new ConstructorCopyPending(); + copyContext.reference(originObj, pendingMarker); + return pendingMarker; + } + + protected final void checkNoConstructorCopyBackrefs( + Object[] fieldValues, int[] constructorFieldIndexes, Object pendingMarker) { + if (pendingMarker == null) { + return; + } + IdentityHashMap seen = null; + for (int constructorFieldIndex : constructorFieldIndexes) { + if (constructorFieldIndex >= 0) { + seen = + checkNoConstructorCopyBackref( + fieldValues[constructorFieldIndex], pendingMarker, type, seen); + } + } + } + + private static IdentityHashMap checkNoConstructorCopyBackref( + Object fieldValue, + Object pendingMarker, + Class type, + IdentityHashMap seen) { + if (fieldValue == null || isConstructorBackrefLeaf(fieldValue.getClass())) { + return seen; + } + if (fieldValue == pendingMarker) { + throwConstructorCycle(type); + } + if (seen == null) { + seen = new IdentityHashMap<>(); + } else if (seen.containsKey(fieldValue)) { + return seen; + } + seen.put(fieldValue, Boolean.TRUE); + Class fieldClass = fieldValue.getClass(); + if (fieldClass.isArray()) { + if (!fieldClass.getComponentType().isPrimitive()) { + int length = Array.getLength(fieldValue); + for (int i = 0; i < length; i++) { + seen = checkNoConstructorCopyBackref(Array.get(fieldValue, i), pendingMarker, type, seen); + } + } + return seen; + } + if (fieldValue instanceof Optional) { + Optional optional = (Optional) fieldValue; + if (optional.isPresent()) { + seen = checkNoConstructorCopyBackref(optional.get(), pendingMarker, type, seen); + } + return seen; + } + if (fieldValue instanceof AtomicReference) { + return checkNoConstructorCopyBackref( + ((AtomicReference) fieldValue).get(), pendingMarker, type, seen); + } + if (fieldValue instanceof Iterable) { + for (Object element : (Iterable) fieldValue) { + seen = checkNoConstructorCopyBackref(element, pendingMarker, type, seen); + } + return seen; + } + if (fieldValue instanceof Map) { + for (Map.Entry entry : ((Map) fieldValue).entrySet()) { + seen = checkNoConstructorCopyBackref(entry.getKey(), pendingMarker, type, seen); + seen = checkNoConstructorCopyBackref(entry.getValue(), pendingMarker, type, seen); + } + return seen; + } + if (fieldValue instanceof Map.Entry) { + Map.Entry entry = (Map.Entry) fieldValue; + seen = checkNoConstructorCopyBackref(entry.getKey(), pendingMarker, type, seen); + return checkNoConstructorCopyBackref(entry.getValue(), pendingMarker, type, seen); + } + if (isJdkClass(fieldClass)) { + return seen; + } + for (Descriptor descriptor : Descriptor.getDescriptors(fieldClass)) { + if (descriptor.getRawType().isPrimitive() || descriptor.getField() == null) { + continue; + } + Object value = FieldAccessor.createAccessor(descriptor.getField()).getObject(fieldValue); + seen = checkNoConstructorCopyBackref(value, pendingMarker, type, seen); + } + return seen; + } + + private static final class ConstructorCopyPending {} + + private static boolean isConstructorBackrefLeaf(Class fieldClass) { + return fieldClass.isPrimitive() + || fieldClass.isEnum() + || fieldClass == Class.class + || fieldClass == String.class + || Number.class.isAssignableFrom(fieldClass) + || fieldClass == Boolean.class + || fieldClass == Character.class; + } + + private static boolean isJdkClass(Class fieldClass) { + String name = fieldClass.getName(); + return name.startsWith("java.") + || name.startsWith("javax.") + || name.startsWith("jdk.") + || name.startsWith("sun."); + } + + public static void beginConstructorRef(ReadContext readContext) { + if (readContext.hasPreservedRefId()) { + constructorRefIds(readContext).add(readContext.lastPreservedRefId()); + } + } + + public static void endConstructorRef(ReadContext readContext) { + IntArray refIds = (IntArray) readContext.getContextObject(CONSTRUCTOR_REF_IDS); + if (refIds != null && refIds.size > 0) { + refIds.pop(); + } + } + + public static void referenceConstructorRef(ReadContext readContext, Object object) { + int constructorRefId = currentConstructorRefId(readContext); + if (constructorRefId >= 0) { + readContext.setReadRef(constructorRefId, object); + return; + } + if (readContext.hasPreservedRefId()) { + readContext.reference(object); + } + } + + public static Object ctorFieldValue(ReadContext readContext, Object value, Class type) { + if (consumeSelfRef(readContext)) { + throwConstructorCycle(type); + } + return value; + } + + public static Object bufferFieldValue(ReadContext readContext, Object value, Class type) { + if (!consumeSelfRef(readContext)) { + return value; + } + if (value == null) { + return SELF_REFERENCE; + } + throwConstructorCycle(type); + return null; + } + + public static Object resolveBufferedValue(Object value, Object targetObject) { + return value == SELF_REFERENCE ? targetObject : value; + } + + private static boolean consumeSelfRef(ReadContext readContext) { + int constructorRefId = currentConstructorRefId(readContext); + return constructorRefId >= 0 && consumeUnresolvedConstructorRef(readContext, constructorRefId); + } + + public static void trackConstructorRefRead(ReadContext readContext, MemoryBuffer buffer) { + IntArray constructorRefIds = (IntArray) readContext.getContextObject(CONSTRUCTOR_REF_IDS); + if (constructorRefIds == null || constructorRefIds.size == 0) { + return; + } + int readerIndex = buffer.readerIndex(); + if (buffer.getByte(readerIndex) != Fory.REF_FLAG) { + return; + } + int refId; + try { + buffer.readerIndex(readerIndex + 1); + refId = buffer.readVarUInt32Small14(); + } finally { + buffer.readerIndex(readerIndex); + } + if (readContext.getReadRef(refId) == null && containsRefId(constructorRefIds, refId)) { + unresolvedConstructorRefIds(readContext).add(refId); + } + } + + private static IntArray constructorRefIds(ReadContext readContext) { + IntArray refIds = (IntArray) readContext.getContextObject(CONSTRUCTOR_REF_IDS); + if (refIds == null) { + refIds = new IntArray(4); + readContext.putContextObject(CONSTRUCTOR_REF_IDS, refIds); + } + return refIds; + } + + private static IntArray unresolvedConstructorRefIds(ReadContext readContext) { + IntArray refIds = (IntArray) readContext.getContextObject(UNRESOLVED_CONSTRUCTOR_REF_IDS); + if (refIds == null) { + refIds = new IntArray(4); + readContext.putContextObject(UNRESOLVED_CONSTRUCTOR_REF_IDS, refIds); + } + return refIds; + } + + private static int currentConstructorRefId(ReadContext readContext) { + IntArray refIds = (IntArray) readContext.getContextObject(CONSTRUCTOR_REF_IDS); + if (refIds == null || refIds.size == 0) { + return -1; + } + return refIds.get(refIds.size - 1); + } + + private static boolean containsRefId(IntArray refIds, int refId) { + for (int i = refIds.size - 1; i >= 0; i--) { + if (refIds.get(i) == refId) { + return true; + } + } + return false; + } + + private static boolean consumeUnresolvedConstructorRef(ReadContext readContext, int refId) { + IntArray unresolvedRefIds = + (IntArray) readContext.getContextObject(UNRESOLVED_CONSTRUCTOR_REF_IDS); + if (unresolvedRefIds == null || unresolvedRefIds.size == 0) { + return false; + } + boolean found = false; + int newSize = 0; + for (int i = 0; i < unresolvedRefIds.size; i++) { + int unresolvedRefId = unresolvedRefIds.get(i); + if (unresolvedRefId == refId) { + found = true; + } else { + unresolvedRefIds.elementData[newSize++] = unresolvedRefId; + } + } + unresolvedRefIds.size = newSize; + return found; + } + + protected static void throwConstructorCycle(Class type) { + throw new ForyException( + "Cyclic references to constructor-created type " + + type.getName() + + " cannot be restored before the object is constructed. Use a no-arg constructor " + + "or keep the cycle outside constructor parameters."); } } diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/CodegenSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/CodegenSerializer.java index 57ebcb10ad..4bc3b1a427 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/CodegenSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/CodegenSerializer.java @@ -25,15 +25,23 @@ import org.apache.fory.Fory; import org.apache.fory.builder.CodecUtils; import org.apache.fory.builder.Generated; +import org.apache.fory.codegen.CodeGenerator; import org.apache.fory.context.ReadContext; import org.apache.fory.context.WriteContext; import org.apache.fory.platform.AndroidSupport; +import org.apache.fory.platform.internal._JDKAccess; import org.apache.fory.resolver.TypeResolver; /** Util for JIT Serialization. */ public final class CodegenSerializer { public static boolean supportCodegenForJavaSerialization(Class cls) { + if (cls.getClassLoader() == null + && (!_JDKAccess.isExported(cls) || !CodeGenerator.sourcePublicAccessible(cls))) { + // Source generated into the unnamed module cannot legally name concealed or package-private + // JDK implementation classes. Field-access serializers own those cases. + return false; + } // bean class can be static nested class, but can't be a non-static inner class. // Check modifiers first to avoid loading the enclosing class unnecessarily — // in classloader-isolated environments (e.g. OSGi, module systems) the enclosing diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/CompatibleCollectionArrayReader.java b/java/fory-core/src/main/java/org/apache/fory/serializer/CompatibleCollectionArrayReader.java index c125edd722..42b1436e90 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/CompatibleCollectionArrayReader.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/CompatibleCollectionArrayReader.java @@ -307,7 +307,7 @@ private static Object readTracking( int elementTypeId, Class targetType) { RefReader refReader = readContext.getRefReader(); - int nextReadRefId = refReader.tryPreserveRefId(readContext.getBuffer()); + int nextReadRefId = readContext.tryPreserveRefId(); if (nextReadRefId >= Fory.NOT_NULL_VALUE_FLAG) { Object value = readNotNull(readContext, readMode, arrayTypeId, elementTypeId, targetType); refReader.setReadRef(nextReadRefId, value); diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/CompatibleLayerSerializerBase.java b/java/fory-core/src/main/java/org/apache/fory/serializer/CompatibleLayerSerializerBase.java index f7fec4063f..e7040f16f4 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/CompatibleLayerSerializerBase.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/CompatibleLayerSerializerBase.java @@ -21,6 +21,7 @@ import org.apache.fory.collection.IdentityObjectIntMap; import org.apache.fory.collection.ObjectIntMap; +import org.apache.fory.context.CopyContext; import org.apache.fory.context.MetaWriteContext; import org.apache.fory.context.ReadContext; import org.apache.fory.context.RefReader; @@ -46,7 +47,9 @@ public abstract class CompatibleLayerSerializerBase extends AbstractObjectSer protected SerializationFieldInfo[] allFields = new SerializationFieldInfo[0]; public CompatibleLayerSerializerBase(TypeResolver typeResolver, Class type) { - super(typeResolver, type); + // Layer serializers are field-only views over an already-created subclass instance. Null keeps + // object construction with the concrete serializer owner. + super(typeResolver, type, null); } public final void setLayerSerializerMeta(TypeDef layerTypeDef, Class layerMarkerClass) { @@ -176,6 +179,15 @@ public Object[] getFieldValuesForPutFields( return vals; } + @SuppressWarnings("rawtypes") + public Object[] copyFieldValuesForPutFields( + CopyContext copyContext, Object obj, ObjectIntMap fieldIndexMap, int arraySize) { + checkLayerSerializerMeta(); + Object[] vals = new Object[arraySize]; + collectCopiedPutFieldValues(copyContext, obj, fieldIndexMap, vals, allFields); + return vals; + } + public void skipFields(ReadContext readContext) { checkLayerSerializerMeta(); MemoryBuffer buffer = readContext.getBuffer(); @@ -338,4 +350,27 @@ private void collectPutFieldValues( } } } + + @SuppressWarnings("rawtypes") + private void collectCopiedPutFieldValues( + CopyContext copyContext, + Object obj, + ObjectIntMap fieldIndexMap, + Object[] vals, + SerializationFieldInfo[] fieldInfos) { + for (SerializationFieldInfo fieldInfo : fieldInfos) { + FieldAccessor fieldAccessor = fieldInfo.fieldAccessor; + if (fieldAccessor != null) { + String fieldName = fieldAccessor.getField().getName(); + int index = fieldIndexMap.get(fieldName, -1); + if (index != -1 && index < vals.length) { + Object fieldValue = fieldAccessor.get(obj); + vals[index] = + fieldInfo.isPrimitiveField || fieldValue == null + ? fieldValue + : copyContext.copyObject(fieldValue); + } + } + } + } } diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/CompatibleSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/CompatibleSerializer.java index a4f2de53f6..4d27fe869f 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/CompatibleSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/CompatibleSerializer.java @@ -31,9 +31,6 @@ import org.apache.fory.logging.LoggerFactory; import org.apache.fory.memory.MemoryBuffer; import org.apache.fory.meta.TypeDef; -import org.apache.fory.platform.AndroidSupport; -import org.apache.fory.platform.GraalvmSupport; -import org.apache.fory.platform.UnsafeOps; import org.apache.fory.reflect.FieldAccessor; import org.apache.fory.resolver.ClassResolver; import org.apache.fory.resolver.RefMode; @@ -231,10 +228,7 @@ private T newInstance() { if (!hasDefaultValues) { return newBean(); } - T obj = - AndroidSupport.IS_ANDROID || GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE - ? newBean() - : UnsafeOps.newInstance(type); + T obj = newBean(); // Set default values for missing fields in Scala case classes DefaultValueUtils.setDefaultValues(obj, defaultValueFields); return obj; @@ -250,7 +244,7 @@ public T read(ReadContext readContext) { readFields(readContext, fieldValues); } fieldValues = RecordUtils.remapping(recordInfo, fieldValues); - T t = objectCreator.newInstanceWithArguments(fieldValues); + T t = objectInstantiator.newInstanceWithArguments(fieldValues); Arrays.fill(recordInfo.getRecordComponents(), null); return t; } @@ -266,6 +260,14 @@ public T read(ReadContext readContext) { return targetObject; } + private void setFieldValue(T targetObject, SerializationFieldInfo fieldInfo, Object fieldValue) { + if (fieldInfo.fieldAccessor != null) { + fieldInfo.fieldAccessor.putObject(targetObject, fieldValue); + } else if (fieldInfo.fieldConverter != null) { + fieldInfo.fieldConverter.set(targetObject, fieldValue); + } + } + private void readFields(ReadContext readContext, T targetObject) { MemoryBuffer buffer = readContext.getBuffer(); RefReader refReader = readContext.getRefReader(); @@ -384,6 +386,23 @@ private Object readField( } } + private Object readFieldValue( + ReadContext readContext, + RefReader refReader, + Generics generics, + SerializationFieldInfo fieldInfo, + MemoryBuffer buffer, + CompatibleCollectionArrayReader.ReadAction action) { + if (fieldInfo.fieldAccessor == null + && fieldInfo.fieldConverter != null + && fieldInfo.codecCategory == FieldGroups.FieldCodecCategory.BUILD_IN + && action == null) { + return AbstractObjectSerializer.readBuildInFieldValue( + readContext, typeResolver, refReader, fieldInfo, buffer); + } + return readField(readContext, refReader, generics, fieldInfo, buffer, action); + } + private void printFieldDebugInfo(SerializationFieldInfo fieldInfo, MemoryBuffer buffer) { LOG.info( "[Java] read field {} of type {}, reader index {}", diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/ExceptionSerializers.java b/java/fory-core/src/main/java/org/apache/fory/serializer/ExceptionSerializers.java index b0f5f99e04..e5689087ff 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/ExceptionSerializers.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/ExceptionSerializers.java @@ -38,15 +38,17 @@ import org.apache.fory.context.WriteContext; import org.apache.fory.exception.ForyException; import org.apache.fory.memory.MemoryBuffer; +import org.apache.fory.memory.MemoryUtils; import org.apache.fory.meta.TypeDef; import org.apache.fory.platform.AndroidSupport; import org.apache.fory.platform.GraalvmSupport; -import org.apache.fory.platform.UnsafeOps; -import org.apache.fory.reflect.ObjectCreator; -import org.apache.fory.reflect.ObjectCreators; +import org.apache.fory.platform.JdkVersion; +import org.apache.fory.platform.internal._JDKAccess; +import org.apache.fory.reflect.FieldAccessor; +import org.apache.fory.reflect.ObjectInstantiator; +import org.apache.fory.reflect.ObjectInstantiators; import org.apache.fory.reflect.ReflectionUtils; import org.apache.fory.resolver.TypeResolver; -import org.apache.fory.util.unsafe._JDKAccess; /** Serializers for {@link Throwable} and {@link StackTraceElement}. */ @SuppressWarnings({"rawtypes", "unchecked"}) @@ -58,7 +60,7 @@ private ExceptionSerializers() {} public static final class ExceptionSerializer extends Serializer { private final Config config; private final TypeResolver typeResolver; - private final ObjectCreator objectCreator; + private final ObjectInstantiator objectInstantiator; private final Constructor messageConstructor; private volatile Serializer[] slotsSerializers; private volatile boolean rebuildSlotsSerializersAtRuntime; @@ -68,18 +70,19 @@ public ExceptionSerializer(TypeResolver typeResolver, Class type) { this.config = typeResolver.getConfig(); this.typeResolver = typeResolver; messageConstructor = getOptionalMessageConstructor(type); - objectCreator = - messageConstructor == null && !AndroidSupport.IS_ANDROID - ? createThrowableObjectCreator(type) + objectInstantiator = + messageConstructor == null && MemoryUtils.JDK_LANG_FIELD_ACCESS + ? createThrowableObjectInstantiator(typeResolver, type) : null; slotsSerializers = buildSlotsSerializers(typeResolver, type); - if (AndroidSupport.IS_ANDROID + if (!MemoryUtils.JDK_LANG_FIELD_ACCESS && isJdkThrowable(type) && hasSubclassFields(slotsSerializers)) { throw new ForyException( - "Android doesn't support JDK Throwable type " + "Throwable serialization for JDK type " + type.getName() - + " with subclass fields because JDK private field layouts aren't stable on Android."); + + " with subclass fields requires JDK internal field access. " + + jdkFieldAccessMessage()); } // Native-image runtime must rebuild slot serializers once so field accessors and // descriptors are created against the runtime heap layout instead of reusing @@ -110,7 +113,7 @@ public void write(WriteContext writeContext, T value) { public T read(ReadContext readContext) { Serializer[] slotsSerializers = getSlotsSerializers(); StackTraceElement[] stackTrace = (StackTraceElement[]) readContext.readRef(); - if (AndroidSupport.IS_ANDROID) { + if (!MemoryUtils.JDK_LANG_FIELD_ACCESS) { return readAndroidThrowableWithoutDetailMessageField( readContext, stackTrace, slotsSerializers); } @@ -120,14 +123,12 @@ public T read(ReadContext readContext) { String detailMessage = readContext.readStringRef(); List suppressedExceptions = readSuppressedExceptions(readContext); skipExtraFields(readContext); - UnsafeOps.putObject(obj, ThrowableOffsets.DETAIL_MESSAGE_FIELD_OFFSET, detailMessage); + ThrowableAccessors.DETAIL_MESSAGE_ACCESSOR.putObject(obj, detailMessage); if (stackTrace != null) { obj.setStackTrace(stackTrace); } - if (cause != null) { - obj.initCause(cause); - } - addSuppressedExceptions(obj, suppressedExceptions); + ThrowableAccessors.setCause(obj, cause); + ThrowableAccessors.setSuppressedExceptions(obj, suppressedExceptions); readAndSetFields(readContext, obj, slotsSerializers, config); return obj; } @@ -136,10 +137,10 @@ private T readAndroidThrowableWithoutDetailMessageField( ReadContext readContext, StackTraceElement[] stackTrace, Serializer[] slotsSerializers) { if (messageConstructor == null) { throw new ForyException( - "Android doesn't support deserializing Throwable type " + "Deserializing Throwable type " + type.getName() - + " without a String message constructor because private JDK field access is " - + "unsupported."); + + " without a String message constructor requires JDK internal field access. " + + jdkFieldAccessMessage()); } int refId = readContext.lastPreservedRefId(); if (refId >= 0) { @@ -151,9 +152,10 @@ private T readAndroidThrowableWithoutDetailMessageField( skipExtraFields(readContext); if (containsPendingThrowable(cause) || containsPendingThrowable(suppressedExceptions)) { throw new ForyException( - "Android doesn't support deserializing cyclic Throwable references for type " + "Deserializing cyclic Throwable references for type " + type.getName() - + " because private JDK field access is unsupported."); + + " requires JDK internal field access. " + + jdkFieldAccessMessage()); } T obj = newThrowableWithMessage(detailMessage); readContext.reference(obj); @@ -179,7 +181,7 @@ private T newThrowableForRead() { + " without a String message constructor because it requires Unsafe allocation " + "or unsupported private-field access."); } - return objectCreator.newInstance(); + return objectInstantiator.newInstance(); } private T newThrowableWithMessage(String detailMessage) { @@ -221,18 +223,26 @@ private void writeNumClassLayers(MemoryBuffer buffer, Serializer[] slotsSerializ } } + private static String jdkFieldAccessMessage() { + if (!AndroidSupport.IS_ANDROID && JdkVersion.MAJOR_VERSION >= 25) { + return _JDKAccess.jdk25AccessMessage(); + } + return "This Throwable shape is unsupported on runtimes without JDK internal field access."; + } + public static final class StackTraceElementSerializer extends Serializer { private static final MethodHandles.Lookup LOOKUP = - AndroidSupport.IS_ANDROID ? null : _JDKAccess._trustedLookup(StackTraceElement.class); + AndroidSupport.IS_ANDROID + ? null + : (MemoryUtils.JDK_LANG_FIELD_ACCESS + ? _JDKAccess._trustedLookup(StackTraceElement.class) + : MethodHandles.publicLookup()); private static final MethodHandle CLASS_LOADER_NAME_GETTER = AndroidSupport.IS_ANDROID ? null : getOptionalGetter("getClassLoaderName"); private static final MethodHandle MODULE_NAME_GETTER = getOptionalGetter("getModuleName"); private static final MethodHandle MODULE_VERSION_GETTER = getOptionalGetter("getModuleVersion"); private static final MethodHandle STACK_TRACE_ELEMENT_CTR_V1 = - AndroidSupport.IS_ANDROID - ? null - : ReflectionUtils.getCtrHandle( - StackTraceElement.class, String.class, String.class, String.class, int.class); + getOptionalCtr(String.class, String.class, String.class, int.class); private static final MethodHandle STACK_TRACE_ELEMENT_CTR_V2 = AndroidSupport.IS_ANDROID ? null @@ -349,23 +359,26 @@ private static StackTraceElement newStackTraceElement( fileName, lineNumber); } - return (StackTraceElement) - STACK_TRACE_ELEMENT_CTR_V1.invoke(declaringClass, methodName, fileName, lineNumber); + if (STACK_TRACE_ELEMENT_CTR_V1 != null) { + return (StackTraceElement) + STACK_TRACE_ELEMENT_CTR_V1.invoke(declaringClass, methodName, fileName, lineNumber); + } + return new StackTraceElement(declaringClass, methodName, fileName, lineNumber); } catch (Throwable t) { throw new RuntimeException(t); } } } - private static ObjectCreator createThrowableObjectCreator( - Class type) { - if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { - return new ObjectCreators.UnsafeObjectCreator<>(type); + private static ObjectInstantiator createThrowableObjectInstantiator( + TypeResolver typeResolver, Class type) { + if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE || JdkVersion.MAJOR_VERSION >= 25) { + return typeResolver.getObjectInstantiator(type); } if (ReflectionUtils.getCtrHandle(type, false) != null) { - return ObjectCreators.getObjectCreator(type); + return typeResolver.getObjectInstantiator(type); } - return new ObjectCreators.ParentNoArgCtrObjectCreator<>(type); + return new ObjectInstantiators.ParentNoArgCtrInstantiator<>(type); } private static Constructor getOptionalMessageConstructor(Class type) { @@ -418,7 +431,9 @@ private static Serializer[] buildSlotsSerializers(TypeResolver typeResolver, slotsSerializer = new CompatibleLayerSerializer(typeResolver, type, layerTypeDef, layerMarkerClass); } else { - slotsSerializer = new ObjectSerializer<>(typeResolver, type, false); + // Throwable slot serializers populate fields on the throwable allocated by + // ExceptionSerializer. + slotsSerializer = new ObjectSerializer<>(typeResolver, type, false, null); } serializers.add(slotsSerializer); type = (Class) type.getSuperclass(); @@ -540,19 +555,40 @@ private static boolean containsPendingThrowable(Throwable throwable, Set suppressedExceptions) { + SUPPRESSED_ACCESSOR.putObject( + throwable, + suppressedExceptions.isEmpty() + ? DEFAULT_SUPPRESSED_EXCEPTIONS + : new ArrayList<>(suppressedExceptions)); + } } private static final class PendingThrowable extends Throwable { diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/ExternalizableSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/ExternalizableSerializer.java index f1539d83bf..e9cd3a92ff 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/ExternalizableSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/ExternalizableSerializer.java @@ -57,7 +57,7 @@ public void write(WriteContext writeContext, T value) { @Override public T read(ReadContext readContext) { - T t = objectCreator.newInstance(); + T t = objectInstantiator.newInstance(); readContext.reference(t); objectInput.setReadContext(readContext); try { diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/FieldGroups.java b/java/fory-core/src/main/java/org/apache/fory/serializer/FieldGroups.java index 3dab3aa492..1337285e97 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/FieldGroups.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/FieldGroups.java @@ -233,7 +233,7 @@ public SerializationFieldInfo(TypeResolver resolver, Descriptor d) { this.fieldAccessor = null; } // Use local field type to determine if field is primitive. - // This determines how to write the value to the object (UnsafeOps.putInt vs putObject). + // This determines how to write the value to the object (unboxed putInt vs putObject). isPrimitiveField = typeRef.getRawType().isPrimitive(); fieldConverter = d.getFieldConverter(); // TypeExtMeta is xlang field-wrapper metadata. Native local descriptors keep native diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/FinalFieldReplaceResolveSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/FinalFieldReplaceResolveSerializer.java index 92ca339219..40bdb8b535 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/FinalFieldReplaceResolveSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/FinalFieldReplaceResolveSerializer.java @@ -41,13 +41,13 @@ public FinalFieldReplaceResolveSerializer(TypeResolver typeResolver, Class type) @Override protected void writeObject( WriteContext writeContext, Object value, MethodInfoCache jdkMethodInfoCache) { - jdkMethodInfoCache.objectSerializer.write(writeContext, value); + jdkMethodInfoCache.objectSerializer().write(writeContext, value); } @Override protected Object readObject(ReadContext readContext) { MethodInfoCache jdkMethodInfoCache = getMethodInfoCache(type); - Object o = jdkMethodInfoCache.objectSerializer.read(readContext); + Object o = jdkMethodInfoCache.objectSerializer().read(readContext); ReplaceResolveInfo replaceResolveInfo = jdkMethodInfoCache.info; if (replaceResolveInfo.readResolveMethod == null) { return o; diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/JavaSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/JavaSerializer.java index 397c3ac151..d0c2007f82 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/JavaSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/JavaSerializer.java @@ -19,6 +19,8 @@ package org.apache.fory.serializer; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.Externalizable; import java.io.IOException; import java.io.ObjectInputStream; @@ -30,6 +32,7 @@ import java.nio.ByteBuffer; import org.apache.fory.Fory; import org.apache.fory.collection.ClassValueCache; +import org.apache.fory.context.CopyContext; import org.apache.fory.context.ReadContext; import org.apache.fory.context.WriteContext; import org.apache.fory.io.ClassLoaderObjectInputStream; @@ -51,14 +54,16 @@ *

When a serializer not found and {@link ClassResolver#requireJavaSerialization(Class)} return * true, this serializer will be used. */ -@SuppressWarnings({"rawtypes", "unchecked"}) -public class JavaSerializer extends AbstractObjectSerializer { +@SuppressWarnings({"unchecked"}) +public class JavaSerializer extends Serializer { private static final Logger LOG = LoggerFactory.getLogger(JavaSerializer.class); + private final TypeResolver typeResolver; private final MemoryBufferObjectInput objectInput; private final MemoryBufferObjectOutput objectOutput; public JavaSerializer(TypeResolver typeResolver, Class cls) { - super(typeResolver, cls); + super(typeResolver.getConfig(), (Class) cls); + this.typeResolver = typeResolver; // TODO(chgaokunyang) enable this check when ObjectSerializer is implemented. // Preconditions.checkArgument(ClassResolver.requireJavaSerialization(cls)); if (cls != SerializedLambda.class) { @@ -69,8 +74,8 @@ public JavaSerializer(TypeResolver typeResolver, Class cls) { Serializer.class.getName(), Externalizable.class.getName()); } - objectInput = new MemoryBufferObjectInput(config, null); - objectOutput = new MemoryBufferObjectOutput(config, null); + objectInput = new MemoryBufferObjectInput(typeResolver.getConfig(), null); + objectOutput = new MemoryBufferObjectOutput(typeResolver.getConfig(), null); } @Override @@ -113,6 +118,26 @@ public Object read(ReadContext readContext) { throw new IllegalStateException("unreachable code"); } + @Override + public Object copy(CopyContext copyContext, Object value) { + // JavaSerializer copy must run the Java serialization lifecycle because readObject can rebuild + // transient state that object-field copy cannot infer. + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try (ObjectOutputStream output = new ObjectOutputStream(bytes)) { + output.writeObject(value); + } + try (ObjectInputStream input = + new ClassLoaderObjectInputStream( + typeResolver, new ByteArrayInputStream(bytes.toByteArray()))) { + return input.readObject(); + } + } catch (IOException | ClassNotFoundException e) { + ExceptionUtils.throwException(e); + throw new IllegalStateException("unreachable code"); + } + } + private static final ClassValueCache writeObjectMethodCache = ClassValueCache.newClassKeyCache(32); diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/JdkProxySerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/JdkProxySerializer.java index 9d13d97682..b2467f44e4 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/JdkProxySerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/JdkProxySerializer.java @@ -26,9 +26,9 @@ import org.apache.fory.context.CopyContext; import org.apache.fory.context.ReadContext; import org.apache.fory.context.WriteContext; -import org.apache.fory.platform.AndroidSupport; +import org.apache.fory.memory.MemoryUtils; import org.apache.fory.platform.GraalvmSupport; -import org.apache.fory.platform.UnsafeOps; +import org.apache.fory.reflect.FieldAccessor; import org.apache.fory.reflect.ReflectionUtils; import org.apache.fory.resolver.TypeResolver; import org.apache.fory.util.Preconditions; @@ -73,10 +73,9 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl } private static final class ProxyHandlerField { - // Make offset compatible with graalvm native image, but load it only on the JVM Unsafe path. private static final Field FIELD = ReflectionUtils.getField(Proxy.class, InvocationHandler.class); - private static final long OFFSET = UnsafeOps.objectFieldOffset(FIELD); + private static final FieldAccessor ACCESSOR = FieldAccessor.createAccessor(FIELD); } private interface StubInterface { @@ -118,7 +117,7 @@ public Object copy(CopyContext copyContext, Object value) { Preconditions.checkNotNull(copyHandler); return Proxy.newProxyInstance(typeResolver.getClassLoader(), interfaces, copyHandler); } - if (AndroidSupport.IS_ANDROID) { + if (!MemoryUtils.JDK_PROXY_FIELD_ACCESS) { DeferredInvocationHandler deferredHandler = new DeferredInvocationHandler(); Object proxy = Proxy.newProxyInstance(typeResolver.getClassLoader(), interfaces, deferredHandler); @@ -130,7 +129,7 @@ public Object copy(CopyContext copyContext, Object value) { } Object proxy = Proxy.newProxyInstance(typeResolver.getClassLoader(), interfaces, STUB_HANDLER); copyContext.reference(value, proxy); - UnsafeOps.putObject(proxy, ProxyHandlerField.OFFSET, copyContext.copyObject(invocationHandler)); + ProxyHandlerField.ACCESSOR.putObject(proxy, copyContext.copyObject(invocationHandler)); return proxy; } @@ -144,7 +143,7 @@ public Object read(ReadContext readContext) { unwrapInvocationHandler((InvocationHandler) readContext.readRef()); return Proxy.newProxyInstance(typeResolver.getClassLoader(), interfaces, invocationHandler); } - if (AndroidSupport.IS_ANDROID) { + if (!MemoryUtils.JDK_PROXY_FIELD_ACCESS) { DeferredInvocationHandler deferredHandler = new DeferredInvocationHandler(); Object proxy = Proxy.newProxyInstance(typeResolver.getClassLoader(), interfaces, deferredHandler); @@ -158,7 +157,7 @@ public Object read(ReadContext readContext) { readContext.setReadRef(refId, proxy); InvocationHandler invocationHandler = unwrapInvocationHandler((InvocationHandler) readContext.readRef()); - UnsafeOps.putObject(proxy, ProxyHandlerField.OFFSET, invocationHandler); + ProxyHandlerField.ACCESSOR.putObject(proxy, invocationHandler); return proxy; } diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/LambdaSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/LambdaSerializer.java index f7f68a1a6e..6376b62d03 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/LambdaSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/LambdaSerializer.java @@ -28,10 +28,10 @@ import org.apache.fory.context.ReadContext; import org.apache.fory.context.WriteContext; import org.apache.fory.platform.AndroidSupport; +import org.apache.fory.platform.internal._JDKAccess; import org.apache.fory.resolver.TypeResolver; import org.apache.fory.util.Preconditions; import org.apache.fory.util.function.SerializableFunction; -import org.apache.fory.util.unsafe._JDKAccess; /** * Serializer for java serializable lambda. Use fory to serialize java lambda instead of JDK diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/ObjectSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/ObjectSerializer.java index 3830ead2f4..e656359d14 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/ObjectSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/ObjectSerializer.java @@ -33,6 +33,7 @@ import org.apache.fory.logging.LoggerFactory; import org.apache.fory.memory.MemoryBuffer; import org.apache.fory.meta.TypeDef; +import org.apache.fory.reflect.ObjectInstantiator; import org.apache.fory.resolver.TypeResolver; import org.apache.fory.serializer.FieldGroups.SerializationFieldInfo; import org.apache.fory.serializer.struct.Fingerprint; @@ -70,7 +71,15 @@ public ObjectSerializer(TypeResolver typeResolver, Class cls) { } public ObjectSerializer(TypeResolver typeResolver, Class cls, boolean resolveParent) { - super(typeResolver, cls); + this(typeResolver, cls, resolveParent, typeResolver.getObjectInstantiator(cls)); + } + + public ObjectSerializer( + TypeResolver typeResolver, + Class cls, + boolean resolveParent, + ObjectInstantiator objectInstantiator) { + super(typeResolver, cls, objectInstantiator); // avoid recursive building serializers. // Use `setSerializerIfAbsent` to avoid overwriting existing serializer for class when used // as data serializer. @@ -137,6 +146,11 @@ public void write(WriteContext writeContext, T value) { // Protocol order: primitive, nullable primitive, then all non-primitives by field identifier. RefWriter refWriter = writeContext.getRefWriter(); Generics generics = writeContext.getGenerics(); + writeFields(writeContext, value, refWriter, generics); + } + + private void writeFields( + WriteContext writeContext, T value, RefWriter refWriter, Generics generics) { for (SerializationFieldInfo fieldInfo : allFields) { writeFieldByCodecCategory(writeContext, value, refWriter, generics, fieldInfo); } @@ -198,7 +212,7 @@ public T read(ReadContext readContext) { if (isRecord) { Object[] fields = readFields(readContext); fields = RecordUtils.remapping(recordInfo, fields); - T obj = objectCreator.newInstanceWithArguments(fields); + T obj = objectInstantiator.newInstanceWithArguments(fields); Arrays.fill(recordInfo.getRecordComponents(), null); return obj; } diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/ObjectStreamSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/ObjectStreamSerializer.java index 4c548a260c..20a3851e1e 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/ObjectStreamSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/ObjectStreamSerializer.java @@ -31,6 +31,7 @@ import java.io.ObjectStreamField; import java.io.Serializable; import java.io.UnsupportedEncodingException; +import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -49,6 +50,7 @@ import org.apache.fory.collection.ObjectArray; import org.apache.fory.collection.ObjectIntMap; import org.apache.fory.config.Int64Encoding; +import org.apache.fory.context.CopyContext; import org.apache.fory.context.MetaReadContext; import org.apache.fory.context.ReadContext; import org.apache.fory.context.WriteContext; @@ -62,9 +64,7 @@ import org.apache.fory.meta.TypeDef; import org.apache.fory.platform.AndroidSupport; import org.apache.fory.platform.GraalvmSupport; -import org.apache.fory.reflect.ObjectCreator; -import org.apache.fory.reflect.ObjectCreators; -import org.apache.fory.reflect.ReflectionUtils; +import org.apache.fory.platform.internal._JDKAccess; import org.apache.fory.resolver.ClassResolver; import org.apache.fory.resolver.TypeInfo; import org.apache.fory.resolver.TypeResolver; @@ -74,7 +74,6 @@ import org.apache.fory.type.Types; import org.apache.fory.util.ExceptionUtils; import org.apache.fory.util.Preconditions; -import org.apache.fory.util.unsafe._JDKAccess; /** * Implement jdk custom serialization only if following conditions are met: @@ -82,10 +81,12 @@ *
    *
  • `writeObject/readObject` occurs only at current class in class hierarchy. *
  • `writeReplace/readResolve` don't occur in class hierarchy. - *
  • class hierarchy doesn't have duplicated fields. If any of those conditions are not met, - * fallback jdk custom serialization to {@link JavaSerializer}. + *
  • class hierarchy doesn't have duplicated fields. *
* + *

Serializer selection must not change by JDK version. Types that use this serializer on JDK8 + * through JDK24 must keep this serializer on JDK25+ so existing Fory bytes remain readable. + * *

`ObjectInputStream#setObjectInputFilter` will be ignored by this serializer. */ @SuppressWarnings({"unchecked", "rawtypes"}) @@ -160,17 +161,17 @@ CompatibleLayerSerializerBase getReadSerializer( /** * Safe wrapper for ObjectStreamClass.lookup that handles GraalVM native image limitations. In * GraalVM native image, ObjectStreamClass.lookup may fail for certain classes like Throwable due - * to missing SerializationConstructorAccessor. This method catches such errors and returns null, - * allowing the serializer to use alternative approaches like Unsafe.allocateInstance. + * to missing SerializationConstructorAccessor. This method catches such errors and returns null + * so the serializer can use its constructor-bypassing object instantiator. */ private static ObjectStreamClass safeObjectStreamClassLookup(Class type) { if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { try { return ObjectStreamClass.lookup(type); } catch (Throwable e) { - // In GraalVM native image, ObjectStreamClass.lookup may fail for certain classes - // due to missing SerializationConstructorAccessor. We catch this and return null - // to allow fallback to Unsafe-based object creation. + // In GraalVM native image, ObjectStreamClass.lookup may fail for certain classes due to + // missing SerializationConstructorAccessor. Returning null keeps stream reconstruction on + // the serializer-owned object instantiator path. LOG.warn( "ObjectStreamClass.lookup failed for {} in GraalVM native image: {}", type.getName(), @@ -184,7 +185,7 @@ private static ObjectStreamClass safeObjectStreamClassLookup(Class type) { } public ObjectStreamSerializer(TypeResolver typeResolver, Class type) { - super(typeResolver, type, createObjectCreatorForGraalVM(type)); + super(typeResolver, type, typeResolver.getSharedRegistry().getObjectStreamInstantiator(type)); if (!Serializable.class.isAssignableFrom(type)) { throw new IllegalArgumentException( String.format("Class %s should implement %s.", type, Serializable.class)); @@ -213,20 +214,6 @@ public ObjectStreamSerializer(TypeResolver typeResolver, Class type) { slotsInfos = slotsInfoList.toArray(new SlotInfo[0]); } - /** - * Creates an appropriate ObjectCreator for GraalVM native image environment. In GraalVM, we - * prefer UnsafeObjectCreator to avoid serialization constructor issues. - */ - private static ObjectCreator createObjectCreatorForGraalVM(Class type) { - if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { - // In GraalVM native image, use Unsafe to avoid serialization constructor issues - return new ObjectCreators.UnsafeObjectCreator<>(type); - } else { - // In regular JVM, use the standard object creator - return ObjectCreators.getObjectCreator(type); - } - } - @Override public void write(WriteContext writeContext, Object value) { MemoryBuffer buffer = writeContext.getBuffer(); @@ -282,7 +269,7 @@ public void write(WriteContext writeContext, Object value) { @Override public Object read(ReadContext readContext) { MemoryBuffer buffer = readContext.getBuffer(); - Object obj = objectCreator.newInstance(); + Object obj = objectInstantiator.newInstance(); readContext.reference(obj); int numClasses = buffer.readInt16(); int slotIndex = 0; @@ -340,43 +327,14 @@ public Object read(ReadContext readContext) { Method readObjectMethod = streamTypeInfo.readObjectMethod; if (readObjectMethod == null) { - // For standard field serialization - use getCurrentReadSerializer() - matchedSlot.getCurrentReadSerializer().readAndSetFields(readContext, obj); - } else { - // For custom readObject, it handles its own format - ForyStructInputStream objectInputStream = matchedSlot.getObjectInputStream(); - MemoryBuffer oldBuffer = objectInputStream.buffer; - Object oldObject = objectInputStream.targetObject; - ReadContext oldReadContext = objectInputStream.readContext; - ForyStructInputStream.GetFieldImpl oldGetField = objectInputStream.getField; - ForyStructInputStream.GetFieldImpl getField = - (ForyStructInputStream.GetFieldImpl) matchedSlot.getFieldPool().popOrNull(); - if (getField == null) { - getField = new ForyStructInputStream.GetFieldImpl(matchedSlot); - } - boolean fieldsRead = objectInputStream.fieldsRead; - try { - objectInputStream.fieldsRead = false; - objectInputStream.buffer = buffer; - objectInputStream.targetObject = obj; - objectInputStream.readContext = readContext; - objectInputStream.getField = getField; - objectInputStream.callbacks = callbacks; - if (streamTypeInfo.readObjectFunc != null) { - streamTypeInfo.readObjectFunc.accept(obj, objectInputStream); - } else { - readObjectMethod.invoke(obj, objectInputStream); - } - } finally { - objectInputStream.fieldsRead = fieldsRead; - objectInputStream.buffer = oldBuffer; - objectInputStream.targetObject = oldObject; - objectInputStream.readContext = oldReadContext; - objectInputStream.getField = oldGetField; - matchedSlot.getFieldPool().add(getField); - objectInputStream.callbacks = null; - Arrays.fill(getField.vals, ForyStructInputStream.NO_VALUE_STUB); + if (streamTypeInfo.defaultReadObjectHandle != null) { + readObjectStreamSlot(matchedSlot, readContext, obj, callbacks, true); + } else { + // For standard field serialization - use getCurrentReadSerializer() + matchedSlot.getCurrentReadSerializer().readAndSetFields(readContext, obj); } + } else { + readObjectStreamSlot(matchedSlot, readContext, obj, callbacks, false); } } @@ -397,12 +355,131 @@ public Object read(ReadContext readContext) { for (ObjectInputValidation validation : callbacks.values()) { validation.validateObject(); } - } catch (InvocationTargetException | IllegalAccessException | InvalidObjectException e) { + } catch (InvocationTargetException + | IllegalAccessException + | IOException + | ClassNotFoundException e) { throwSerializationException(type, e); } return obj; } + private void readObjectStreamSlot( + SlotInfo matchedSlot, + ReadContext readContext, + Object obj, + TreeMap callbacks, + boolean defaultRead) + throws IOException, + ClassNotFoundException, + InvocationTargetException, + IllegalAccessException { + StreamTypeInfo streamTypeInfo = matchedSlot.getStreamTypeInfo(); + ForyStructInputStream objectInputStream = matchedSlot.getObjectInputStream(); + MemoryBuffer oldBuffer = objectInputStream.buffer; + Object oldObject = objectInputStream.targetObject; + ReadContext oldReadContext = objectInputStream.readContext; + Object[] oldFieldValuesOverride = objectInputStream.fieldValuesOverride; + ForyStructInputStream.GetFieldImpl oldGetField = objectInputStream.getField; + ForyStructInputStream.GetFieldImpl getField = + (ForyStructInputStream.GetFieldImpl) matchedSlot.getFieldPool().popOrNull(); + if (getField == null) { + getField = new ForyStructInputStream.GetFieldImpl(matchedSlot); + } + boolean fieldsRead = objectInputStream.fieldsRead; + try { + objectInputStream.fieldsRead = false; + objectInputStream.buffer = readContext.getBuffer(); + objectInputStream.targetObject = obj; + objectInputStream.readContext = readContext; + objectInputStream.fieldValuesOverride = null; + objectInputStream.getField = getField; + objectInputStream.callbacks = callbacks; + if (defaultRead) { + objectInputStream.runDefaultReadObject(); + } else if (streamTypeInfo.readObjectFunc != null) { + streamTypeInfo.readObjectFunc.accept(obj, objectInputStream); + } else { + streamTypeInfo.readObjectMethod.invoke(obj, objectInputStream); + } + } finally { + objectInputStream.fieldsRead = fieldsRead; + objectInputStream.buffer = oldBuffer; + objectInputStream.targetObject = oldObject; + objectInputStream.readContext = oldReadContext; + objectInputStream.fieldValuesOverride = oldFieldValuesOverride; + objectInputStream.getField = oldGetField; + matchedSlot.getFieldPool().add(getField); + objectInputStream.callbacks = null; + Arrays.fill(getField.vals, ForyStructInputStream.NO_VALUE_STUB); + } + } + + @Override + public Object copy(CopyContext copyContext, Object value) { + if (!canCopyWithDefaultReadObject()) { + return super.copy(copyContext, value); + } + Object copy = objectInstantiator.newInstance(); + copyContext.reference(value, copy); + try { + for (SlotInfo slotInfo : slotsInfos) { + copyObjectStreamSlot(copyContext, value, copy, slotInfo); + } + return copy; + } catch (IOException | ClassNotFoundException e) { + throw new ForyException("Failed to copy Java serialization fields for " + type, e); + } + } + + private boolean canCopyWithDefaultReadObject() { + for (SlotInfo slotInfo : slotsInfos) { + if (slotInfo.getStreamTypeInfo().defaultReadObjectHandle == null) { + return false; + } + } + return true; + } + + private void copyObjectStreamSlot( + CopyContext copyContext, Object sourceObject, Object targetObject, SlotInfo slotInfo) + throws IOException, ClassNotFoundException { + ForyStructInputStream objectInputStream = slotInfo.getObjectInputStream(); + Object oldObject = objectInputStream.targetObject; + ReadContext oldReadContext = objectInputStream.readContext; + Object[] oldFieldValuesOverride = objectInputStream.fieldValuesOverride; + ForyStructInputStream.GetFieldImpl oldGetField = objectInputStream.getField; + ForyStructInputStream.GetFieldImpl getField = + (ForyStructInputStream.GetFieldImpl) slotInfo.getFieldPool().popOrNull(); + if (getField == null) { + getField = new ForyStructInputStream.GetFieldImpl(slotInfo); + } + boolean fieldsRead = objectInputStream.fieldsRead; + try { + objectInputStream.fieldsRead = false; + objectInputStream.targetObject = targetObject; + objectInputStream.readContext = null; + objectInputStream.fieldValuesOverride = + slotInfo + .getSlotsSerializer() + .copyFieldValuesForPutFields( + copyContext, + sourceObject, + slotInfo.getFieldIndexMap(), + slotInfo.getNumPutFields()); + objectInputStream.getField = getField; + objectInputStream.runDefaultReadObject(); + } finally { + objectInputStream.fieldsRead = fieldsRead; + objectInputStream.targetObject = oldObject; + objectInputStream.readContext = oldReadContext; + objectInputStream.fieldValuesOverride = oldFieldValuesOverride; + objectInputStream.getField = oldGetField; + slotInfo.getFieldPool().add(getField); + Arrays.fill(getField.vals, ForyStructInputStream.NO_VALUE_STUB); + } + } + /** * Skip data for a layer that exists in sender but not in receiver. This is needed for schema * evolution when sender's class hierarchy has layers that receiver doesn't have. @@ -602,33 +679,31 @@ private static class StreamTypeInfo { private final Method writeObjectMethod; private final Method readObjectMethod; private final Method readObjectNoData; + private final MethodHandle defaultReadObjectHandle; private final BiConsumer writeObjectFunc; private final BiConsumer readObjectFunc; private final Consumer readObjectNoDataFunc; private StreamTypeInfo(Class type) { - // ObjectStreamClass.lookup has cache inside, invocation cost won't be big. - ObjectStreamClass objectStreamClass = safeObjectStreamClassLookup(type); - // In JDK17, set private jdk method accessible will fail by default, use ObjectStreamClass - // instead, since it set accessible. Method writeMethod = null; Method readMethod = null; Method noDataMethod = null; - if (AndroidSupport.IS_ANDROID) { + if (AndroidSupport.IS_ANDROID || GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { writeMethod = JavaSerializer.getWriteObjectMethod(type, false); readMethod = JavaSerializer.getReadRefMethod(type, false); noDataMethod = JavaSerializer.getReadRefNoData(type, false); - } else if (objectStreamClass != null) { - writeMethod = - (Method) ReflectionUtils.getObjectFieldValue(objectStreamClass, "writeObjectMethod"); - readMethod = - (Method) ReflectionUtils.getObjectFieldValue(objectStreamClass, "readObjectMethod"); - noDataMethod = - (Method) ReflectionUtils.getObjectFieldValue(objectStreamClass, "readObjectNoData"); + } else { + writeMethod = SerializationHookLookup.getWriteObjectMethod(type); + readMethod = SerializationHookLookup.getReadObjectMethod(type); + noDataMethod = SerializationHookLookup.getReadObjectNoDataMethod(type); } this.writeObjectMethod = writeMethod; this.readObjectMethod = readMethod; this.readObjectNoData = noDataMethod; + this.defaultReadObjectHandle = + AndroidSupport.IS_ANDROID || GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE + ? null + : SerializationHookLookup.getDefaultReadObjectHandle(type); if (AndroidSupport.IS_ANDROID) { makeAccessible(writeObjectMethod); makeAccessible(readObjectMethod); @@ -760,7 +835,8 @@ public SlotsInfo(TypeResolver typeResolver, Class type) { fieldIndexMap = new ObjectIntMap<>(4, 0.4f); if (streamTypeInfo != null && (streamTypeInfo.writeObjectMethod != null - || streamTypeInfo.readObjectMethod != null)) { + || streamTypeInfo.readObjectMethod != null + || streamTypeInfo.defaultReadObjectHandle != null)) { this.numPutFields = slotsSerializer.getNumFields(); this.putFieldTypes = new Class[numPutFields]; slotsSerializer.populateFieldInfo(fieldIndexMap, putFieldTypes); @@ -779,7 +855,9 @@ public SlotsInfo(TypeResolver typeResolver, Class type) { } else { objectOutputStream = null; } - if (streamTypeInfo != null && streamTypeInfo.readObjectMethod != null) { + if (streamTypeInfo != null + && (streamTypeInfo.readObjectMethod != null + || streamTypeInfo.defaultReadObjectHandle != null)) { try { objectInputStream = new ForyStructInputStream(this); } catch (IOException e) { @@ -1235,6 +1313,7 @@ private static class ForyStructInputStream extends ObjectInputStream { private MemoryBuffer buffer; private Object targetObject; private GetFieldImpl getField; + private Object[] fieldValuesOverride; private boolean fieldsRead; private TreeMap callbacks; @@ -1387,8 +1466,11 @@ public GetField readFields() throws IOException { if (fieldsRead) { throw new NotActiveException("not in readObject invocation or fields already read"); } - // Read field values using MetaShare serialization - Object[] vals = slotsInfo.getCurrentReadSerializer().readFieldValues(readContext); + Object[] vals = fieldValuesOverride; + if (vals == null) { + // Read field values using MetaShare serialization + vals = slotsInfo.getCurrentReadSerializer().readFieldValues(readContext); + } System.arraycopy(vals, 0, getField.vals, 0, vals.length); fieldsRead = true; return getField; @@ -1427,11 +1509,29 @@ public void defaultReadObject() throws IOException, ClassNotFoundException { if (fieldsRead) { throw new NotActiveException("not in readObject invocation or fields already read"); } - // Read fields using MetaShare serialization (layer meta already read by getReadSerializer()) - slotsInfo.getCurrentReadSerializer().readAndSetFields(readContext, targetObject); + runDefaultReadObject(); fieldsRead = true; } + private void runDefaultReadObject() throws IOException, ClassNotFoundException { + MethodHandle defaultReadObjectHandle = slotsInfo.getStreamTypeInfo().defaultReadObjectHandle; + if (defaultReadObjectHandle == null) { + // Read fields using MetaShare serialization (layer meta already read by + // getReadSerializer()) + slotsInfo.getCurrentReadSerializer().readAndSetFields(readContext, targetObject); + return; + } + try { + // This public Java serialization compatibility hook writes final fields without requiring + // per-package JDK opens. It calls this stream's readFields() implementation for values. + defaultReadObjectHandle.invoke(targetObject, this); + } catch (IOException | ClassNotFoundException e) { + throw e; + } catch (Throwable e) { + throw new IOException("Failed to default-read Java serialization fields", e); + } + } + // At `registerValidation` point int `readObject` root is only partially correct. To fully // restore it user may need access to other state which is created by the subclass and // at this point will be null. Users thus use `registerValidation`. diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/PlatformStringUtils.java b/java/fory-core/src/main/java/org/apache/fory/serializer/PlatformStringUtils.java new file mode 100644 index 0000000000..a5f3747b31 --- /dev/null +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/PlatformStringUtils.java @@ -0,0 +1,261 @@ +/* + * 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.serializer; + +import java.lang.reflect.Field; +import org.apache.fory.memory.MemoryBuffer; +import org.apache.fory.memory.NativeByteOrder; +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; + +/** Platform-owned string internals used by {@link StringSerializer}. */ +final class PlatformStringUtils { + private static final Unsafe UNSAFE = AndroidSupport.IS_ANDROID ? null : _UnsafeUtils.UNSAFE; + private static final int BYTE_ARRAY_OFFSET; + private static final int CHAR_ARRAY_OFFSET; + + // GraalVM native-image needs arrayBaseOffset calls to store directly into their static fields so + // it can recompute the offsets for the image runtime. + static { + if (AndroidSupport.IS_ANDROID) { + BYTE_ARRAY_OFFSET = 0; + CHAR_ARRAY_OFFSET = 0; + } else { + BYTE_ARRAY_OFFSET = UNSAFE.arrayBaseOffset(byte[].class); + CHAR_ARRAY_OFFSET = UNSAFE.arrayBaseOffset(char[].class); + } + } + + private static final StringFields STRING_FIELDS = stringFields(); + + static final boolean JDK_STRING_FIELD_ACCESS = STRING_FIELDS.fieldAccess; + static final boolean STRING_VALUE_FIELD_IS_CHARS = + JDK_STRING_FIELD_ACCESS && STRING_FIELDS.valueFieldIsChars; + static final boolean STRING_VALUE_FIELD_IS_BYTES = + JDK_STRING_FIELD_ACCESS && STRING_FIELDS.valueFieldIsBytes; + static final boolean STRING_HAS_COUNT_OFFSET = JDK_STRING_FIELD_ACCESS && STRING_FIELDS.counted; + + private static final long STRING_VALUE_FIELD_OFFSET = STRING_FIELDS.valueOffset; + private static final long STRING_CODER_FIELD_OFFSET = STRING_FIELDS.coderOffset; + private static final long STRING_COUNT_FIELD_OFFSET = STRING_FIELDS.countOffset; + private static final long STRING_OFFSET_FIELD_OFFSET = STRING_FIELDS.offsetOffset; + + private PlatformStringUtils() {} + + private static StringFields stringFields() { + if (AndroidSupport.IS_ANDROID + || GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE + || !_JDKAccess.JDK_INTERNAL_FIELD_ACCESS) { + return StringFields.noAccess(); + } + try { + Field valueField = String.class.getDeclaredField("value"); + boolean valueFieldIsChars = valueField.getType() == char[].class; + boolean valueFieldIsBytes = valueField.getType() == byte[].class; + long valueOffset = UNSAFE.objectFieldOffset(valueField); + Field countField = getStringFieldNullable("count"); + Field offsetField = getStringFieldNullable("offset"); + boolean counted = false; + long countOffset = -1; + long offsetOffset = -1; + if (countField != null || offsetField != null) { + Preconditions.checkArgument( + countField != null && offsetField != null, "Current jdk not supported"); + Preconditions.checkArgument( + countField.getType() == int.class && offsetField.getType() == int.class, + "Current jdk not supported"); + counted = true; + countOffset = UNSAFE.objectFieldOffset(countField); + offsetOffset = UNSAFE.objectFieldOffset(offsetField); + } + long coderOffset = valueFieldIsBytes ? stringCoderFieldOffset() : -1; + return new StringFields( + true, + valueFieldIsChars, + valueFieldIsBytes, + counted, + valueOffset, + coderOffset, + countOffset, + offsetOffset); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } + } + + private static Field getStringFieldNullable(String fieldName) { + try { + return String.class.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + return null; + } + } + + private static long stringCoderFieldOffset() { + try { + return UNSAFE.objectFieldOffset(String.class.getDeclaredField("coder")); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } + } + + private static final class StringFields { + private final boolean fieldAccess; + private final boolean valueFieldIsChars; + private final boolean valueFieldIsBytes; + private final boolean counted; + private final long valueOffset; + private final long coderOffset; + private final long countOffset; + private final long offsetOffset; + + private StringFields( + boolean fieldAccess, + boolean valueFieldIsChars, + boolean valueFieldIsBytes, + boolean counted, + long valueOffset, + long coderOffset, + long countOffset, + long offsetOffset) { + this.fieldAccess = fieldAccess; + this.valueFieldIsChars = valueFieldIsChars; + this.valueFieldIsBytes = valueFieldIsBytes; + this.counted = counted; + this.valueOffset = valueOffset; + this.coderOffset = coderOffset; + this.countOffset = countOffset; + this.offsetOffset = offsetOffset; + } + + private static StringFields noAccess() { + return new StringFields(false, false, false, false, -1, -1, -1, -1); + } + } + + static Object getStringValue(String value) { + return UNSAFE.getObject(value, STRING_VALUE_FIELD_OFFSET); + } + + static byte getStringCoder(String value) { + return UNSAFE.getByte(value, STRING_CODER_FIELD_OFFSET); + } + + static int getStringOffset(String value) { + return UNSAFE.getInt(value, STRING_OFFSET_FIELD_OFFSET); + } + + static int getStringCount(String value) { + return UNSAFE.getInt(value, STRING_COUNT_FIELD_OFFSET); + } + + static long getCharsLong(char[] chars, int charIndex) { + if (AndroidSupport.IS_ANDROID) { + long c0 = chars[charIndex]; + long c1 = chars[charIndex + 1]; + long c2 = chars[charIndex + 2]; + long c3 = chars[charIndex + 3]; + if (NativeByteOrder.IS_LITTLE_ENDIAN) { + return c0 | (c1 << 16) | (c2 << 32) | (c3 << 48); + } else { + return (c0 << 48) | (c1 << 32) | (c2 << 16) | c3; + } + } + return UNSAFE.getLong(chars, CHAR_ARRAY_OFFSET + ((long) charIndex << 1)); + } + + static long getBytesLong(byte[] bytes, int byteIndex) { + if (AndroidSupport.IS_ANDROID) { + if (NativeByteOrder.IS_LITTLE_ENDIAN) { + return ((long) bytes[byteIndex] & 0xff) + | (((long) bytes[byteIndex + 1] & 0xff) << 8) + | (((long) bytes[byteIndex + 2] & 0xff) << 16) + | (((long) bytes[byteIndex + 3] & 0xff) << 24) + | (((long) bytes[byteIndex + 4] & 0xff) << 32) + | (((long) bytes[byteIndex + 5] & 0xff) << 40) + | (((long) bytes[byteIndex + 6] & 0xff) << 48) + | (((long) bytes[byteIndex + 7] & 0xff) << 56); + } else { + return (((long) bytes[byteIndex] & 0xff) << 56) + | (((long) bytes[byteIndex + 1] & 0xff) << 48) + | (((long) bytes[byteIndex + 2] & 0xff) << 40) + | (((long) bytes[byteIndex + 3] & 0xff) << 32) + | (((long) bytes[byteIndex + 4] & 0xff) << 24) + | (((long) bytes[byteIndex + 5] & 0xff) << 16) + | (((long) bytes[byteIndex + 6] & 0xff) << 8) + | ((long) bytes[byteIndex + 7] & 0xff); + } + } + // Unsafe object offsets are long. Keep the cast so JDK8-compiled bytecode calls + // getLong(Object, long) when the artifact runs on JDK9+. + return UNSAFE.getLong(bytes, (long) BYTE_ARRAY_OFFSET + byteIndex); + } + + static char getBytesChar(byte[] bytes, int byteIndex) { + if (AndroidSupport.IS_ANDROID) { + if (NativeByteOrder.IS_LITTLE_ENDIAN) { + return (char) ((bytes[byteIndex] & 0xff) | ((bytes[byteIndex + 1] & 0xff) << 8)); + } else { + return (char) (((bytes[byteIndex] & 0xff) << 8) | (bytes[byteIndex + 1] & 0xff)); + } + } + return UNSAFE.getChar(bytes, (long) BYTE_ARRAY_OFFSET + byteIndex); + } + + static void copyCharsToBytes( + char[] chars, int charOffset, byte[] target, int byteOffset, int numBytes) { + if (AndroidSupport.IS_ANDROID) { + int charIndex = charOffset; + if (NativeByteOrder.IS_LITTLE_ENDIAN) { + for (int i = byteOffset, end = byteOffset + numBytes; i < end; i += 2) { + char c = chars[charIndex++]; + target[i] = (byte) c; + target[i + 1] = (byte) (c >>> 8); + } + } else { + for (int i = byteOffset, end = byteOffset + numBytes; i < end; i += 2) { + char c = chars[charIndex++]; + target[i] = (byte) (c >>> 8); + target[i + 1] = (byte) c; + } + } + return; + } + UNSAFE.copyMemory( + chars, + CHAR_ARRAY_OFFSET + ((long) charOffset << 1), + target, + BYTE_ARRAY_OFFSET + byteOffset, + numBytes); + } + + static void putBytes(MemoryBuffer buffer, int writerIndex, byte[] bytes, int numBytes) { + if (AndroidSupport.IS_ANDROID) { + buffer.put(writerIndex, bytes, 0, numBytes); + return; + } + long address = buffer._unsafeWriterAddress() + writerIndex - buffer.writerIndex(); + UNSAFE.copyMemory(bytes, BYTE_ARRAY_OFFSET, null, address, numBytes); + } +} diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/ReplaceResolveSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/ReplaceResolveSerializer.java index 1556ba73ef..55d8e302c8 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/ReplaceResolveSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/ReplaceResolveSerializer.java @@ -20,8 +20,8 @@ package org.apache.fory.serializer; import java.io.Externalizable; -import java.io.ObjectStreamClass; import java.io.Serializable; +import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.reflect.Method; import java.util.HashMap; @@ -37,13 +37,13 @@ import org.apache.fory.logging.LoggerFactory; import org.apache.fory.memory.MemoryBuffer; import org.apache.fory.platform.AndroidSupport; -import org.apache.fory.reflect.ReflectionUtils; +import org.apache.fory.platform.GraalvmSupport; +import org.apache.fory.platform.internal._JDKAccess; import org.apache.fory.resolver.ClassResolver; import org.apache.fory.resolver.TypeInfo; import org.apache.fory.resolver.TypeResolver; import org.apache.fory.util.ExceptionUtils; import org.apache.fory.util.Preconditions; -import org.apache.fory.util.unsafe._JDKAccess; /** * Serializer for class which has jdk `writeReplace`/`readResolve` method defined. This serializer @@ -73,17 +73,12 @@ protected static class ReplaceResolveInfo { private ReplaceResolveInfo(Class cls) { Method writeReplaceMethod, readResolveMethod; - // In JDK17, set private jdk method accessible will fail by default, use ObjectStreamClass - // instead, since it set accessible. - if (AndroidSupport.IS_ANDROID) { + if (AndroidSupport.IS_ANDROID || GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { writeReplaceMethod = JavaSerializer.getWriteReplaceMethod(cls); readResolveMethod = JavaSerializer.getReadResolveMethod(cls); } else if (Serializable.class.isAssignableFrom(cls)) { - ObjectStreamClass objectStreamClass = ObjectStreamClass.lookup(cls); - writeReplaceMethod = - (Method) ReflectionUtils.getObjectFieldValue(objectStreamClass, "writeReplaceMethod"); - readResolveMethod = - (Method) ReflectionUtils.getObjectFieldValue(objectStreamClass, "readResolveMethod"); + writeReplaceMethod = SerializationHookLookup.getWriteReplaceMethod(cls); + readResolveMethod = SerializationHookLookup.getReadResolveMethod(cls); } else { // FIXME class with `writeReplace` method defined should be Serializable, // but hessian ignores this check and many existing system are using hessian, @@ -118,22 +113,11 @@ private ReplaceResolveInfo(Class cls) { makeAccessible(readResolveMethod); } else { MethodHandles.Lookup lookup = _JDKAccess._trustedLookup(declaringClass); - try { - if (writeReplaceMethod != null) { - writeReplaceFunc = - _JDKAccess.makeJDKFunction(lookup, lookup.unreflect(writeReplaceMethod)); - } - if (readResolveMethod != null) { - readResolveFunc = - _JDKAccess.makeJDKFunction(lookup, lookup.unreflect(readResolveMethod)); - } - } catch (Exception e) { - if (writeReplaceMethod != null && !writeReplaceMethod.isAccessible()) { - writeReplaceMethod.setAccessible(true); - } - if (readResolveMethod != null && !readResolveMethod.isAccessible()) { - readResolveMethod.setAccessible(true); - } + if (writeReplaceMethod != null) { + writeReplaceFunc = makeHookFunc(lookup, writeReplaceMethod); + } + if (readResolveMethod != null) { + readResolveFunc = makeHookFunc(lookup, readResolveMethod); } } } @@ -141,6 +125,28 @@ private ReplaceResolveInfo(Class cls) { this.readResolveFunc = readResolveFunc; } + private static Function makeHookFunc(MethodHandles.Lookup lookup, Method method) { + MethodHandle handle; + try { + handle = lookup.unreflect(method); + } catch (IllegalAccessException e) { + throw new ForyException( + "Failed to access Java replacement hook " + + method + + ". " + + _JDKAccess.jdk25AccessMessage(), + e); + } + try { + return _JDKAccess.makeJDKFunction(lookup, handle); + } catch (Throwable e) { + if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { + return new MethodHandleFunction(handle); + } + throw ExceptionUtils.throwException(e); + } + } + private static void makeAccessible(Method method) { if (method == null) { return; @@ -152,6 +158,23 @@ private static void makeAccessible(Method method) { } } + private static final class MethodHandleFunction implements Function { + private final MethodHandle handle; + + private MethodHandleFunction(MethodHandle handle) { + this.handle = handle; + } + + @Override + public Object apply(Object value) { + try { + return handle.invoke(value); + } catch (Throwable e) { + throw ExceptionUtils.throwException(e); + } + } + } + Object writeReplace(Object o) { if (writeReplaceFunc != null) { return writeReplaceFunc.apply(o); @@ -184,22 +207,66 @@ Object readResolve(Object o) { protected static class MethodInfoCache { protected final ReplaceResolveInfo info; + private final TypeResolver typeResolver; + private final Class cls; + private Class serializerClass; - protected Serializer objectSerializer; + protected volatile Serializer objectSerializer; - public MethodInfoCache(ReplaceResolveInfo info) { + public MethodInfoCache(ReplaceResolveInfo info, TypeResolver typeResolver, Class cls) { this.info = info; + this.typeResolver = typeResolver; + this.cls = cls; + } + + public void setSerializerClass(Class serializerClass) { + this.serializerClass = serializerClass; } public void setObjectSerializer(Serializer objectSerializer) { this.objectSerializer = objectSerializer; } + + public Serializer objectSerializer() { + Serializer serializer = objectSerializer; + if (serializer == null) { + synchronized (this) { + serializer = objectSerializer; + if (serializer == null) { + Class sc = serializerClass; + if (sc == null) { + sc = serializerClass = dataSerializerClass(typeResolver, cls, this); + serializer = objectSerializer; + if (serializer != null) { + return serializer; + } + } + serializer = createDataSerializer(typeResolver, cls, sc); + objectSerializer = serializer; + } + } + } + return serializer; + } } static MethodInfoCache newJDKMethodInfoCache(TypeResolver typeResolver, Class cls) { ReplaceResolveInfo replaceResolveInfo = REPLACE_RESOLVE_INFO_CACHE.get(cls, () -> new ReplaceResolveInfo(cls)); - MethodInfoCache methodInfoCache = new MethodInfoCache(replaceResolveInfo); + MethodInfoCache methodInfoCache = new MethodInfoCache(replaceResolveInfo, typeResolver, cls); + ClassResolver classResolver = (ClassResolver) typeResolver; + Serializer registeredSerializer = classResolver.getSerializer(cls, false); + if (registeredSerializer != null + && !(registeredSerializer instanceof ReplaceResolveSerializer)) { + methodInfoCache.setObjectSerializer(registeredSerializer); + return methodInfoCache; + } + methodInfoCache.setSerializerClass(null); + return methodInfoCache; + } + + private static Class dataSerializerClass( + TypeResolver typeResolver, Class cls, MethodInfoCache methodInfoCache) { ClassResolver classResolver = (ClassResolver) typeResolver; Class serializerClass; if (Externalizable.class.isAssignableFrom(cls)) { @@ -214,8 +281,7 @@ static MethodInfoCache newJDKMethodInfoCache(TypeResolver typeResolver, Class } else { serializerClass = typeResolver.getDefaultJDKStreamSerializerType(); } - methodInfoCache.setObjectSerializer(createDataSerializer(typeResolver, cls, serializerClass)); - return methodInfoCache; + return serializerClass; } /** @@ -320,7 +386,7 @@ public void write(WriteContext writeContext, Object value) { protected void writeObject( WriteContext writeContext, Object value, MethodInfoCache jdkMethodInfoCache) { classResolver.writeClassInternal(writeContext, writeTypeInfo); - jdkMethodInfoCache.objectSerializer.write(writeContext, value); + jdkMethodInfoCache.objectSerializer().write(writeContext, value); } @Override @@ -360,7 +426,7 @@ public Object read(ReadContext readContext) { protected Object readObject(ReadContext readContext) { Class cls = classResolver.readClassInternal(readContext); MethodInfoCache jdkMethodInfoCache = getMethodInfoCache(cls); - Object o = jdkMethodInfoCache.objectSerializer.read(readContext); + Object o = jdkMethodInfoCache.objectSerializer().read(readContext); ReplaceResolveInfo replaceResolveInfo = jdkMethodInfoCache.info; if (replaceResolveInfo.readResolveMethod == null) { return o; @@ -372,7 +438,7 @@ protected Object readObject(ReadContext readContext) { public Object copy(CopyContext copyContext, Object originObj) { ReplaceResolveInfo replaceResolveInfo = jdkMethodInfoWriteCache.info; if (replaceResolveInfo.writeReplaceMethod == null) { - return jdkMethodInfoWriteCache.objectSerializer.copy(copyContext, originObj); + return jdkMethodInfoWriteCache.objectSerializer().copy(copyContext, originObj); } Object newObj = originObj; newObj = replaceResolveInfo.writeReplace(newObj); @@ -380,7 +446,7 @@ public Object copy(CopyContext copyContext, Object originObj) { copyContext.reference(originObj, newObj); } MethodInfoCache jdkMethodInfoCache = getMethodInfoCache(newObj.getClass()); - newObj = jdkMethodInfoCache.objectSerializer.copy(copyContext, newObj); + newObj = jdkMethodInfoCache.objectSerializer().copy(copyContext, newObj); replaceResolveInfo = jdkMethodInfoCache.info; if (replaceResolveInfo.readResolveMethod != null) { newObj = replaceResolveInfo.readResolve(newObj); diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/SerializationHookLookup.java b/java/fory-core/src/main/java/org/apache/fory/serializer/SerializationHookLookup.java new file mode 100644 index 0000000000..a07ee825d1 --- /dev/null +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/SerializationHookLookup.java @@ -0,0 +1,221 @@ +/* + * 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.serializer; + +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import org.apache.fory.collection.ClassValueCache; +import org.apache.fory.platform.JdkVersion; +import org.apache.fory.platform.internal._JDKAccess; +import org.apache.fory.util.ExceptionUtils; + +final class SerializationHookLookup { + private SerializationHookLookup() {} + + static MethodHandle readResolveHandle(Class type, Method method) + throws IllegalAccessException { + return _JDKAccess._trustedLookup(type).unreflect(method); + } + + private static final class Methods { + private static final Object REFLECTION_FACTORY; + private static final MethodHandle WRITE_OBJECT; + private static final MethodHandle READ_OBJECT; + private static final MethodHandle READ_OBJECT_NO_DATA; + private static final MethodHandle DEFAULT_READ_OBJECT; + private static final MethodHandle WRITE_REPLACE; + private static final MethodHandle READ_RESOLVE; + + static { + Object reflectionFactory = null; + MethodHandle writeObject = null; + MethodHandle readObject = null; + MethodHandle readObjectNoData = null; + MethodHandle defaultReadObject = null; + MethodHandle writeReplace = null; + MethodHandle readResolve = null; + if (JdkVersion.MAJOR_VERSION < 25) { + try { + Class factoryClass = Class.forName("sun.reflect.ReflectionFactory"); + MethodHandles.Lookup lookup = _JDKAccess._trustedLookup(factoryClass); + MethodHandle getReflectionFactory = + lookup.findStatic( + factoryClass, "getReflectionFactory", MethodType.methodType(factoryClass)); + reflectionFactory = getReflectionFactory.invoke(); + MethodType hookType = MethodType.methodType(MethodHandle.class, Class.class); + writeObject = lookup.findVirtual(factoryClass, "writeObjectForSerialization", hookType); + readObject = lookup.findVirtual(factoryClass, "readObjectForSerialization", hookType); + readObjectNoData = + lookup.findVirtual(factoryClass, "readObjectNoDataForSerialization", hookType); + try { + defaultReadObject = + lookup.findVirtual(factoryClass, "defaultReadObjectForSerialization", hookType); + } catch (NoSuchMethodException | IllegalAccessException e) { + ExceptionUtils.ignore(e); + } + writeReplace = lookup.findVirtual(factoryClass, "writeReplaceForSerialization", hookType); + readResolve = lookup.findVirtual(factoryClass, "readResolveForSerialization", hookType); + } catch (Throwable e) { + ExceptionUtils.ignore(e); + } + } + REFLECTION_FACTORY = reflectionFactory; + WRITE_OBJECT = writeObject; + READ_OBJECT = readObject; + READ_OBJECT_NO_DATA = readObjectNoData; + DEFAULT_READ_OBJECT = defaultReadObject; + WRITE_REPLACE = writeReplace; + READ_RESOLVE = readResolve; + } + } + + private static MethodHandle getHandle(Class type, MethodHandle factoryMethod) { + if (Methods.REFLECTION_FACTORY == null || factoryMethod == null) { + return null; + } + try { + return (MethodHandle) factoryMethod.invoke(Methods.REFLECTION_FACTORY, type); + } catch (Throwable e) { + ExceptionUtils.ignore(e); + return null; + } + } + + private static Method getMethod(Class type, MethodHandle factoryMethod) { + MethodHandle handle = getHandle(type, factoryMethod); + return handle == null ? null : MethodHandles.reflectAs(Method.class, handle); + } + + private static final ClassValueCache directMethodsCache = + ClassValueCache.newClassKeyCache(32); + + private static DirectMethods directMethods(Class type) { + // JavaSerializer intentionally supports non-Serializable compatibility hooks. Serializable + // stream hooks must keep ObjectStreamClass inheritance rules, especially for private + // superclass writeReplace/readResolve methods. + return directMethodsCache.get(type, () -> new DirectMethods(type)); + } + + private static final class DirectMethods { + private final Method writeObject; + private final Method readObject; + private final Method readObjectNoData; + private final Method writeReplace; + private final Method readResolve; + + private DirectMethods(Class type) { + writeObject = getPrivateMethod(type, "writeObject", void.class, ObjectOutputStream.class); + readObject = getPrivateMethod(type, "readObject", void.class, ObjectInputStream.class); + readObjectNoData = getPrivateMethod(type, "readObjectNoData", void.class); + writeReplace = getInheritableObjectMethod(type, "writeReplace"); + readResolve = getInheritableObjectMethod(type, "readResolve"); + } + } + + private static Method getPrivateMethod( + Class type, String methodName, Class returnType, Class... parameterTypes) { + try { + Method method = type.getDeclaredMethod(methodName, parameterTypes); + int modifiers = method.getModifiers(); + if (Modifier.isPrivate(modifiers) + && !Modifier.isStatic(modifiers) + && method.getReturnType() == returnType) { + return method; + } + } catch (NoSuchMethodException | SecurityException e) { + ExceptionUtils.ignore(e); + } + return null; + } + + private static Method getInheritableObjectMethod(Class type, String methodName) { + Class cls = type; + while (cls != null) { + try { + Method method = cls.getDeclaredMethod(methodName); + int modifiers = method.getModifiers(); + if (!Modifier.isStatic(modifiers) + && method.getParameterTypes().length == 0 + && method.getReturnType() == Object.class + && isInheritable(type, cls, modifiers)) { + return method; + } + return null; + } catch (NoSuchMethodException e) { + ExceptionUtils.ignore(e); + } catch (SecurityException e) { + return null; + } + cls = cls.getSuperclass(); + } + return null; + } + + private static boolean isInheritable(Class type, Class declaringClass, int modifiers) { + if (Modifier.isPrivate(modifiers)) { + return type == declaringClass; + } + if (Modifier.isPublic(modifiers) || Modifier.isProtected(modifiers)) { + return true; + } + return type.getClassLoader() == declaringClass.getClassLoader() + && packageName(type).equals(packageName(declaringClass)); + } + + private static String packageName(Class type) { + String className = type.getName(); + int packageEnd = className.lastIndexOf('.'); + return packageEnd < 0 ? "" : className.substring(0, packageEnd); + } + + static Method getWriteObjectMethod(Class type) { + Method method = getMethod(type, Methods.WRITE_OBJECT); + return method == null ? directMethods(type).writeObject : method; + } + + static Method getReadObjectMethod(Class type) { + Method method = getMethod(type, Methods.READ_OBJECT); + return method == null ? directMethods(type).readObject : method; + } + + static Method getReadObjectNoDataMethod(Class type) { + Method method = getMethod(type, Methods.READ_OBJECT_NO_DATA); + return method == null ? directMethods(type).readObjectNoData : method; + } + + static MethodHandle getDefaultReadObjectHandle(Class type) { + return getHandle(type, Methods.DEFAULT_READ_OBJECT); + } + + static Method getWriteReplaceMethod(Class type) { + Method method = getMethod(type, Methods.WRITE_REPLACE); + return method == null ? directMethods(type).writeReplace : method; + } + + static Method getReadResolveMethod(Class type) { + Method method = getMethod(type, Methods.READ_RESOLVE); + return method == null ? directMethods(type).readResolve : method; + } +} diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/SerializedLambdaSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/SerializedLambdaSerializer.java index 0962e0710b..0c225bddaf 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/SerializedLambdaSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/SerializedLambdaSerializer.java @@ -31,7 +31,6 @@ import org.apache.fory.platform.AndroidSupport; import org.apache.fory.resolver.TypeResolver; import org.apache.fory.util.Preconditions; -import org.apache.fory.util.unsafe._JDKAccess; /** * Serializer for {@link SerializedLambda}. It writes the JDK lambda payload through the public @@ -54,7 +53,7 @@ public class SerializedLambdaSerializer extends Serializer { Preconditions.checkNotNull( readResolveMethod, "Missing readResolve for " + SERIALIZED_LAMBDA); READ_RESOLVE_HANDLE = - _JDKAccess._trustedLookup(SERIALIZED_LAMBDA).unreflect(readResolveMethod); + SerializationHookLookup.readResolveHandle(SERIALIZED_LAMBDA, readResolveMethod); } catch (IllegalAccessException e) { throw new ForyException(e); } diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/Serializers.java b/java/fory-core/src/main/java/org/apache/fory/serializer/Serializers.java index 4bff2392f7..c0e0cf5b33 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/Serializers.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/Serializers.java @@ -48,9 +48,11 @@ import org.apache.fory.context.ReadContext; import org.apache.fory.context.WriteContext; import org.apache.fory.memory.MemoryBuffer; +import org.apache.fory.memory.MemoryUtils; import org.apache.fory.meta.TypeDef; import org.apache.fory.platform.AndroidSupport; import org.apache.fory.platform.GraalvmSupport; +import org.apache.fory.platform.internal._JDKAccess; import org.apache.fory.reflect.ReflectionUtils; import org.apache.fory.resolver.ClassResolver; import org.apache.fory.resolver.TypeInfo; @@ -65,8 +67,6 @@ import org.apache.fory.serializer.scala.SingletonMapSerializer; import org.apache.fory.serializer.scala.SingletonObjectSerializer; import org.apache.fory.util.ExceptionUtils; -import org.apache.fory.util.StringUtils; -import org.apache.fory.util.unsafe._JDKAccess; /** Serialization utils and common serializers. */ @SuppressWarnings({"rawtypes", "unchecked"}) @@ -273,7 +273,55 @@ private static Serializer createSerializer( return createSerializerReflectively(typeResolver, type, serializerClass); } try { + // Public serializers in exported JPMS packages should not require private package opens. + Serializer serializer = + tryCreateSerializer( + typeResolver, type, serializerClass, MethodHandles.publicLookup(), false); + if (serializer != null) { + return serializer; + } + if (!hasSupportedConstructor(serializerClass)) { + throw new IllegalArgumentException( + "Serializer " + + serializerClass.getName() + + " doesn't define a supported constructor for " + + type); + } MethodHandles.Lookup lookup = _JDKAccess._trustedLookup(serializerClass); + return tryCreateSerializer(typeResolver, type, serializerClass, lookup, true); + } catch (Throwable t) { + ExceptionUtils.throwException(t); + throw new IllegalStateException("unreachable"); + } + } + + private static boolean hasSupportedConstructor(Class serializerClass) { + return hasConstructor(serializerClass, TypeResolver.class, Class.class) + || hasConstructor(serializerClass, TypeResolver.class) + || hasConstructor(serializerClass, Config.class, Class.class) + || hasConstructor(serializerClass, Config.class) + || hasConstructor(serializerClass, Class.class) + || hasConstructor(serializerClass); + } + + private static boolean hasConstructor( + Class serializerClass, Class... parameterTypes) { + try { + serializerClass.getDeclaredConstructor(parameterTypes); + return true; + } catch (NoSuchMethodException e) { + return false; + } + } + + private static Serializer tryCreateSerializer( + TypeResolver typeResolver, + Class type, + Class serializerClass, + MethodHandles.Lookup lookup, + boolean checked) + throws Throwable { + try { Config config = typeResolver.getConfig(); try { MethodHandle ctr = lookup.findConstructor(serializerClass, SIG1); @@ -281,6 +329,11 @@ private static Serializer createSerializer( return (Serializer) ctr.invoke(typeResolver, type); } catch (NoSuchMethodException e) { ExceptionUtils.ignore(e); + } catch (IllegalAccessException e) { + if (!checked) { + return null; + } + throw e; } try { MethodHandle ctr = lookup.findConstructor(serializerClass, SIG2); @@ -288,6 +341,11 @@ private static Serializer createSerializer( return (Serializer) ctr.invoke(typeResolver); } catch (NoSuchMethodException e) { ExceptionUtils.ignore(e); + } catch (IllegalAccessException e) { + if (!checked) { + return null; + } + throw e; } try { MethodHandle ctr = lookup.findConstructor(serializerClass, SIG3); @@ -295,6 +353,11 @@ private static Serializer createSerializer( return (Serializer) ctr.invoke(config, type); } catch (NoSuchMethodException e) { ExceptionUtils.ignore(e); + } catch (IllegalAccessException e) { + if (!checked) { + return null; + } + throw e; } try { MethodHandle ctr = lookup.findConstructor(serializerClass, SIG4); @@ -302,6 +365,11 @@ private static Serializer createSerializer( return (Serializer) ctr.invoke(config); } catch (NoSuchMethodException e) { ExceptionUtils.ignore(e); + } catch (IllegalAccessException e) { + if (!checked) { + return null; + } + throw e; } try { MethodHandle ctr = lookup.findConstructor(serializerClass, SIG5); @@ -309,13 +377,38 @@ private static Serializer createSerializer( return (Serializer) ctr.invoke(type); } catch (NoSuchMethodException e) { ExceptionUtils.ignore(e); + } catch (IllegalAccessException e) { + if (!checked) { + return null; + } + throw e; + } + try { + MethodHandle ctr = lookup.findConstructor(serializerClass, SIG6); + CTR_MAP.put(serializerClass, Tuple2.of(SIG6, ctr)); + return (Serializer) ctr.invoke(); + } catch (NoSuchMethodException e) { + ExceptionUtils.ignore(e); + } catch (IllegalAccessException e) { + if (!checked) { + return null; + } + throw e; + } + if (!checked) { + return null; + } + MethodHandle ctr = ReflectionUtils.getCtrHandle(serializerClass, true); + if (ctr == null) { + return null; } - MethodHandle ctr = ReflectionUtils.getCtrHandle(serializerClass); CTR_MAP.put(serializerClass, Tuple2.of(SIG6, ctr)); return (Serializer) ctr.invoke(); - } catch (Throwable t) { - ExceptionUtils.throwException(t); - throw new IllegalStateException("unreachable"); + } catch (IllegalAccessException e) { + if (!checked) { + return null; + } + throw e; } } @@ -404,7 +497,7 @@ public static T read(ReadContext readContext, Serializer serializer) { private static final Function GET_VALUE; static { - if (AndroidSupport.IS_ANDROID) { + if (!MemoryUtils.JDK_LANG_FIELD_ACCESS) { GET_VALUE = null; GET_CODER = null; } else { @@ -437,7 +530,7 @@ public void write(WriteContext writeContext, T value) { stringSerializer.writeString(buffer, value.toString()); return; } - if (AndroidSupport.IS_ANDROID) { + if (!MemoryUtils.JDK_LANG_FIELD_ACCESS) { stringSerializer.writeString(buffer, value.toString()); return; } @@ -456,7 +549,7 @@ public void write(WriteContext writeContext, T value) { buffer.writeBytes(v, 0, bytesLen); } else { char[] v = (char[]) GET_VALUE.apply(value); - if (StringUtils.isLatin(v)) { + if (StringEncodingUtils.isLatin(v)) { stringSerializer.writeCharsLatin1(buffer, v, value.length()); } else { stringSerializer.writeCharsUTF16(buffer, v, value.length()); diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/SlicedStringUtil.java b/java/fory-core/src/main/java/org/apache/fory/serializer/SlicedStringUtil.java deleted file mode 100644 index d8ecbee4ce..0000000000 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/SlicedStringUtil.java +++ /dev/null @@ -1,296 +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.serializer; - -import org.apache.fory.memory.LittleEndian; -import org.apache.fory.memory.MemoryBuffer; -import org.apache.fory.memory.NativeByteOrder; -import org.apache.fory.platform.UnsafeOps; -import org.apache.fory.util.MathUtils; -import org.apache.fory.util.StringEncodingUtils; -import org.apache.fory.util.StringUtils; - -final class SlicedStringUtil { - private static final byte LATIN1 = 0; - private static final byte UTF16 = 1; - private static final byte UTF8 = 2; - - private SlicedStringUtil() {} - - static void writeCharsLatin1WithOffset( - StringSerializer serializer, MemoryBuffer buffer, char[] chars, int offset, int count) { - int writerIndex = buffer.writerIndex(); - long header = ((long) count << 2) | LATIN1; - buffer.ensure(writerIndex + 5 + count); - byte[] targetArray = buffer.getHeapMemory(); - if (targetArray != null) { - final int targetIndex = buffer._unsafeHeapWriterIndex(); - int arrIndex = targetIndex; - arrIndex += LittleEndian.putVarUint36Small(targetArray, arrIndex, header); - writerIndex += arrIndex - targetIndex; - for (int i = 0; i < count; i++) { - targetArray[arrIndex + i] = (byte) chars[offset + i]; - } - } else { - writerIndex += buffer._unsafePutVarUint36Small(writerIndex, header); - final byte[] tmpArray = serializer.getByteArray(count); - for (int i = 0; i < count; i++) { - tmpArray[i] = (byte) chars[offset + i]; - } - buffer.put(writerIndex, tmpArray, 0, count); - } - writerIndex += count; - buffer._unsafeWriterIndex(writerIndex); - } - - static void writeCharsUTF16WithOffset( - StringSerializer serializer, MemoryBuffer buffer, char[] chars, int offset, int count) { - int numBytes = MathUtils.doubleExact(count); - int writerIndex = buffer.writerIndex(); - long header = ((long) numBytes << 2) | UTF16; - buffer.ensure(writerIndex + 5 + numBytes); - final byte[] targetArray = buffer.getHeapMemory(); - if (targetArray != null) { - final int targetIndex = buffer._unsafeHeapWriterIndex(); - int arrIndex = targetIndex; - arrIndex += LittleEndian.putVarUint36Small(targetArray, arrIndex, header); - writerIndex += arrIndex - targetIndex + numBytes; - if (NativeByteOrder.IS_LITTLE_ENDIAN) { - // FIXME JDK11 utf16 string uses little-endian order. - UnsafeOps.UNSAFE.copyMemory( - chars, - UnsafeOps.CHAR_ARRAY_OFFSET + ((long) offset << 1), - targetArray, - UnsafeOps.BYTE_ARRAY_OFFSET + arrIndex, - numBytes); - } else { - writeCharsUTF16BEToHeap(chars, offset, arrIndex, numBytes, targetArray); - } - } else { - writerIndex += buffer._unsafePutVarUint36Small(writerIndex, header); - if (NativeByteOrder.IS_LITTLE_ENDIAN) { - writerIndex = - offHeapWriteCharsUTF16WithOffset( - serializer, buffer, chars, offset, writerIndex, numBytes); - } else { - writerIndex = - offHeapWriteCharsUTF16BEWithOffset( - serializer, buffer, chars, offset, writerIndex, numBytes); - } - } - buffer._unsafeWriterIndex(writerIndex); - } - - static void writeCharsUTF8WithOffset( - StringSerializer serializer, MemoryBuffer buffer, char[] chars, int offset, int count) { - int estimateMaxBytes = count * 3; - int approxNumBytes = (int) (count * 1.5) + 1; - int writerIndex = buffer.writerIndex(); - buffer.ensure(writerIndex + 9 + estimateMaxBytes); - byte[] targetArray = buffer.getHeapMemory(); - if (targetArray != null) { - int targetIndex = buffer._unsafeHeapWriterIndex(); - int headerPos = targetIndex; - int arrIndex = targetIndex; - long header = ((long) approxNumBytes << 2) | UTF8; - int headerBytesWritten = LittleEndian.putVarUint36Small(targetArray, arrIndex, header); - arrIndex += headerBytesWritten; - writerIndex += headerBytesWritten; - targetIndex = - StringEncodingUtils.convertUTF16ToUTF8(chars, offset, count, targetArray, arrIndex); - byte stashedByte = targetArray[arrIndex]; - int written = targetIndex - arrIndex; - header = ((long) written << 2) | UTF8; - int diff = - LittleEndian.putVarUint36Small(targetArray, headerPos, header) - headerBytesWritten; - if (diff != 0) { - handleWriteCharsUTF8UnalignedHeaderBytes(targetArray, arrIndex, diff, written, stashedByte); - } - buffer._unsafeWriterIndex(writerIndex + written + diff); - } else { - final byte[] tmpArray = serializer.getByteArray(estimateMaxBytes); - int written = StringEncodingUtils.convertUTF16ToUTF8(chars, offset, count, tmpArray, 0); - long header = ((long) written << 2) | UTF8; - writerIndex += buffer._unsafePutVarUint36Small(writerIndex, header); - buffer.put(writerIndex, tmpArray, 0, written); - buffer._unsafeWriterIndex(writerIndex + written); - } - } - - static void writeCharsUTF8PerfOptimizedWithOffset( - StringSerializer serializer, MemoryBuffer buffer, char[] chars, int offset, int count) { - int estimateMaxBytes = count * 3; - int numBytes = MathUtils.doubleExact(count); - int writerIndex = buffer.writerIndex(); - long header = ((long) numBytes << 2) | UTF8; - buffer.ensure(writerIndex + 9 + estimateMaxBytes); - byte[] targetArray = buffer.getHeapMemory(); - if (targetArray != null) { - int targetIndex = buffer._unsafeHeapWriterIndex(); - int arrIndex = targetIndex; - arrIndex += LittleEndian.putVarUint36Small(targetArray, arrIndex, header); - writerIndex += arrIndex - targetIndex; - targetIndex = - StringEncodingUtils.convertUTF16ToUTF8(chars, offset, count, targetArray, arrIndex + 4); - int written = targetIndex - arrIndex - 4; - buffer._unsafePutInt32(writerIndex, written); - buffer._unsafeWriterIndex(writerIndex + 4 + written); - } else { - final byte[] tmpArray = serializer.getByteArray(estimateMaxBytes); - int written = StringEncodingUtils.convertUTF16ToUTF8(chars, offset, count, tmpArray, 0); - writerIndex += buffer._unsafePutVarUint36Small(writerIndex, header); - buffer._unsafePutInt32(writerIndex, written); - writerIndex += 4; - buffer.put(writerIndex, tmpArray, 0, written); - buffer._unsafeWriterIndex(writerIndex + written); - } - } - - static boolean isLatin(char[] chars, int offset, int count) { - int end = offset + count; - int vectorizedChars = count & ~3; - int vectorEnd = offset + vectorizedChars; - long byteOffset = UnsafeOps.CHAR_ARRAY_OFFSET + ((long) offset << 1); - long endOffset = UnsafeOps.CHAR_ARRAY_OFFSET + ((long) vectorEnd << 1); - for (long off = byteOffset; off < endOffset; off += 8) { - long multiChars = UnsafeOps.getLong(chars, off); - if ((multiChars & StringUtils.MULTI_CHARS_NON_LATIN_MASK) != 0) { - return false; - } - } - for (int i = vectorEnd; i < end; i++) { - if (chars[i] > 0xFF) { - return false; - } - } - return true; - } - - static byte bestCoder(char[] chars, int offset, int count) { - int sampleNum = Math.min(64, count); - int vectorizedLen = sampleNum >> 2; - int vectorizedChars = vectorizedLen << 2; - long byteOffset = UnsafeOps.CHAR_ARRAY_OFFSET + ((long) offset << 1); - long endOffset = byteOffset + ((long) vectorizedChars << 1); - int asciiCount = 0; - int latin1Count = 0; - int charOffset = offset; - for (long off = byteOffset; off < endOffset; off += 8, charOffset += 4) { - long multiChars = UnsafeOps.getLong(chars, off); - if ((multiChars & StringUtils.MULTI_CHARS_NON_ASCII_MASK) == 0) { - latin1Count += 4; - asciiCount += 4; - } else if ((multiChars & StringUtils.MULTI_CHARS_NON_LATIN_MASK) == 0) { - latin1Count += 4; - for (int i = 0; i < 4; ++i) { - if (chars[charOffset + i] < 0x80) { - asciiCount++; - } - } - } else { - for (int i = 0; i < 4; ++i) { - char c = chars[charOffset + i]; - if (c < 0x80) { - latin1Count++; - asciiCount++; - } else if (c <= 0xFF) { - latin1Count++; - } - } - } - } - - for (int i = vectorizedChars; i < sampleNum; i++) { - char c = chars[offset + i]; - if (c < 0x80) { - latin1Count++; - asciiCount++; - } else if (c <= 0xFF) { - latin1Count++; - } - } - - if (latin1Count == count || (latin1Count == sampleNum && isLatin(chars, offset, count))) { - return LATIN1; - } else if (asciiCount >= sampleNum * 0.5) { - return UTF8; - } else { - return UTF16; - } - } - - private static void handleWriteCharsUTF8UnalignedHeaderBytes( - byte[] targetArray, int arrIndex, int diff, int written, byte stashed) { - if (diff == 1) { - System.arraycopy(targetArray, arrIndex + 1, targetArray, arrIndex + 2, written - 1); - targetArray[arrIndex + 1] = stashed; - } else { - System.arraycopy(targetArray, arrIndex, targetArray, arrIndex - 1, written); - } - } - - private static void writeCharsUTF16BEToHeap( - char[] chars, int offset, int arrIndex, int numBytes, byte[] targetArray) { - int charIndex = offset; - for (int i = arrIndex, end = i + numBytes; i < end; i += 2) { - char c = chars[charIndex++]; - targetArray[i] = (byte) c; - targetArray[i + 1] = (byte) (c >>> 8); - } - } - - private static int offHeapWriteCharsUTF16WithOffset( - StringSerializer serializer, - MemoryBuffer buffer, - char[] chars, - int offset, - int writerIndex, - int numBytes) { - byte[] tmpArray = serializer.getByteArray(numBytes); - UnsafeOps.UNSAFE.copyMemory( - chars, - UnsafeOps.CHAR_ARRAY_OFFSET + ((long) offset << 1), - tmpArray, - UnsafeOps.BYTE_ARRAY_OFFSET, - numBytes); - buffer.put(writerIndex, tmpArray, 0, numBytes); - writerIndex += numBytes; - return writerIndex; - } - - private static int offHeapWriteCharsUTF16BEWithOffset( - StringSerializer serializer, - MemoryBuffer buffer, - char[] chars, - int offset, - int writerIndex, - int numBytes) { - byte[] tmpArray = serializer.getByteArray(numBytes); - int charIndex = offset; - for (int i = 0; i < numBytes; i += 2) { - char c = chars[charIndex++]; - tmpArray[i] = (byte) c; - tmpArray[i + 1] = (byte) (c >>> 8); - } - buffer.put(writerIndex, tmpArray, 0, numBytes); - writerIndex += numBytes; - return writerIndex; - } -} diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/StaticGeneratedStructSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/StaticGeneratedStructSerializer.java index e3990ac7c7..81091c85d7 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/StaticGeneratedStructSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/StaticGeneratedStructSerializer.java @@ -227,6 +227,52 @@ public final int[] localFieldIds( return ids; } + protected final long[] buildConstructorFieldBits(int size, int[] indexes) { + if (indexes == null) { + return null; + } + long[] bits = newFieldBits(size); + for (int index : indexes) { + if (index >= 0) { + markField(bits, index); + } + } + return bits; + } + + protected static long[] newFieldBits(int size) { + return new long[(size + Long.SIZE - 1) / Long.SIZE]; + } + + protected static boolean hasField(long[] bits, int fieldId) { + return (bits[fieldId / Long.SIZE] & (1L << (fieldId % Long.SIZE))) != 0; + } + + protected static void markField(long[] bits, int fieldId) { + bits[fieldId / Long.SIZE] |= 1L << (fieldId % Long.SIZE); + } + + protected static int countConstructorFields(long[] constructorFieldBits) { + int count = 0; + for (long constructorFields : constructorFieldBits) { + count += Long.bitCount(constructorFields); + } + return count; + } + + protected final void setGeneratedFieldValue( + Object targetObject, SerializationFieldInfo fieldInfo, Object fieldValue) { + if (fieldInfo.fieldAccessor != null) { + fieldInfo.fieldAccessor.putObject(targetObject, fieldValue); + return; + } + if (fieldInfo.fieldConverter != null) { + fieldInfo.fieldConverter.set(targetObject, fieldValue); + return; + } + throw new ForyException("Generated field " + fieldInfo.getName() + " is not writable"); + } + protected final void writeBuildInFieldValue( WriteContext writeContext, SerializationFieldInfo fieldInfo, Object fieldValue) { // Some schema-built-in fields still use container-shaped Java accessors, such as diff --git a/java/fory-core/src/main/java/org/apache/fory/util/StringEncodingUtils.java b/java/fory-core/src/main/java/org/apache/fory/serializer/StringEncodingUtils.java similarity index 59% rename from java/fory-core/src/main/java/org/apache/fory/util/StringEncodingUtils.java rename to java/fory-core/src/main/java/org/apache/fory/serializer/StringEncodingUtils.java index e409dd3bff..186d4e2455 100644 --- a/java/fory-core/src/main/java/org/apache/fory/util/StringEncodingUtils.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/StringEncodingUtils.java @@ -17,22 +17,73 @@ * under the License. */ -package org.apache.fory.util; +package org.apache.fory.serializer; import static org.apache.fory.util.StringUtils.MULTI_CHARS_NON_ASCII_MASK; +import static org.apache.fory.util.StringUtils.MULTI_CHARS_NON_LATIN_MASK; +import org.apache.fory.memory.LittleEndian; +import org.apache.fory.memory.MemoryBuffer; import org.apache.fory.memory.NativeByteOrder; -import org.apache.fory.platform.UnsafeOps; +import org.apache.fory.util.MathUtils; /** String Encoding Utils. */ public class StringEncodingUtils { + private static final byte LATIN1 = 0; + private static final byte UTF16 = 1; + private static final byte UTF8 = 2; + + public static boolean isLatin(char[] chars) { + return isLatin(chars, 0); + } + + public static boolean isLatin(char[] chars, int start) { + if (start > chars.length) { + return false; + } + int numChars = chars.length; + int charIndex = start; + while (charIndex + 4 <= numChars) { + // Check 4 chars in a vectorized way. See CompressStringSuite.latinSuperWordCheck. + long multiChars = PlatformStringUtils.getCharsLong(chars, charIndex); + if ((multiChars & MULTI_CHARS_NON_LATIN_MASK) != 0) { + return false; + } + charIndex += 4; + } + for (int i = charIndex; i < numChars; i++) { + if (chars[i] > 0xFF) { + return false; + } + } + return true; + } + + static boolean isLatin(char[] chars, int offset, int count) { + int end = offset + count; + int vectorizedChars = count & ~3; + int vectorEnd = offset + vectorizedChars; + for (int charIndex = offset; charIndex < vectorEnd; charIndex += 4) { + long multiChars = PlatformStringUtils.getCharsLong(chars, charIndex); + if ((multiChars & MULTI_CHARS_NON_LATIN_MASK) != 0) { + return false; + } + } + for (int i = vectorEnd; i < end; i++) { + if (chars[i] > 0xFF) { + return false; + } + } + return true; + } /** A fast convert algorithm to convert an utf16 char array into an utf8 byte array. */ public static int convertUTF16ToUTF8(char[] src, byte[] dst, int dp) { int numChars = src.length; - for (int charOffset = 0, arrayOffset = UnsafeOps.CHAR_ARRAY_OFFSET; charOffset < numChars; ) { + for (int charOffset = 0; charOffset < numChars; ) { if (charOffset + 4 <= numChars - && (UnsafeOps.getLong(src, arrayOffset) & MULTI_CHARS_NON_ASCII_MASK) == 0) { + && (PlatformStringUtils.getCharsLong(src, charOffset) & MULTI_CHARS_NON_ASCII_MASK) + == 0) { // ascii only dst[dp] = (byte) src[charOffset]; dst[dp + 1] = (byte) src[charOffset + 1]; @@ -40,10 +91,8 @@ public static int convertUTF16ToUTF8(char[] src, byte[] dst, int dp) { dst[dp + 3] = (byte) src[charOffset + 3]; dp += 4; charOffset += 4; - arrayOffset += 8; } else { char c = src[charOffset++]; - arrayOffset += 2; if (c < 0x80) { dst[dp++] = (byte) c; } else if (c < 0x800) { @@ -54,7 +103,6 @@ public static int convertUTF16ToUTF8(char[] src, byte[] dst, int dp) { utf8ToChar2(src, charOffset, c, dst, dp); dp += 4; charOffset++; - arrayOffset += 2; } else { dst[dp] = (byte) (0xe0 | ((c >> 12))); dst[dp + 1] = (byte) (0x80 | ((c >> 6) & 0x3f)); @@ -69,20 +117,18 @@ public static int convertUTF16ToUTF8(char[] src, byte[] dst, int dp) { /** A fast convert algorithm to convert an utf16 char array slice into an utf8 byte array. */ public static int convertUTF16ToUTF8(char[] src, int offset, int len, byte[] dst, int dp) { int end = offset + len; - for (int charOffset = offset, arrayOffset = UnsafeOps.CHAR_ARRAY_OFFSET + (offset << 1); - charOffset < end; ) { + for (int charOffset = offset; charOffset < end; ) { if (charOffset + 4 <= end - && (UnsafeOps.getLong(src, arrayOffset) & MULTI_CHARS_NON_ASCII_MASK) == 0) { + && (PlatformStringUtils.getCharsLong(src, charOffset) & MULTI_CHARS_NON_ASCII_MASK) + == 0) { dst[dp] = (byte) src[charOffset]; dst[dp + 1] = (byte) src[charOffset + 1]; dst[dp + 2] = (byte) src[charOffset + 2]; dst[dp + 3] = (byte) src[charOffset + 3]; dp += 4; charOffset += 4; - arrayOffset += 8; } else { char c = src[charOffset++]; - arrayOffset += 2; if (c < 0x80) { dst[dp++] = (byte) c; } else if (c < 0x800) { @@ -96,7 +142,6 @@ public static int convertUTF16ToUTF8(char[] src, int offset, int len, byte[] dst utf8ToChar2(src, charOffset, c, dst, dp); dp += 4; charOffset++; - arrayOffset += 2; } else { dst[dp] = (byte) (0xe0 | ((c >> 12))); dst[dp + 1] = (byte) (0x80 | ((c >> 6) & 0x3f)); @@ -113,9 +158,7 @@ public static int convertUTF16ToUTF8(byte[] src, byte[] dst, int dp) { int numBytes = src.length; for (int offset = 0; offset < numBytes; ) { if (offset + 8 <= numBytes - && (UnsafeOps.getLong(src, UnsafeOps.BYTE_ARRAY_OFFSET + offset) - & MULTI_CHARS_NON_ASCII_MASK) - == 0) { + && (PlatformStringUtils.getBytesLong(src, offset) & MULTI_CHARS_NON_ASCII_MASK) == 0) { // ascii only if (NativeByteOrder.IS_LITTLE_ENDIAN) { dst[dp] = src[offset]; @@ -131,7 +174,7 @@ public static int convertUTF16ToUTF8(byte[] src, byte[] dst, int dp) { dp += 4; offset += 8; } else { - char c = UnsafeOps.getChar(src, UnsafeOps.BYTE_ARRAY_OFFSET + offset); + char c = PlatformStringUtils.getBytesChar(src, offset); offset += 2; if (c < 0x80) { @@ -169,8 +212,7 @@ public static int convertUTF8ToUTF16(byte[] src, int offset, int len, byte[] dst while (offset < end) { if (offset + 8 <= end - && (UnsafeOps.getLong(src, UnsafeOps.BYTE_ARRAY_OFFSET + offset) & 0x8080808080808080L) - == 0) { + && (PlatformStringUtils.getBytesLong(src, offset) & 0x8080808080808080L) == 0) { // ascii only if (NativeByteOrder.IS_LITTLE_ENDIAN) { dst[dp] = src[offset]; @@ -292,8 +334,7 @@ public static int convertUTF8ToUTF16(byte[] src, int offset, int len, char[] dst int dp = 0; while (offset < end) { if (offset + 8 <= end - && (UnsafeOps.getLong(src, UnsafeOps.BYTE_ARRAY_OFFSET + offset) & 0x8080808080808080L) - == 0) { + && (PlatformStringUtils.getBytesLong(src, offset) & 0x8080808080808080L) == 0) { // ascii only dst[dp] = (char) src[offset]; dst[dp + 1] = (char) src[offset + 1]; @@ -410,8 +451,7 @@ private static void utf8ToChar2( char d; if (c > Character.MAX_HIGH_SURROGATE || numBytes - offset < 1 - || (d = UnsafeOps.getChar(src, UnsafeOps.BYTE_ARRAY_OFFSET + offset)) - < Character.MIN_LOW_SURROGATE + || (d = PlatformStringUtils.getBytesChar(src, offset)) < Character.MIN_LOW_SURROGATE || d > Character.MAX_LOW_SURROGATE) { throw new RuntimeException("malformed input off : " + offset); } @@ -466,8 +506,7 @@ public static int convertUTF8ToLatin1(byte[] src, int offset, int length, byte[] while (offset < end) { // Vectorized ASCII fast path if (offset + 8 <= end - && (UnsafeOps.getLong(src, UnsafeOps.BYTE_ARRAY_OFFSET + offset) & 0x8080808080808080L) - == 0) { + && (PlatformStringUtils.getBytesLong(src, offset) & 0x8080808080808080L) == 0) { // 8 ASCII bytes - direct copy dst[dstPos] = src[offset]; dst[dstPos + 1] = src[offset + 1]; @@ -508,7 +547,7 @@ public static boolean isUTF8WithinAscii(byte[] bytes, int offset, int length) { // Check 8 bytes at a time for (int i = offset; i < vectorizedEnd; i += 8) { - if ((UnsafeOps.getLong(bytes, UnsafeOps.BYTE_ARRAY_OFFSET + i) & 0x8080808080808080L) != 0) { + if ((PlatformStringUtils.getBytesLong(bytes, i) & 0x8080808080808080L) != 0) { return false; } } @@ -522,4 +561,231 @@ public static boolean isUTF8WithinAscii(byte[] bytes, int offset, int length) { return true; } + + static void writeCharsLatin1WithOffset( + StringSerializer serializer, MemoryBuffer buffer, char[] chars, int offset, int count) { + int writerIndex = buffer.writerIndex(); + long header = ((long) count << 2) | LATIN1; + buffer.ensure(writerIndex + 5 + count); + byte[] targetArray = buffer.getHeapMemory(); + if (targetArray != null) { + final int targetIndex = buffer._unsafeHeapWriterIndex(); + int arrIndex = targetIndex; + arrIndex += LittleEndian.putVarUint36Small(targetArray, arrIndex, header); + writerIndex += arrIndex - targetIndex; + for (int i = 0; i < count; i++) { + targetArray[arrIndex + i] = (byte) chars[offset + i]; + } + } else { + writerIndex += buffer._unsafePutVarUint36Small(writerIndex, header); + final byte[] tmpArray = serializer.getByteArray(count); + for (int i = 0; i < count; i++) { + tmpArray[i] = (byte) chars[offset + i]; + } + buffer.put(writerIndex, tmpArray, 0, count); + } + writerIndex += count; + buffer._unsafeWriterIndex(writerIndex); + } + + static void writeCharsUTF16WithOffset( + StringSerializer serializer, MemoryBuffer buffer, char[] chars, int offset, int count) { + int numBytes = MathUtils.doubleExact(count); + int writerIndex = buffer.writerIndex(); + long header = ((long) numBytes << 2) | UTF16; + buffer.ensure(writerIndex + 5 + numBytes); + final byte[] targetArray = buffer.getHeapMemory(); + if (targetArray != null) { + final int targetIndex = buffer._unsafeHeapWriterIndex(); + int arrIndex = targetIndex; + arrIndex += LittleEndian.putVarUint36Small(targetArray, arrIndex, header); + writerIndex += arrIndex - targetIndex + numBytes; + if (NativeByteOrder.IS_LITTLE_ENDIAN) { + // FIXME JDK11 utf16 string uses little-endian order. + PlatformStringUtils.copyCharsToBytes(chars, offset, targetArray, arrIndex, numBytes); + } else { + writeCharsUTF16BEToHeap(chars, offset, arrIndex, numBytes, targetArray); + } + } else { + writerIndex += buffer._unsafePutVarUint36Small(writerIndex, header); + if (NativeByteOrder.IS_LITTLE_ENDIAN) { + writerIndex = + offHeapWriteCharsUTF16WithOffset( + serializer, buffer, chars, offset, writerIndex, numBytes); + } else { + writerIndex = + offHeapWriteCharsUTF16BEWithOffset( + serializer, buffer, chars, offset, writerIndex, numBytes); + } + } + buffer._unsafeWriterIndex(writerIndex); + } + + static void writeCharsUTF8WithOffset( + StringSerializer serializer, MemoryBuffer buffer, char[] chars, int offset, int count) { + int estimateMaxBytes = count * 3; + int approxNumBytes = (int) (count * 1.5) + 1; + int writerIndex = buffer.writerIndex(); + buffer.ensure(writerIndex + 9 + estimateMaxBytes); + byte[] targetArray = buffer.getHeapMemory(); + if (targetArray != null) { + int targetIndex = buffer._unsafeHeapWriterIndex(); + int headerPos = targetIndex; + int arrIndex = targetIndex; + long header = ((long) approxNumBytes << 2) | UTF8; + int headerBytesWritten = LittleEndian.putVarUint36Small(targetArray, arrIndex, header); + arrIndex += headerBytesWritten; + writerIndex += headerBytesWritten; + targetIndex = convertUTF16ToUTF8(chars, offset, count, targetArray, arrIndex); + byte stashedByte = targetArray[arrIndex]; + int written = targetIndex - arrIndex; + header = ((long) written << 2) | UTF8; + int diff = + LittleEndian.putVarUint36Small(targetArray, headerPos, header) - headerBytesWritten; + if (diff != 0) { + handleWriteCharsUTF8UnalignedHeaderBytes(targetArray, arrIndex, diff, written, stashedByte); + } + buffer._unsafeWriterIndex(writerIndex + written + diff); + } else { + final byte[] tmpArray = serializer.getByteArray(estimateMaxBytes); + int written = convertUTF16ToUTF8(chars, offset, count, tmpArray, 0); + long header = ((long) written << 2) | UTF8; + writerIndex += buffer._unsafePutVarUint36Small(writerIndex, header); + buffer.put(writerIndex, tmpArray, 0, written); + buffer._unsafeWriterIndex(writerIndex + written); + } + } + + static void writeCharsUTF8PerfOptimizedWithOffset( + StringSerializer serializer, MemoryBuffer buffer, char[] chars, int offset, int count) { + int estimateMaxBytes = count * 3; + int numBytes = MathUtils.doubleExact(count); + int writerIndex = buffer.writerIndex(); + long header = ((long) numBytes << 2) | UTF8; + buffer.ensure(writerIndex + 9 + estimateMaxBytes); + byte[] targetArray = buffer.getHeapMemory(); + if (targetArray != null) { + int targetIndex = buffer._unsafeHeapWriterIndex(); + int arrIndex = targetIndex; + arrIndex += LittleEndian.putVarUint36Small(targetArray, arrIndex, header); + writerIndex += arrIndex - targetIndex; + targetIndex = convertUTF16ToUTF8(chars, offset, count, targetArray, arrIndex + 4); + int written = targetIndex - arrIndex - 4; + buffer._unsafePutInt32(writerIndex, written); + buffer._unsafeWriterIndex(writerIndex + 4 + written); + } else { + final byte[] tmpArray = serializer.getByteArray(estimateMaxBytes); + int written = convertUTF16ToUTF8(chars, offset, count, tmpArray, 0); + writerIndex += buffer._unsafePutVarUint36Small(writerIndex, header); + buffer._unsafePutInt32(writerIndex, written); + writerIndex += 4; + buffer.put(writerIndex, tmpArray, 0, written); + buffer._unsafeWriterIndex(writerIndex + written); + } + } + + static byte bestCoder(char[] chars, int offset, int count) { + int sampleNum = Math.min(64, count); + int vectorizedLen = sampleNum >> 2; + int vectorizedChars = vectorizedLen << 2; + int asciiCount = 0; + int latin1Count = 0; + int charOffset = offset; + int vectorEnd = offset + vectorizedChars; + for (; charOffset < vectorEnd; charOffset += 4) { + long multiChars = PlatformStringUtils.getCharsLong(chars, charOffset); + if ((multiChars & MULTI_CHARS_NON_ASCII_MASK) == 0) { + latin1Count += 4; + asciiCount += 4; + } else if ((multiChars & MULTI_CHARS_NON_LATIN_MASK) == 0) { + latin1Count += 4; + for (int i = 0; i < 4; ++i) { + if (chars[charOffset + i] < 0x80) { + asciiCount++; + } + } + } else { + for (int i = 0; i < 4; ++i) { + char c = chars[charOffset + i]; + if (c < 0x80) { + latin1Count++; + asciiCount++; + } else if (c <= 0xFF) { + latin1Count++; + } + } + } + } + + for (int i = vectorizedChars; i < sampleNum; i++) { + char c = chars[offset + i]; + if (c < 0x80) { + latin1Count++; + asciiCount++; + } else if (c <= 0xFF) { + latin1Count++; + } + } + + if (latin1Count == count || (latin1Count == sampleNum && isLatin(chars, offset, count))) { + return LATIN1; + } else if (asciiCount >= sampleNum * 0.5) { + return UTF8; + } else { + return UTF16; + } + } + + private static void handleWriteCharsUTF8UnalignedHeaderBytes( + byte[] targetArray, int arrIndex, int diff, int written, byte stashed) { + if (diff == 1) { + System.arraycopy(targetArray, arrIndex + 1, targetArray, arrIndex + 2, written - 1); + targetArray[arrIndex + 1] = stashed; + } else { + System.arraycopy(targetArray, arrIndex, targetArray, arrIndex - 1, written); + } + } + + private static void writeCharsUTF16BEToHeap( + char[] chars, int offset, int arrIndex, int numBytes, byte[] targetArray) { + int charIndex = offset; + for (int i = arrIndex, end = i + numBytes; i < end; i += 2) { + char c = chars[charIndex++]; + targetArray[i] = (byte) c; + targetArray[i + 1] = (byte) (c >>> 8); + } + } + + private static int offHeapWriteCharsUTF16WithOffset( + StringSerializer serializer, + MemoryBuffer buffer, + char[] chars, + int offset, + int writerIndex, + int numBytes) { + byte[] tmpArray = serializer.getByteArray(numBytes); + PlatformStringUtils.copyCharsToBytes(chars, offset, tmpArray, 0, numBytes); + buffer.put(writerIndex, tmpArray, 0, numBytes); + writerIndex += numBytes; + return writerIndex; + } + + private static int offHeapWriteCharsUTF16BEWithOffset( + StringSerializer serializer, + MemoryBuffer buffer, + char[] chars, + int offset, + int writerIndex, + int numBytes) { + byte[] tmpArray = serializer.getByteArray(numBytes); + int charIndex = offset; + for (int i = 0; i < numBytes; i += 2) { + char c = chars[charIndex++]; + tmpArray[i] = (byte) c; + tmpArray[i + 1] = (byte) (c >>> 8); + } + buffer.put(writerIndex, tmpArray, 0, numBytes); + writerIndex += numBytes; + return writerIndex; + } } diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/StringSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/StringSerializer.java index 939ee9aad6..f0f956d84a 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/StringSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/StringSerializer.java @@ -26,9 +26,8 @@ import java.lang.invoke.CallSite; import java.lang.invoke.LambdaMetafactory; 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.Field; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.function.BiFunction; @@ -45,17 +44,12 @@ import org.apache.fory.memory.MemoryBuffer; import org.apache.fory.memory.NativeByteOrder; import org.apache.fory.platform.AndroidSupport; -import org.apache.fory.platform.JdkVersion; -import org.apache.fory.platform.UnsafeOps; -import org.apache.fory.reflect.ReflectionUtils; +import org.apache.fory.platform.internal._JDKAccess; import org.apache.fory.util.MathUtils; import org.apache.fory.util.Preconditions; -import org.apache.fory.util.StringEncodingUtils; -import org.apache.fory.util.StringUtils; -import org.apache.fory.util.unsafe._JDKAccess; /** - * String serializer based on {@link sun.misc.Unsafe} and {@link MethodHandle} for speed. + * String serializer based on JDK-internal string access and byte-array accessors for speed. * *

Note that string operations is very common in serialization, and jvm inline and branch * elimination is not reliable even in c2 compiler, so we try to inline and avoid checks as we can @@ -63,8 +57,12 @@ */ @SuppressWarnings("unchecked") public final class StringSerializer extends ImmutableSerializer { - private static final boolean STRING_VALUE_FIELD_IS_CHARS; - private static final boolean STRING_VALUE_FIELD_IS_BYTES; + private static final boolean STRING_VALUE_FIELD_IS_CHARS = + PlatformStringUtils.STRING_VALUE_FIELD_IS_CHARS; + private static final boolean STRING_VALUE_FIELD_IS_BYTES = + PlatformStringUtils.STRING_VALUE_FIELD_IS_BYTES; + private static final boolean JDK_INTERNAL_FIELD_ACCESS = + PlatformStringUtils.JDK_STRING_FIELD_ACCESS; private static final byte LATIN1 = 0; private static final Byte LATIN1_BOXED = LATIN1; @@ -73,69 +71,16 @@ public final class StringSerializer extends ImmutableSerializer { private static final byte UTF8 = 2; private static final int DEFAULT_BUFFER_SIZE = 1024; - // Make offset compatible with graalvm native image. - private static final long STRING_VALUE_FIELD_OFFSET; - private static final boolean STRING_HAS_COUNT_OFFSET; - private static final long STRING_COUNT_FIELD_OFFSET; - private static final long STRING_OFFSET_FIELD_OFFSET; - - private static class Offset { - // Make offset compatible with graalvm native image. - private static final long STRING_CODER_FIELD_OFFSET; - - static { - if (AndroidSupport.IS_ANDROID) { - STRING_CODER_FIELD_OFFSET = -1; - } else { - try { - STRING_CODER_FIELD_OFFSET = - UnsafeOps.objectFieldOffset(String.class.getDeclaredField("coder")); - } catch (NoSuchFieldException e) { - throw new RuntimeException(e); - } - } - } - } - - static { - if (AndroidSupport.IS_ANDROID) { - STRING_VALUE_FIELD_IS_CHARS = false; - STRING_VALUE_FIELD_IS_BYTES = false; - STRING_VALUE_FIELD_OFFSET = -1; - STRING_HAS_COUNT_OFFSET = false; - STRING_COUNT_FIELD_OFFSET = -1; - STRING_OFFSET_FIELD_OFFSET = -1; - } else { - Field valueField = ReflectionUtils.getFieldNullable(String.class, "value"); - // Java8 string - STRING_VALUE_FIELD_IS_CHARS = valueField != null && valueField.getType() == char[].class; - // Java11 string - STRING_VALUE_FIELD_IS_BYTES = valueField != null && valueField.getType() == byte[].class; - try { - // Make offset compatible with graalvm native image. - STRING_VALUE_FIELD_OFFSET = - UnsafeOps.objectFieldOffset(String.class.getDeclaredField("value")); - } catch (NoSuchFieldException e) { - throw new RuntimeException(e); - } - Field countField = ReflectionUtils.getFieldNullable(String.class, "count"); - Field offsetField = ReflectionUtils.getFieldNullable(String.class, "offset"); - if (countField != null || offsetField != null) { - Preconditions.checkArgument( - countField != null && offsetField != null, "Current jdk not supported"); - Preconditions.checkArgument( - countField.getType() == int.class && offsetField.getType() == int.class, - "Current jdk not supported"); - STRING_HAS_COUNT_OFFSET = true; - STRING_COUNT_FIELD_OFFSET = UnsafeOps.objectFieldOffset(countField); - STRING_OFFSET_FIELD_OFFSET = UnsafeOps.objectFieldOffset(offsetField); - } else { - STRING_HAS_COUNT_OFFSET = false; - STRING_COUNT_FIELD_OFFSET = -1; - STRING_OFFSET_FIELD_OFFSET = -1; - } - } - } + private static final boolean STRING_HAS_COUNT_OFFSET = + PlatformStringUtils.STRING_HAS_COUNT_OFFSET; + private static final Lookup STRING_LOOKUP = + JDK_INTERNAL_FIELD_ACCESS ? _JDKAccess._trustedLookup(String.class) : null; + private static final BiFunction CHARS_STRING_ZERO_COPY_CTR = + JDK_INTERNAL_FIELD_ACCESS ? getCharsStringZeroCopyCtr() : null; + private static final BiFunction BYTES_STRING_ZERO_COPY_CTR = + JDK_INTERNAL_FIELD_ACCESS ? getBytesStringZeroCopyCtr() : null; + private static final Function LATIN_BYTES_STRING_ZERO_COPY_CTR = + JDK_INTERNAL_FIELD_ACCESS ? getLatinBytesStringZeroCopyCtr() : null; private final boolean compressString; private final boolean writeNumUtf16BytesForUtf8Encoding; @@ -174,6 +119,9 @@ public String read(ReadContext readContext) { public static Expression writeStringExpr( Expression strSerializer, Expression buffer, Expression str, boolean compressString) { + if (!JDK_INTERNAL_FIELD_ACCESS) { + return new Invoke(strSerializer, "writeString", buffer, str); + } if (STRING_VALUE_FIELD_IS_BYTES) { if (compressString) { return new Invoke(strSerializer, "writeCompressedBytesString", buffer, str); @@ -202,6 +150,9 @@ public static Expression writeStringExpr( public static Expression readStringExpr( Expression strSerializer, Expression buffer, boolean compressString) { + if (!JDK_INTERNAL_FIELD_ACCESS) { + return new Invoke(strSerializer, "readString", STRING_TYPE, buffer); + } if (STRING_VALUE_FIELD_IS_BYTES) { if (compressString) { return new Invoke(strSerializer, "readCompressedBytesString", STRING_TYPE, buffer); @@ -366,7 +317,7 @@ public String readCompressedCharsString(MemoryBuffer buffer) { // Invoked by fory JIT public void writeString(MemoryBuffer buffer, String value) { - if (AndroidSupport.IS_ANDROID) { + if (!JDK_INTERNAL_FIELD_ACCESS) { writeStringSlow(buffer, value); return; } @@ -400,7 +351,7 @@ private void writeJava8String(MemoryBuffer buffer, String value) { // Invoked by fory JIT public String readString(MemoryBuffer buffer) { - if (AndroidSupport.IS_ANDROID) { + if (!JDK_INTERNAL_FIELD_ACCESS) { return readStringSlow(buffer); } if (STRING_VALUE_FIELD_IS_BYTES) { @@ -421,7 +372,7 @@ public String readString(MemoryBuffer buffer) { private void writeStringSlow(MemoryBuffer buffer, String value) { char[] chars = value.toCharArray(); - if (isLatin(chars)) { + if (StringEncodingUtils.isLatin(chars)) { writeCharsLatin1(buffer, chars, chars.length); return; } @@ -469,19 +420,26 @@ private static void writeVarUint36Small(MemoryBuffer buffer, long value) { buffer._unsafeWriterIndex(writerIndex); } - private static boolean isLatin(char[] chars) { - for (char c : chars) { - if (c > 0xFF) { - return false; - } - } - return true; + private static Object getStringValue(String value) { + return PlatformStringUtils.getStringValue(value); + } + + private static byte getStringCoder(String value) { + return PlatformStringUtils.getStringCoder(value); + } + + private static int getStringOffset(String value) { + return PlatformStringUtils.getStringOffset(value); + } + + private static int getStringCount(String value) { + return PlatformStringUtils.getStringCount(value); } @CodegenInvoke public void writeCompressedBytesString(MemoryBuffer buffer, String value) { - final byte[] bytes = (byte[]) UnsafeOps.getObject(value, STRING_VALUE_FIELD_OFFSET); - final byte coder = UnsafeOps.getByte(value, Offset.STRING_CODER_FIELD_OFFSET); + final byte[] bytes = (byte[]) getStringValue(value); + final byte coder = getStringCoder(value); if (coder == LATIN1 || bestCoder(bytes) == UTF16) { writeBytesString(buffer, coder, bytes); } else { @@ -495,7 +453,7 @@ public void writeCompressedBytesString(MemoryBuffer buffer, String value) { @CodegenInvoke public void writeCompressedCharsString(MemoryBuffer buffer, String value) { - final char[] chars = (char[]) UnsafeOps.getObject(value, STRING_VALUE_FIELD_OFFSET); + final char[] chars = (char[]) getStringValue(value); final byte coder = bestCoder(chars); if (coder == LATIN1) { writeCharsLatin1(buffer, chars, chars.length); @@ -512,27 +470,28 @@ public void writeCompressedCharsString(MemoryBuffer buffer, String value) { @CodegenInvoke public void writeCompressedCharsStringWithOffset(MemoryBuffer buffer, String value) { - final char[] chars = (char[]) UnsafeOps.getObject(value, STRING_VALUE_FIELD_OFFSET); - final int offset = UnsafeOps.getInt(value, STRING_OFFSET_FIELD_OFFSET); - final int count = UnsafeOps.getInt(value, STRING_COUNT_FIELD_OFFSET); - final byte coder = SlicedStringUtil.bestCoder(chars, offset, count); + final char[] chars = (char[]) getStringValue(value); + final int offset = getStringOffset(value); + final int count = getStringCount(value); + final byte coder = StringEncodingUtils.bestCoder(chars, offset, count); if (coder == LATIN1) { - SlicedStringUtil.writeCharsLatin1WithOffset(this, buffer, chars, offset, count); + StringEncodingUtils.writeCharsLatin1WithOffset(this, buffer, chars, offset, count); } else if (coder == UTF8) { if (writeNumUtf16BytesForUtf8Encoding) { - SlicedStringUtil.writeCharsUTF8PerfOptimizedWithOffset(this, buffer, chars, offset, count); + StringEncodingUtils.writeCharsUTF8PerfOptimizedWithOffset( + this, buffer, chars, offset, count); } else { - SlicedStringUtil.writeCharsUTF8WithOffset(this, buffer, chars, offset, count); + StringEncodingUtils.writeCharsUTF8WithOffset(this, buffer, chars, offset, count); } } else { - SlicedStringUtil.writeCharsUTF16WithOffset(this, buffer, chars, offset, count); + StringEncodingUtils.writeCharsUTF16WithOffset(this, buffer, chars, offset, count); } } @CodegenInvoke public static void writeBytesString(MemoryBuffer buffer, String value) { - byte[] bytes = (byte[]) UnsafeOps.getObject(value, STRING_VALUE_FIELD_OFFSET); - byte coder = UnsafeOps.getByte(value, Offset.STRING_CODER_FIELD_OFFSET); + byte[] bytes = (byte[]) getStringValue(value); + byte coder = getStringCoder(value); writeBytesString(buffer, coder, bytes); } @@ -557,14 +516,8 @@ public static void writeBytesString(MemoryBuffer buffer, byte coder, byte[] byte writerIndex += arrIndex - targetIndex; System.arraycopy(bytes, 0, targetArray, arrIndex, bytesLen); } else { - final long targetAddress = buffer._unsafeWriterAddress(); - final int headerBytes = buffer._unsafePutVarUint36Small(writerIndex, header); - writerIndex += headerBytes; - // The preceding ensure makes this copy bounds-safe. Keep the direct copy local so generated - // string serializers do not route every off-heap string payload through checked raw-copy - // helpers. - UnsafeOps.copyMemory( - bytes, UnsafeOps.BYTE_ARRAY_OFFSET, null, targetAddress + headerBytes, bytesLen); + writerIndex += buffer._unsafePutVarUint36Small(writerIndex, header); + PlatformStringUtils.putBytes(buffer, writerIndex, bytes, bytesLen); } writerIndex += bytesLen; buffer._unsafeWriterIndex(writerIndex); @@ -572,8 +525,8 @@ public static void writeBytesString(MemoryBuffer buffer, byte coder, byte[] byte @CodegenInvoke public void writeCharsString(MemoryBuffer buffer, String value) { - final char[] chars = (char[]) UnsafeOps.getObject(value, STRING_VALUE_FIELD_OFFSET); - if (StringUtils.isLatin(chars)) { + final char[] chars = (char[]) getStringValue(value); + if (StringEncodingUtils.isLatin(chars)) { writeCharsLatin1(buffer, chars, chars.length); } else { writeCharsUTF16(buffer, chars, chars.length); @@ -582,13 +535,13 @@ public void writeCharsString(MemoryBuffer buffer, String value) { @CodegenInvoke public void writeCharsStringWithOffset(MemoryBuffer buffer, String value) { - final char[] chars = (char[]) UnsafeOps.getObject(value, STRING_VALUE_FIELD_OFFSET); - final int offset = UnsafeOps.getInt(value, STRING_OFFSET_FIELD_OFFSET); - final int count = UnsafeOps.getInt(value, STRING_COUNT_FIELD_OFFSET); - if (SlicedStringUtil.isLatin(chars, offset, count)) { - SlicedStringUtil.writeCharsLatin1WithOffset(this, buffer, chars, offset, count); + final char[] chars = (char[]) getStringValue(value); + final int offset = getStringOffset(value); + final int count = getStringCount(value); + if (StringEncodingUtils.isLatin(chars, offset, count)) { + StringEncodingUtils.writeCharsLatin1WithOffset(this, buffer, chars, offset, count); } else { - SlicedStringUtil.writeCharsUTF16WithOffset(this, buffer, chars, offset, count); + StringEncodingUtils.writeCharsUTF16WithOffset(this, buffer, chars, offset, count); } } @@ -777,12 +730,7 @@ public void writeCharsUTF16(MemoryBuffer buffer, char[] chars, int numChars) { writeCharsUTF16ToHeapSlow(chars, arrIndex, numBytes, targetArray); } else { // FIXME JDK11 utf16 string uses little-endian order. - UnsafeOps.UNSAFE.copyMemory( - chars, - UnsafeOps.CHAR_ARRAY_OFFSET, - targetArray, - UnsafeOps.BYTE_ARRAY_OFFSET + arrIndex, - numBytes); + PlatformStringUtils.copyCharsToBytes(chars, 0, targetArray, arrIndex, numBytes); } } else { writeCharsUTF16BEToHeap(chars, arrIndex, numBytes, targetArray); @@ -943,55 +891,30 @@ private void writeBytesUTF8PerfOptimized(MemoryBuffer buffer, byte[] bytes) { } } - private static final MethodHandles.Lookup STRING_LOOK_UP = - AndroidSupport.IS_ANDROID ? null : _JDKAccess._trustedLookup(String.class); - private static final BiFunction CHARS_STRING_ZERO_COPY_CTR = - AndroidSupport.IS_ANDROID ? null : getCharsStringZeroCopyCtr(); - private static final BiFunction BYTES_STRING_ZERO_COPY_CTR = - AndroidSupport.IS_ANDROID ? null : getBytesStringZeroCopyCtr(); - private static final Function LATIN_BYTES_STRING_ZERO_COPY_CTR = - AndroidSupport.IS_ANDROID ? null : getLatinBytesStringZeroCopyCtr(); - public static String newCharsStringZeroCopy(char[] data) { - if (AndroidSupport.IS_ANDROID) { - return newCharsStringSlow(data); + if (!JDK_INTERNAL_FIELD_ACCESS) { + return new String(data); } if (!STRING_VALUE_FIELD_IS_CHARS) { throw new IllegalStateException("String value isn't char[], current java isn't supported"); } - // 25% faster than unsafe put field, only 10% slower than `new String(str)` return CHARS_STRING_ZERO_COPY_CTR.apply(data, Boolean.TRUE); } - private static String newCharsStringSlow(char[] data) { - return new String(data); - } - // coder param first to make inline call args // `(buffer.readByte(), buffer.readBytesWithSizeEmbedded())` work. public static String newBytesStringZeroCopy(byte coder, byte[] data) { - if (AndroidSupport.IS_ANDROID) { + if (!JDK_INTERNAL_FIELD_ACCESS) { return newBytesStringSlow(coder, data); } if (coder == LATIN1) { - // 700% faster than unsafe put field in java11, only 10% slower than `new String(str)` for - // string length 230. - // 50% faster than unsafe put field in java11 for string length 10. if (LATIN_BYTES_STRING_ZERO_COPY_CTR != null) { return LATIN_BYTES_STRING_ZERO_COPY_CTR.apply(data); - } else { - // JDK17 removed newStringLatin1 - return BYTES_STRING_ZERO_COPY_CTR.apply(data, LATIN1_BOXED); } + return BYTES_STRING_ZERO_COPY_CTR.apply(data, LATIN1_BOXED); } else if (coder == UTF16) { - // avoid byte box cost. return BYTES_STRING_ZERO_COPY_CTR.apply(data, UTF16_BOXED); } else { - // 700% faster than unsafe put field in java11, only 10% slower than `new String(str)` for - // string length 230. - // 50% faster than unsafe put field in java11 for string length 10. - // `invokeExact` must pass exact params with exact types: - // `(Object) data, coder` will throw WrongMethodTypeException return BYTES_STRING_ZERO_COPY_CTR.apply(data, coder); } } @@ -1019,10 +942,9 @@ private static BiFunction getCharsStringZeroCopyCtr() { return null; } try { - // Faster than handle.invokeExact(data, boolean) CallSite callSite = LambdaMetafactory.metafactory( - STRING_LOOK_UP, + STRING_LOOKUP, "apply", MethodType.methodType(BiFunction.class), handle.type().generic(), @@ -1042,13 +964,12 @@ private static BiFunction getBytesStringZeroCopyCtr() { if (handle == null) { return null; } - // Faster than handle.invokeExact(data, byte) try { MethodType instantiatedMethodType = MethodType.methodType(handle.type().returnType(), new Class[] {byte[].class, Byte.class}); CallSite callSite = LambdaMetafactory.metafactory( - STRING_LOOK_UP, + STRING_LOOKUP, "apply", MethodType.methodType(BiFunction.class), handle.type().generic(), @@ -1061,20 +982,15 @@ private static BiFunction getBytesStringZeroCopyCtr() { } private static Function getLatinBytesStringZeroCopyCtr() { - if (!STRING_VALUE_FIELD_IS_BYTES) { - return null; - } - if (STRING_LOOK_UP == null) { + if (!STRING_VALUE_FIELD_IS_BYTES || STRING_LOOKUP == null) { return null; } try { Class clazz = Class.forName("java.lang.StringCoding"); - MethodHandles.Lookup caller = STRING_LOOK_UP.in(clazz); - // JDK17 removed this method. + Lookup caller = STRING_LOOKUP.in(clazz); MethodHandle handle = caller.findStatic( clazz, "newStringLatin1", MethodType.methodType(String.class, byte[].class)); - // Faster than handle.invokeExact(data, byte) return _JDKAccess.makeFunction(caller, handle, Function.class); } catch (Throwable e) { return null; @@ -1082,16 +998,15 @@ private static Function getLatinBytesStringZeroCopyCtr() { } private static MethodHandle getJavaStringZeroCopyCtrHandle() { - Preconditions.checkArgument(JdkVersion.MAJOR_VERSION >= 8); - if (STRING_LOOK_UP == null) { + if (STRING_LOOKUP == null) { return null; } try { if (STRING_VALUE_FIELD_IS_CHARS) { - return STRING_LOOK_UP.findConstructor( + return STRING_LOOKUP.findConstructor( String.class, MethodType.methodType(void.class, char[].class, boolean.class)); } else { - return STRING_LOOK_UP.findConstructor( + return STRING_LOOKUP.findConstructor( String.class, MethodType.methodType(void.class, byte[].class, byte.class)); } } catch (Exception e) { @@ -1215,13 +1130,10 @@ private static byte bestCoder(char[] chars) { int sampleNum = Math.min(64, numChars); int vectorizedLen = sampleNum >> 2; int vectorizedChars = vectorizedLen << 2; - int endOffset = UnsafeOps.CHAR_ARRAY_OFFSET + (vectorizedChars << 1); int asciiCount = 0; int latin1Count = 0; - for (int offset = UnsafeOps.CHAR_ARRAY_OFFSET, charOffset = 0; - offset < endOffset; - offset += 8, charOffset += 4) { - long multiChars = UnsafeOps.getLong(chars, offset); + for (int charOffset = 0; charOffset < vectorizedChars; charOffset += 4) { + long multiChars = PlatformStringUtils.getCharsLong(chars, charOffset); if ((multiChars & MULTI_CHARS_NON_ASCII_MASK) == 0) { latin1Count += 4; asciiCount += 4; @@ -1254,7 +1166,7 @@ private static byte bestCoder(char[] chars) { } if (latin1Count == numChars - || (latin1Count == sampleNum && StringUtils.isLatin(chars, sampleNum))) { + || (latin1Count == sampleNum && StringEncodingUtils.isLatin(chars, sampleNum))) { return LATIN1; } else if (asciiCount >= sampleNum * 0.5) { // ascii number > 50%, choose UTF-8 @@ -1270,24 +1182,21 @@ private static byte bestCoder(byte[] bytes) { int sampleNum = Math.min(64 << 1, numBytes); int vectorizedLen = sampleNum >> 3; int vectorizedBytes = vectorizedLen << 3; - int endOffset = UnsafeOps.BYTE_ARRAY_OFFSET + vectorizedBytes; int asciiCount = 0; - for (int offset = UnsafeOps.BYTE_ARRAY_OFFSET, bytesOffset = 0; - offset < endOffset; - offset += 8, bytesOffset += 8) { - long multiChars = UnsafeOps.getLong(bytes, offset); + for (int bytesOffset = 0; bytesOffset < vectorizedBytes; bytesOffset += 8) { + long multiChars = PlatformStringUtils.getBytesLong(bytes, bytesOffset); if ((multiChars & MULTI_CHARS_NON_ASCII_MASK) == 0) { asciiCount += 4; } else { for (int i = 0; i < 8; i += 2) { - if (UnsafeOps.getChar(bytes, offset + i) < 0x80) { + if (PlatformStringUtils.getBytesChar(bytes, bytesOffset + i) < 0x80) { asciiCount++; } } } } for (int i = vectorizedBytes; vectorizedBytes < sampleNum; vectorizedBytes += 2) { - if (UnsafeOps.getChar(bytes, UnsafeOps.BYTE_ARRAY_OFFSET + i) < 0x80) { + if (PlatformStringUtils.getBytesChar(bytes, i) < 0x80) { asciiCount++; } } diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/URLSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/URLSerializer.java index 70043723cb..239e731eb2 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/URLSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/URLSerializer.java @@ -21,6 +21,7 @@ import java.net.MalformedURLException; import java.net.URL; +import org.apache.fory.context.CopyContext; import org.apache.fory.context.ReadContext; import org.apache.fory.context.WriteContext; import org.apache.fory.resolver.TypeResolver; @@ -28,10 +29,10 @@ /** Serializer for {@link URL}. */ // TODO(chaokunyang) ensure security to avoid dnslog detection. -public final class URLSerializer extends AbstractObjectSerializer { +public final class URLSerializer extends Serializer { public URLSerializer(TypeResolver typeResolver, Class type) { - super(typeResolver, type); + super(typeResolver.getConfig(), type); } public void write(WriteContext writeContext, URL object) { @@ -46,4 +47,14 @@ public URL read(ReadContext readContext) { throw new IllegalStateException("unreachable"); } } + + @Override + public URL copy(CopyContext copyContext, URL originURL) { + try { + return new URL(originURL.toExternalForm()); + } catch (MalformedURLException e) { + ExceptionUtils.throwException(e); + throw new IllegalStateException("unreachable"); + } + } } diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ChildContainerSerializers.java b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ChildContainerSerializers.java index 4a80478a31..f7840349ef 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ChildContainerSerializers.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ChildContainerSerializers.java @@ -73,7 +73,6 @@ */ @SuppressWarnings({"unchecked", "rawtypes"}) public class ChildContainerSerializers { - public static Class getCollectionSerializerClass(Class cls) { if (ChildCollectionSerializer.superClasses.contains(cls) || ChildSortedSetSerializer.superClasses.contains(cls) @@ -264,7 +263,7 @@ public T newCollection(ReadContext readContext) { public Collection newCollection(CopyContext copyContext, Collection originCollection) { T newCollection = subclassFactory.newCollection( - copyContext.copyObject(((SortedSet) originCollection).comparator())); + ComparatorCopy.copy(copyContext, ((SortedSet) originCollection).comparator())); copyChildFields(copyContext, originCollection, newCollection); return newCollection; } @@ -310,7 +309,7 @@ public T newCollection(ReadContext readContext) { public Collection newCollection(CopyContext copyContext, Collection originCollection) { T newCollection = subclassFactory.newCollection( - copyContext.copyObject(((PriorityQueue) originCollection).comparator()), + ComparatorCopy.copy(copyContext, ((PriorityQueue) originCollection).comparator()), originCollection.size()); copyChildFields(copyContext, originCollection, newCollection); return newCollection; @@ -418,7 +417,7 @@ public Map newMap(ReadContext readContext) { public Map newMap(CopyContext copyContext, Map originMap) { T newMap = subclassFactory.newMap( - copyContext.copyObject(((SortedMap) originMap).comparator())); + ComparatorCopy.copy(copyContext, ((SortedMap) originMap).comparator())); copyChildFields(copyContext, originMap, newMap); return newMap; } @@ -626,7 +625,9 @@ private static Serializer[] buildSlotsSerializers( slotsSerializer = new CompatibleLayerSerializer(typeResolver, cls, layerTypeDef, layerMarkerClass); } else { - slotsSerializer = new ObjectSerializer<>(typeResolver, cls, false); + // Slot serializers only populate fields on the container instance created by the concrete + // container serializer. + slotsSerializer = new ObjectSerializer<>(typeResolver, cls, false, null); } serializers.add(slotsSerializer); cls = (Class) cls.getSuperclass(); diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionSerializers.java b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionSerializers.java index c2d103eb32..d820331c6f 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionSerializers.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionSerializers.java @@ -21,7 +21,6 @@ import java.io.Externalizable; import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; import java.lang.reflect.Field; import java.util.ArrayDeque; import java.util.ArrayList; @@ -57,9 +56,8 @@ import org.apache.fory.exception.DeserializationException; import org.apache.fory.exception.ForyException; import org.apache.fory.memory.MemoryBuffer; -import org.apache.fory.platform.AndroidSupport; -import org.apache.fory.platform.GraalvmSupport; -import org.apache.fory.platform.UnsafeOps; +import org.apache.fory.memory.MemoryUtils; +import org.apache.fory.reflect.FieldAccessor; import org.apache.fory.reflect.ReflectionUtils; import org.apache.fory.resolver.ClassResolver; import org.apache.fory.resolver.TypeInfo; @@ -70,7 +68,6 @@ import org.apache.fory.serializer.Serializer; import org.apache.fory.serializer.Serializers; import org.apache.fory.util.Preconditions; -import org.apache.fory.util.unsafe._JDKAccess; /** * Serializers for classes implements {@link Collection}. All collection serializers should extend @@ -78,6 +75,18 @@ */ @SuppressWarnings({"unchecked", "rawtypes"}) public class CollectionSerializers { + private static final Comparator NATURAL_ORDER_COMPARATOR = Comparator.naturalOrder(); + + private static void requireXlangNaturalOrdering(Class type, Comparator comparator) { + if (comparator != null && comparator != NATURAL_ORDER_COMPARATOR) { + throw new UnsupportedOperationException( + "Xlang serialization of " + + type.getName() + + " with a custom comparator is unsupported because the xlang set wire format " + + "does not encode comparators"); + } + } + private static void throwBinarySizeLimitExceeded(long size, int maxBinarySize) { throw new DeserializationException( "Binary payload size " + size + " exceeds max binary size " + maxBinarySize); @@ -112,6 +121,15 @@ private static void checkBoundedQueueCapacity(Config config, int numElements, in } } + private static UnsupportedOperationException unsupportedBoundedQueueWrite(Class type) { + return new UnsupportedOperationException( + "Serializing or copying " + + type.getName() + + " requires access to its exact capacity field. This runtime can deserialize existing " + + "payloads for this type, but cannot serialize or copy it without JDK concurrent " + + "field access."); + } + public static final class ArrayListSerializer extends CollectionSerializer { public ArrayListSerializer(TypeResolver typeResolver) { super(typeResolver, ArrayList.class, true); @@ -129,14 +147,13 @@ public ArrayList newCollection(ReadContext readContext) { } public static final class ArraysAsListSerializer extends CollectionSerializer> { - private static final class ArrayFieldOffset { - // Make offset compatible with graalvm native image. - private static final long VALUE; + private static final class ArrayAccess { + private static final FieldAccessor ACCESSOR; static { try { Field arrayField = Class.forName("java.util.Arrays$ArrayList").getDeclaredField("a"); - VALUE = UnsafeOps.objectFieldOffset(arrayField); + ACCESSOR = FieldAccessor.createAccessor(arrayField); } catch (final Exception e) { throw new RuntimeException(e); } @@ -162,9 +179,9 @@ public void write(WriteContext writeContext, List value) { super.write(writeContext, value); } else { Object[] array = - AndroidSupport.IS_ANDROID || GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE + !MemoryUtils.JDK_COLLECTION_FIELD_ACCESS ? value.toArray() - : (Object[]) UnsafeOps.getObject(value, ArrayFieldOffset.VALUE); + : (Object[]) ArrayAccess.ACCESSOR.getObject(value); writeContext.writeRef(array); } } @@ -247,6 +264,9 @@ public SortedSetSerializer(TypeResolver typeResolver, Class cls) { @Override public Collection onCollectionWrite(WriteContext writeContext, T value) { + if (config.isXlang()) { + requireXlangNaturalOrdering(type, value.comparator()); + } MemoryBuffer buffer = writeContext.getBuffer(); buffer.writeVarUInt32Small7(value.size()); if (config.isXlang()) { @@ -260,12 +280,11 @@ public Collection onCollectionWrite(WriteContext writeContext, T value) { @SuppressWarnings("unchecked") @Override public T newCollection(ReadContext readContext) { - assert !config.isXlang(); MemoryBuffer buffer = readContext.getBuffer(); int numElements = readCollectionSize(buffer); setNumElements(numElements); T collection; - Comparator comparator = (Comparator) readContext.readRef(); + Comparator comparator = config.isXlang() ? null : (Comparator) readContext.readRef(); if (type == TreeSet.class) { collection = (T) new TreeSet(comparator); } else { @@ -286,7 +305,8 @@ public T newCollection(ReadContext readContext) { @Override public Collection newCollection(CopyContext copyContext, Collection originCollection) { Collection collection; - Comparator comparator = copyContext.copyObject(((SortedSet) originCollection).comparator()); + Comparator comparator = + ComparatorCopy.copy(copyContext, ((SortedSet) originCollection).comparator()); if (Objects.equals(type, TreeSet.class)) { collection = new TreeSet(comparator); } else { @@ -519,6 +539,9 @@ public ConcurrentSkipListSetSerializer( @Override public CollectionSnapshot onCollectionWrite( WriteContext writeContext, ConcurrentSkipListSet value) { + if (config.isXlang()) { + requireXlangNaturalOrdering(type, value.comparator()); + } CollectionSnapshot snapshot = super.onCollectionWrite(writeContext, value); if (config.isXlang()) { return snapshot; @@ -532,7 +555,11 @@ public ConcurrentSkipListSet newCollection(ReadContext readContext) { MemoryBuffer buffer = readContext.getBuffer(); int numElements = readCollectionSize(buffer); setNumElements(numElements); - assert !config.isXlang(); + if (config.isXlang()) { + ConcurrentSkipListSet skipListSet = new ConcurrentSkipListSet(); + readContext.reference(skipListSet); + return skipListSet; + } int refId = readContext.lastPreservedRefId(); // It's possible that comparator/elements has circular ref to set. Comparator comparator = (Comparator) readContext.readRef(); @@ -544,7 +571,7 @@ public ConcurrentSkipListSet newCollection(ReadContext readContext) { @Override public Collection newCollection(CopyContext copyContext, Collection originCollection) { Comparator comparator = - copyContext.copyObject(((ConcurrentSkipListSet) originCollection).comparator()); + ComparatorCopy.copy(copyContext, ((ConcurrentSkipListSet) originCollection).comparator()); return new ConcurrentSkipListSet(comparator); } } @@ -552,23 +579,28 @@ public Collection newCollection(CopyContext copyContext, Collection originCollec public static final class SetFromMapSerializer extends CollectionSerializer> { private static final List EMPTY_COLLECTION_STUB = new ArrayList<>(); - private static final class JvmSetFromMapAccess { - private static final long MAP_FIELD_OFFSET; - private static final MethodHandle M_SETTER; - private static final MethodHandle S_SETTER; + private static final class SetFromMapAccess { + private static final FieldAccessor MAP_ACCESSOR; + private static final FieldAccessor KEY_SET_ACCESSOR; static { try { Class type = Class.forName("java.util.Collections$SetFromMap"); - Field mapField = type.getDeclaredField("m"); - MAP_FIELD_OFFSET = UnsafeOps.objectFieldOffset(mapField); - MethodHandles.Lookup lookup = _JDKAccess._trustedLookup(type); - M_SETTER = lookup.findSetter(type, "m", Map.class); - S_SETTER = lookup.findSetter(type, "s", Set.class); + MAP_ACCESSOR = FieldAccessor.createAccessor(type.getDeclaredField("m")); + KEY_SET_ACCESSOR = FieldAccessor.createAccessor(type.getDeclaredField("s")); } catch (final Exception e) { throw new RuntimeException(e); } } + + static Map map(Set set) { + return (Map) MAP_ACCESSOR.getObject(set); + } + + static void restore(Set set, Map map) { + MAP_ACCESSOR.putObject(set, map); + KEY_SET_ACCESSOR.putObject(set, map.keySet()); + } } public SetFromMapSerializer(TypeResolver typeResolver, Class> type) { @@ -588,18 +620,19 @@ public Collection newCollection(ReadContext readContext) { set = Collections.newSetFromMap(mapSerializer.newMap(readContext)); setNumElements(mapSerializer.getAndClearNumElements()); } else { - if (AndroidSupport.IS_ANDROID || GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { + if (!MemoryUtils.JDK_COLLECTION_FIELD_ACCESS) { throw new UnsupportedOperationException( - "This runtime cannot read legacy SetFromMap payloads that require hidden JDK field " + "This runtime cannot read SetFromMap backing-map payloads that require hidden JDK field " + "restoration"); } - Map map = (Map) mapSerializer.read(readContext); + Map map = (Map) mapSerializer.read(readContext); try { - set = UnsafeOps.newInstance(type); - JvmSetFromMapAccess.M_SETTER.invoke(set, map); - JvmSetFromMapAccess.S_SETTER.invoke(set, map.keySet()); + set = Collections.newSetFromMap(new HashMap<>()); + SetFromMapAccess.restore(set, map); } catch (Throwable e) { - throw new RuntimeException(e); + throw new UnsupportedOperationException( + "This runtime cannot restore SetFromMap backing-map payloads through final JDK fields", + e); } setNumElements(0); } @@ -610,24 +643,28 @@ public Collection newCollection(ReadContext readContext) { @Override public Collection newCollection(CopyContext copyContext, Collection originCollection) { assert !config.isXlang(); - if (AndroidSupport.IS_ANDROID || GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { - return Collections.newSetFromMap(new HashMap(originCollection.size())); + return Collections.newSetFromMap(new HashMap(originCollection.size())); + } + + @Override + public Set copy(CopyContext copyContext, Set originCollection) { + if (!MemoryUtils.JDK_COLLECTION_FIELD_ACCESS) { + return (Set) super.copy(copyContext, originCollection); } - Map map = - (Map) - UnsafeOps.getObject(originCollection, JvmSetFromMapAccess.MAP_FIELD_OFFSET); - MapLikeSerializer mapSerializer = - (MapLikeSerializer) typeResolver.getSerializer(map.getClass()); - Map newMap = mapSerializer.newMap(copyContext, map); - return Collections.newSetFromMap(newMap); + Map map = SetFromMapAccess.map(originCollection); + Set result = Collections.newSetFromMap(new HashMap<>()); + copyContext.reference(originCollection, result); + Map newMap = copyContext.copyObject(map); + SetFromMapAccess.restore(result, newMap); + return result; } @Override public Collection onCollectionWrite(WriteContext writeContext, Set value) { MemoryBuffer buffer = writeContext.getBuffer(); - final Map map; - final TypeInfo typeInfo; - if (AndroidSupport.IS_ANDROID || GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { + Map map; + TypeInfo typeInfo; + if (!MemoryUtils.JDK_COLLECTION_FIELD_ACCESS) { HashMap source = new HashMap<>(value.size()); for (Object element : value) { source.put(element, Boolean.TRUE); @@ -635,7 +672,7 @@ public Collection onCollectionWrite(WriteContext writeContext, Set value) { map = source; typeInfo = typeResolver.getTypeInfo(HashMap.class); } else { - map = (Map) UnsafeOps.getObject(value, JvmSetFromMapAccess.MAP_FIELD_OFFSET); + map = SetFromMapAccess.map(value); typeInfo = typeResolver.getTypeInfo(map.getClass()); } MapLikeSerializer mapSerializer = (MapLikeSerializer) typeInfo.getSerializer(); @@ -833,7 +870,8 @@ public Collection onCollectionWrite(WriteContext writeContext, PriorityQueue val @Override public Collection newCollection(CopyContext copyContext, Collection collection) { return new PriorityQueue( - collection.size(), copyContext.copyObject(((PriorityQueue) collection).comparator())); + collection.size(), + ComparatorCopy.copy(copyContext, ((PriorityQueue) collection).comparator())); } @Override @@ -861,13 +899,13 @@ public static class ArrayBlockingQueueSerializer // Use reflection to get the items array length which represents the capacity. // This avoids race conditions when reading remainingCapacity() and size() separately. - private static final class ItemsOffset { - private static final long VALUE; + private static final class ItemsAccess { + private static final FieldAccessor ACCESSOR; static { try { Field itemsField = ArrayBlockingQueue.class.getDeclaredField("items"); - VALUE = UnsafeOps.objectFieldOffset(itemsField); + ACCESSOR = FieldAccessor.createAccessor(itemsField); } catch (NoSuchFieldException e) { throw new RuntimeException(e); } @@ -879,10 +917,10 @@ public ArrayBlockingQueueSerializer(TypeResolver typeResolver, Class { // Use reflection to get the capacity field directly. // This avoids race conditions when reading remainingCapacity() and size() separately. - private static final class CapacityOffset { - private static final long VALUE; + private static final class CapacityAccess { + private static final FieldAccessor ACCESSOR; static { try { Field capacityField = LinkedBlockingQueue.class.getDeclaredField("capacity"); - VALUE = UnsafeOps.objectFieldOffset(capacityField); + ACCESSOR = FieldAccessor.createAccessor(capacityField); } catch (NoSuchFieldException e) { throw new RuntimeException(e); } @@ -946,10 +984,10 @@ public LinkedBlockingQueueSerializer( } private static int getCapacity(LinkedBlockingQueue queue) { - if (AndroidSupport.IS_ANDROID || GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { - return queue.size() + queue.remainingCapacity(); + if (!MemoryUtils.JDK_CONCURRENT_FIELD_ACCESS) { + throw unsupportedBoundedQueueWrite(LinkedBlockingQueue.class); } - return UnsafeOps.getInt(queue, CapacityOffset.VALUE); + return CapacityAccess.ACCESSOR.getInt(queue); } @Override diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ComparatorCopy.java b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ComparatorCopy.java new file mode 100644 index 0000000000..23fc54bcf0 --- /dev/null +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ComparatorCopy.java @@ -0,0 +1,43 @@ +/* + * 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.serializer.collection; + +import java.util.Comparator; +import org.apache.fory.context.CopyContext; + +final class ComparatorCopy { + private static final Class NATURAL_ORDER_CLASS = Comparator.naturalOrder().getClass(); + private static final Class REVERSE_ORDER_CLASS = Comparator.reverseOrder().getClass(); + + private ComparatorCopy() {} + + static Comparator copy(CopyContext copyContext, Comparator comparator) { + if (comparator == null || isJdkSingleton(comparator)) { + // Xlang copy is JVM-local; immutable JDK comparator singletons do not need a wire type. + return comparator; + } + return copyContext.copyObject(comparator); + } + + private static boolean isJdkSingleton(Comparator comparator) { + Class type = comparator.getClass(); + return type == NATURAL_ORDER_CLASS || type == REVERSE_ORDER_CLASS; + } +} diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/GuavaCollectionSerializers.java b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/GuavaCollectionSerializers.java index 4799c7d2ad..ac4e801104 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/GuavaCollectionSerializers.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/GuavaCollectionSerializers.java @@ -19,6 +19,7 @@ package org.apache.fory.serializer.collection; +import com.google.common.collect.HashBasedTable; import com.google.common.collect.ImmutableBiMap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -26,16 +27,23 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedMap; import com.google.common.collect.ImmutableSortedSet; +import com.google.common.collect.Table; +import com.google.common.primitives.ImmutableIntArray; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.Map; +import java.util.Map.Entry; import org.apache.fory.context.CopyContext; import org.apache.fory.context.ReadContext; import org.apache.fory.context.WriteContext; +import org.apache.fory.exception.ForyException; import org.apache.fory.memory.MemoryBuffer; import org.apache.fory.resolver.TypeInfo; import org.apache.fory.resolver.TypeResolver; +import org.apache.fory.serializer.Serializer; /** Serializers for common guava types. */ @SuppressWarnings({"unchecked", "rawtypes"}) @@ -44,9 +52,16 @@ public class GuavaCollectionSerializers { private static final String IMMUTABLE_BI_MAP_CLASS_NAME = PKG + ".ImmutableBiMap"; private static final String IMMUTABLE_LIST_CLASS_NAME = PKG + ".ImmutableList"; private static final String IMMUTABLE_MAP_CLASS_NAME = PKG + ".ImmutableMap"; + private static final String IMMUTABLE_MAP_FORM_CLASS_NAME = + IMMUTABLE_MAP_CLASS_NAME + "$SerializedForm"; + private static final String IMMUTABLE_BI_MAP_FORM_CLASS_NAME = + IMMUTABLE_BI_MAP_CLASS_NAME + "$SerializedForm"; private static final String IMMUTABLE_SET_CLASS_NAME = PKG + ".ImmutableSet"; private static final String IMMUTABLE_SORTED_MAP_CLASS_NAME = PKG + ".ImmutableSortedMap"; private static final String IMMUTABLE_SORTED_SET_CLASS_NAME = PKG + ".ImmutableSortedSet"; + private static final String HASH_BASED_TABLE_CLASS_NAME = PKG + ".HashBasedTable"; + private static final String IMMUTABLE_INT_ARRAY_CLASS_NAME = + "com.google.common.primitives.ImmutableIntArray"; private static final int NUM_RESERVED_TYPE_IDS = 13; private static final boolean GUAVA_AVAILABLE = isClassAvailable(IMMUTABLE_BI_MAP_CLASS_NAME) @@ -203,7 +218,7 @@ public T onCollectionRead(Collection collection) { @Override public T copy(CopyContext copyContext, T originCollection) { - Comparator comparator = copyContext.copyObject(originCollection.comparator()); + Comparator comparator = ComparatorCopy.copy(copyContext, originCollection.comparator()); Object[] elements = new Object[originCollection.size()]; copyElements(copyContext, originCollection, elements); return (T) new ImmutableSortedSet.Builder<>(comparator).add(elements).build(); @@ -353,6 +368,187 @@ protected T xnewInstance(Map map) { } } + public abstract static class GuavaMapFormSerializer extends Serializer { + private final Constructor constructor; + private final Method readResolveMethod; + private final boolean biMap; + + public GuavaMapFormSerializer(TypeResolver typeResolver, Class cls, boolean biMap) { + super(typeResolver.getConfig(), cls); + this.biMap = biMap; + try { + Class mapClass = biMap ? ImmutableBiMap.class : ImmutableMap.class; + constructor = cls.getDeclaredConstructor(mapClass); + constructor.setAccessible(true); + readResolveMethod = findReadResolve(cls); + readResolveMethod.setAccessible(true); + } catch (ReflectiveOperationException e) { + throw new ForyException( + "Failed to initialize Guava serialized-form serializer for " + cls, e); + } + } + + @Override + public void write(WriteContext writeContext, Object value) { + Map map = readFormMap(value); + MemoryBuffer buffer = writeContext.getBuffer(); + buffer.writeVarUInt32Small7(map.size()); + for (Entry entry : map.entrySet()) { + writeContext.writeRef(entry.getKey()); + writeContext.writeRef(entry.getValue()); + } + } + + @Override + public Object read(ReadContext readContext) { + MemoryBuffer buffer = readContext.getBuffer(); + int size = buffer.readVarUInt32Small7(); + ImmutableMap.Builder builder = + biMap ? newImmutableBiMapBuilder(size) : newImmutableMapBuilder(size); + for (int i = 0; i < size; i++) { + builder.put(readContext.readRef(), readContext.readRef()); + } + return builder.build(); + } + + @Override + public Object copy(CopyContext copyContext, Object value) { + Map map = readFormMap(value); + ImmutableMap.Builder builder = + biMap ? newImmutableBiMapBuilder(map.size()) : newImmutableMapBuilder(map.size()); + for (Entry entry : map.entrySet()) { + Object key = entry.getKey(); + Object copyKey = key == null ? null : copyContext.copyObject(key); + Object itemValue = entry.getValue(); + Object copyValue = itemValue == null ? null : copyContext.copyObject(itemValue); + builder.put(copyKey, copyValue); + } + return newForm(builder.build()); + } + + private Map readFormMap(Object value) { + try { + return (Map) readResolveMethod.invoke(value); + } catch (ReflectiveOperationException e) { + throw new ForyException("Failed to resolve Guava serialized form " + type, e); + } + } + + private Object newForm(Map map) { + try { + Object guavaMap = biMap ? ImmutableBiMap.copyOf(map) : ImmutableMap.copyOf(map); + return constructor.newInstance(guavaMap); + } catch (ReflectiveOperationException e) { + throw new ForyException("Failed to create Guava serialized form " + type, e); + } + } + + private static Method findReadResolve(Class cls) throws NoSuchMethodException { + Class current = cls; + while (current != null) { + try { + return current.getDeclaredMethod("readResolve"); + } catch (NoSuchMethodException e) { + current = current.getSuperclass(); + } + } + throw new NoSuchMethodException(cls.getName() + ".readResolve()"); + } + } + + public static final class ImmutableMapFormSerializer extends GuavaMapFormSerializer { + public ImmutableMapFormSerializer(TypeResolver typeResolver, Class cls) { + super(typeResolver, cls, false); + } + } + + public static final class ImmutableBiMapFormSerializer extends GuavaMapFormSerializer { + public ImmutableBiMapFormSerializer(TypeResolver typeResolver, Class cls) { + super(typeResolver, cls, true); + } + } + + public static final class ImmutableIntArraySerializer extends Serializer { + + public ImmutableIntArraySerializer(TypeResolver typeResolver, Class cls) { + super(typeResolver.getConfig(), cls); + } + + @Override + public void write(WriteContext writeContext, ImmutableIntArray value) { + MemoryBuffer buffer = writeContext.getBuffer(); + int length = value.length(); + buffer.writeVarUInt32Small7(length); + for (int i = 0; i < length; i++) { + buffer.writeVarInt32(value.get(i)); + } + } + + @Override + public ImmutableIntArray read(ReadContext readContext) { + MemoryBuffer buffer = readContext.getBuffer(); + int length = buffer.readVarUInt32Small7(); + int[] values = new int[length]; + for (int i = 0; i < length; i++) { + values[i] = buffer.readVarInt32(); + } + return ImmutableIntArray.copyOf(values); + } + + @Override + public ImmutableIntArray copy(CopyContext copyContext, ImmutableIntArray value) { + return value; + } + } + + public static final class HashBasedTableSerializer extends Serializer { + + public HashBasedTableSerializer(TypeResolver typeResolver, Class cls) { + super(typeResolver.getConfig(), cls); + typeResolver.setSerializer(cls, this); + } + + @Override + public void write(WriteContext writeContext, HashBasedTable value) { + MemoryBuffer buffer = writeContext.getBuffer(); + buffer.writeVarUInt32Small7(value.size()); + for (Table.Cell cell : ((HashBasedTable) value).cellSet()) { + writeContext.writeRef(cell.getRowKey()); + writeContext.writeRef(cell.getColumnKey()); + writeContext.writeRef(cell.getValue()); + } + } + + @Override + public HashBasedTable read(ReadContext readContext) { + MemoryBuffer buffer = readContext.getBuffer(); + int size = buffer.readVarUInt32Small7(); + HashBasedTable table = HashBasedTable.create(); + if (needToWriteRef) { + readContext.setReadRef(readContext.lastPreservedRefId(), table); + } + for (int i = 0; i < size; i++) { + table.put(readContext.readRef(), readContext.readRef(), readContext.readRef()); + } + return table; + } + + @Override + public HashBasedTable copy(CopyContext copyContext, HashBasedTable value) { + HashBasedTable table = HashBasedTable.create(); + if (needToCopyRef) { + copyContext.reference(value, table); + } + for (Table.Cell cell : ((HashBasedTable) value).cellSet()) { + table.put( + copyContext.copyObject(cell.getRowKey()), + copyContext.copyObject(cell.getColumnKey()), + copyContext.copyObject(cell.getValue())); + } + return table; + } + } + public static final class ImmutableSortedMapSerializer extends MapSerializer { @@ -380,7 +576,7 @@ public Map newMap(ReadContext readContext) { @Override public T copy(CopyContext copyContext, T originMap) { - Comparator comparator = copyContext.copyObject(originMap.comparator()); + Comparator comparator = ComparatorCopy.copy(copyContext, originMap.comparator()); ImmutableSortedMap.Builder builder = new ImmutableSortedMap.Builder(comparator); copyEntries(typeResolver, mapTypeCache(), copyContext, originMap, builder::put); return (T) builder.build(); @@ -403,13 +599,8 @@ public T onMapRead(Map map) { // TODO guava serializers // guava/ArrayListMultimapSerializer - serializer for guava-libraries' ArrayListMultimap // guava/ArrayTableSerializer - serializer for guava-libraries' ArrayTable - // guava/HashBasedTableSerializer - serializer for guava-libraries' HashBasedTable // guava/HashMultimapSerializer -- serializer for guava-libraries' HashMultimap - // guava/ImmutableListSerializer - serializer for guava-libraries' ImmutableList - // guava/ImmutableSetSerializer - serializer for guava-libraries' ImmutableSet - // guava/ImmutableMapSerializer - serializer for guava-libraries' ImmutableMap // guava/ImmutableMultimapSerializer - serializer for guava-libraries' ImmutableMultimap - // guava/ImmutableSortedSetSerializer - serializer for guava-libraries' ImmutableSortedSet // guava/ImmutableTableSerializer - serializer for guava-libraries' ImmutableTable // guava/LinkedHashMultimapSerializer - serializer for guava-libraries' LinkedHashMultimap // guava/LinkedListMultimapSerializer - serializer for guava-libraries' LinkedListMultimap @@ -491,6 +682,21 @@ class GuavaEmptySortedMap {} } } + public static Class getSerializerClass(Class cls) { + switch (cls.getName()) { + case IMMUTABLE_INT_ARRAY_CLASS_NAME: + return ImmutableIntArraySerializer.class; + case IMMUTABLE_MAP_FORM_CLASS_NAME: + return ImmutableMapFormSerializer.class; + case IMMUTABLE_BI_MAP_FORM_CLASS_NAME: + return ImmutableBiMapFormSerializer.class; + case HASH_BASED_TABLE_CLASS_NAME: + return HashBasedTableSerializer.class; + default: + return null; + } + } + static Class loadClass(String className, Class cache) { if (cache.getName().equals(className)) { return cache; diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ImmutableCollectionSerializers.java b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ImmutableCollectionSerializers.java index 08c40435ed..3eef7973c2 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ImmutableCollectionSerializers.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ImmutableCollectionSerializers.java @@ -32,11 +32,11 @@ import org.apache.fory.context.CopyContext; import org.apache.fory.context.ReadContext; import org.apache.fory.memory.MemoryBuffer; -import org.apache.fory.platform.AndroidSupport; +import org.apache.fory.memory.MemoryUtils; import org.apache.fory.platform.JdkVersion; +import org.apache.fory.platform.internal._JDKAccess; import org.apache.fory.resolver.TypeResolver; import org.apache.fory.util.ExceptionUtils; -import org.apache.fory.util.unsafe._JDKAccess; /** Serializers for jdk9+ java.util.ImmutableCollections. */ @SuppressWarnings({"unchecked", "rawtypes"}) @@ -63,7 +63,7 @@ public class ImmutableCollectionSerializers { SetN = Class.forName("java.util.ImmutableCollections$SetN"); Map1 = Class.forName("java.util.ImmutableCollections$Map1"); MapN = Class.forName("java.util.ImmutableCollections$MapN"); - if (!AndroidSupport.IS_ANDROID) { + if (MemoryUtils.JDK_COLLECTION_FIELD_ACCESS) { listFactory = _JDKAccess._trustedLookup(List.class) .findStatic(List.class, "of", MethodType.methodType(List.class, Object[].class)); @@ -79,7 +79,7 @@ public class ImmutableCollectionSerializers { .findConstructor(MapN, MethodType.methodType(void.class, Object[].class)); } } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) { - if (AndroidSupport.IS_ANDROID) { + if (!MemoryUtils.JDK_COLLECTION_FIELD_ACCESS) { useStubClasses(); } else { e.printStackTrace(); @@ -144,7 +144,7 @@ public Collection copy(CopyContext copyContext, Collection originCollection) { } Object[] elements = new Object[originCollection.size()]; copyElements(copyContext, originCollection, elements); - if (AndroidSupport.IS_ANDROID) { + if (!MemoryUtils.JDK_COLLECTION_FIELD_ACCESS) { ArrayList list = new ArrayList(elements.length); Collections.addAll(list, elements); return Collections.unmodifiableList(list); @@ -160,7 +160,7 @@ public Collection copy(CopyContext copyContext, Collection originCollection) { public Collection onCollectionRead(Collection collection) { if (JdkVersion.MAJOR_VERSION > 8) { CollectionContainer container = (CollectionContainer) collection; - if (AndroidSupport.IS_ANDROID) { + if (!MemoryUtils.JDK_COLLECTION_FIELD_ACCESS) { ArrayList list = new ArrayList(container.elements.length); Collections.addAll(list, container.elements); return Collections.unmodifiableList(list); @@ -204,7 +204,7 @@ public Collection copy(CopyContext copyContext, Collection originCollection) { } Object[] elements = new Object[originCollection.size()]; copyElements(copyContext, originCollection, elements); - if (AndroidSupport.IS_ANDROID) { + if (!MemoryUtils.JDK_COLLECTION_FIELD_ACCESS) { HashSet set = new HashSet(elements.length); Collections.addAll(set, elements); return Collections.unmodifiableSet(set); @@ -220,7 +220,7 @@ public Collection copy(CopyContext copyContext, Collection originCollection) { public Collection onCollectionRead(Collection collection) { if (JdkVersion.MAJOR_VERSION > 8) { CollectionContainer container = (CollectionContainer) collection; - if (AndroidSupport.IS_ANDROID) { + if (!MemoryUtils.JDK_COLLECTION_FIELD_ACCESS) { HashSet set = new HashSet(container.elements.length); Collections.addAll(set, container.elements); return Collections.unmodifiableSet(set); @@ -265,7 +265,7 @@ public Map copy(CopyContext copyContext, Map originMap) { int size = originMap.size(); Object[] elements = new Object[size * 2]; copyEntry(copyContext, originMap, elements); - if (AndroidSupport.IS_ANDROID) { + if (!MemoryUtils.JDK_COLLECTION_FIELD_ACCESS) { return Collections.unmodifiableMap(newHashMap(elements, size)); } try { @@ -283,7 +283,7 @@ public Map copy(CopyContext copyContext, Map originMap) { public Map onMapRead(Map map) { if (JdkVersion.MAJOR_VERSION > 8) { JDKImmutableMapContainer container = (JDKImmutableMapContainer) map; - if (AndroidSupport.IS_ANDROID) { + if (!MemoryUtils.JDK_COLLECTION_FIELD_ACCESS) { return Collections.unmodifiableMap(newHashMap(container.array, container.size())); } try { diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/MapSerializers.java b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/MapSerializers.java index 43a464b12f..9b3825495a 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/MapSerializers.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/MapSerializers.java @@ -34,6 +34,7 @@ import java.util.Comparator; import java.util.EnumMap; import java.util.HashMap; +import java.util.IdentityHashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; @@ -65,6 +66,17 @@ */ @SuppressWarnings({"unchecked", "rawtypes"}) public class MapSerializers { + private static final Comparator NATURAL_ORDER_COMPARATOR = Comparator.naturalOrder(); + + private static void requireXlangNaturalOrdering(Class type, Comparator comparator) { + if (comparator != null && comparator != NATURAL_ORDER_COMPARATOR) { + throw new UnsupportedOperationException( + "Xlang serialization of " + + type.getName() + + " with a custom comparator is unsupported because the xlang map wire format " + + "does not encode comparators"); + } + } public static final class HashMapSerializer extends MapSerializer { public HashMapSerializer(TypeResolver typeResolver) { @@ -108,6 +120,24 @@ public Map newMap(Map map) { } } + public static final class IdentityHashMapSerializer + extends JDKCompatibleMapSerializer { + public IdentityHashMapSerializer(TypeResolver typeResolver) { + super(typeResolver, IdentityHashMap.class); + } + + @Override + public IdentityHashMap copy(CopyContext copyContext, IdentityHashMap value) { + IdentityHashMap copy = new IdentityHashMap(value.size()); + copyContext.reference(value, copy); + for (Object entryObject : value.entrySet()) { + Entry entry = (Entry) entryObject; + copy.put(copyContext.copyObject(entry.getKey()), copyContext.copyObject(entry.getValue())); + } + return copy; + } + } + public static final class LazyMapSerializer extends MapSerializer { public LazyMapSerializer(TypeResolver typeResolver) { super(typeResolver, LazyMap.class, true); @@ -153,6 +183,9 @@ public SortedMapSerializer(TypeResolver typeResolver, Class cls) { @Override public Map onMapWrite(WriteContext writeContext, T value) { + if (config.isXlang()) { + requireXlangNaturalOrdering(type, value.comparator()); + } MemoryBuffer buffer = writeContext.getBuffer(); buffer.writeVarUInt32Small7(value.size()); if (config.isXlang()) { @@ -166,11 +199,10 @@ public Map onMapWrite(WriteContext writeContext, T value) { @SuppressWarnings("unchecked") @Override public Map newMap(ReadContext readContext) { - assert !config.isXlang(); MemoryBuffer buffer = readContext.getBuffer(); setNumElements(readMapSize(buffer)); T map; - Comparator comparator = (Comparator) readContext.readRef(); + Comparator comparator = config.isXlang() ? null : (Comparator) readContext.readRef(); if (type == TreeMap.class) { map = (T) new TreeMap(comparator); } else { @@ -190,7 +222,8 @@ public Map newMap(ReadContext readContext) { @Override public Map newMap(CopyContext copyContext, Map originMap) { - Comparator comparator = copyContext.copyObject(((SortedMap) originMap).comparator()); + Comparator comparator = + ComparatorCopy.copy(copyContext, ((SortedMap) originMap).comparator()); Map map; if (type == TreeMap.class) { map = new TreeMap(comparator); @@ -312,6 +345,9 @@ public ConcurrentSkipListMapSerializer( @Override public MapSnapshot onMapWrite(WriteContext writeContext, ConcurrentSkipListMap value) { + if (config.isXlang()) { + requireXlangNaturalOrdering(type, value.comparator()); + } MapSnapshot snapshot = super.onMapWrite(writeContext, value); if (config.isXlang()) { return snapshot; @@ -325,7 +361,7 @@ public ConcurrentSkipListMap newMap(ReadContext readContext) { MemoryBuffer buffer = readContext.getBuffer(); int numElements = readMapSize(buffer); setNumElements(numElements); - Comparator comparator = (Comparator) readContext.readRef(); + Comparator comparator = config.isXlang() ? null : (Comparator) readContext.readRef(); ConcurrentSkipListMap map = new ConcurrentSkipListMap(comparator); readContext.reference(map); return map; @@ -334,7 +370,7 @@ public ConcurrentSkipListMap newMap(ReadContext readContext) { @Override public Map newMap(CopyContext copyContext, Map originMap) { Comparator comparator = - copyContext.copyObject(((ConcurrentSkipListMap) originMap).comparator()); + ComparatorCopy.copy(copyContext, ((ConcurrentSkipListMap) originMap).comparator()); return new ConcurrentSkipListMap(comparator); } } diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/SubListSerializers.java b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/SubListSerializers.java index 6aa9158e70..f11f4b79a3 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/SubListSerializers.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/SubListSerializers.java @@ -31,8 +31,8 @@ import org.apache.fory.logging.Logger; import org.apache.fory.logging.LoggerFactory; import org.apache.fory.memory.MemoryBuffer; -import org.apache.fory.platform.AndroidSupport; -import org.apache.fory.platform.UnsafeOps; +import org.apache.fory.memory.MemoryUtils; +import org.apache.fory.reflect.FieldAccessor; import org.apache.fory.resolver.TypeResolver; @SuppressWarnings({"rawtypes", "unchecked"}) @@ -210,18 +210,19 @@ private ViewPayload(List source, int offset, int size) { } private static final class ViewFields { - private final long sourceOffset; - private final long offsetOffset; - private final long sizeOffset; + private final FieldAccessor sourceAccessor; + private final FieldAccessor offsetAccessor; + private final FieldAccessor sizeAccessor; - private ViewFields(long sourceOffset, long offsetOffset, long sizeOffset) { - this.sourceOffset = sourceOffset; - this.offsetOffset = offsetOffset; - this.sizeOffset = sizeOffset; + private ViewFields( + FieldAccessor sourceAccessor, FieldAccessor offsetAccessor, FieldAccessor sizeAccessor) { + this.sourceAccessor = sourceAccessor; + this.offsetAccessor = offsetAccessor; + this.sizeAccessor = sizeAccessor; } private static ViewFields create(Class type) { - if (AndroidSupport.IS_ANDROID || Stub.class.isAssignableFrom(type)) { + if (!MemoryUtils.JDK_COLLECTION_FIELD_ACCESS || Stub.class.isAssignableFrom(type)) { return null; } Class cls = type; @@ -282,18 +283,18 @@ private static ViewFields createFromDeclaredFields(Class type) { return null; } return new ViewFields( - UnsafeOps.objectFieldOffset(sourceField), - UnsafeOps.objectFieldOffset(offsetField), - UnsafeOps.objectFieldOffset(sizeField)); + FieldAccessor.createAccessor(sourceField), + FieldAccessor.createAccessor(offsetField), + FieldAccessor.createAccessor(sizeField)); } private ViewPayload get(Collection value) { - List source = (List) UnsafeOps.getObject(value, sourceOffset); + List source = (List) sourceAccessor.getObject(value); if (source == null) { return null; } - int offset = UnsafeOps.getInt(value, offsetOffset); - int size = UnsafeOps.getInt(value, sizeOffset); + int offset = offsetAccessor.getInt(value); + int size = sizeAccessor.getInt(value); return new ViewPayload(source, offset, size); } } diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/SynchronizedSerializers.java b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/SynchronizedSerializers.java index bf2b4b486e..da66076745 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/SynchronizedSerializers.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/SynchronizedSerializers.java @@ -40,8 +40,8 @@ import org.apache.fory.context.WriteContext; import org.apache.fory.logging.Logger; import org.apache.fory.logging.LoggerFactory; -import org.apache.fory.platform.AndroidSupport; -import org.apache.fory.platform.UnsafeOps; +import org.apache.fory.memory.MemoryUtils; +import org.apache.fory.reflect.FieldAccessor; import org.apache.fory.resolver.TypeInfo; import org.apache.fory.resolver.TypeResolver; import org.apache.fory.serializer.Serializer; @@ -53,25 +53,23 @@ public class SynchronizedSerializers { private static final Logger LOG = LoggerFactory.getLogger(SynchronizedSerializers.class); - private static class Offset { - // Graalvm unsafe offset substitution support: Make the call followed by a field store - // directly or by a sign extend node followed directly by a field store. - private static final long SOURCE_COLLECTION_FIELD_OFFSET; - private static final long SOURCE_MAP_FIELD_OFFSET; + private static class SourceAccessors { + private static final FieldAccessor SOURCE_COLLECTION_ACCESSOR; + private static final FieldAccessor SOURCE_MAP_ACCESSOR; static { String clsName = "java.util.Collections$SynchronizedCollection"; try { - SOURCE_COLLECTION_FIELD_OFFSET = - UnsafeOps.UNSAFE.objectFieldOffset(Class.forName(clsName).getDeclaredField("c")); + SOURCE_COLLECTION_ACCESSOR = + FieldAccessor.createAccessor(Class.forName(clsName).getDeclaredField("c")); } catch (Exception e) { LOG.info("Could not access source collection field in {}", clsName); throw new RuntimeException(e); } clsName = "java.util.Collections$SynchronizedMap"; try { - SOURCE_MAP_FIELD_OFFSET = - UnsafeOps.UNSAFE.objectFieldOffset(Class.forName(clsName).getDeclaredField("m")); + SOURCE_MAP_ACCESSOR = + FieldAccessor.createAccessor(Class.forName(clsName).getDeclaredField("m")); } catch (Exception e) { LOG.info("Could not access source map field in {}", clsName); throw new RuntimeException(e); @@ -83,19 +81,19 @@ public static final class SynchronizedCollectionSerializer extends CollectionSerializer { private final Function factory; - private final long offset; + private final FieldAccessor sourceAccessor; public SynchronizedCollectionSerializer( - TypeResolver typeResolver, Class cls, Function factory, long offset) { + TypeResolver typeResolver, Class cls, Function factory, FieldAccessor sourceAccessor) { super(typeResolver, cls, false); this.factory = factory; - this.offset = offset; + this.sourceAccessor = sourceAccessor; } @Override public void write(WriteContext writeContext, Collection object) { Preconditions.checkArgument(object.getClass() == type); - if (AndroidSupport.IS_ANDROID) { + if (!MemoryUtils.JDK_COLLECTION_FIELD_ACCESS) { synchronized (object) { Collection source; if (object instanceof SortedSet) { @@ -111,7 +109,7 @@ public void write(WriteContext writeContext, Collection object) { return; } // the ordinal could be replaced by s.th. else (e.g. a explicitly managed "id") - Object unwrapped = UnsafeOps.getObject(object, offset); + Object unwrapped = sourceAccessor.getObject(object); synchronized (object) { writeContext.writeRef(unwrapped); } @@ -125,11 +123,11 @@ public Collection read(ReadContext readContext) { @Override public Collection copy(CopyContext copyContext, Collection object) { - if (AndroidSupport.IS_ANDROID) { + if (!MemoryUtils.JDK_COLLECTION_FIELD_ACCESS) { synchronized (object) { Collection mutableSource; if (object instanceof SortedSet) { - Object comparator = copyContext.copyObject(((SortedSet) object).comparator()); + Object comparator = ComparatorCopy.copy(copyContext, ((SortedSet) object).comparator()); mutableSource = new TreeSet((java.util.Comparator) comparator); } else if (object instanceof Set) { mutableSource = new HashSet(object.size()); @@ -142,26 +140,26 @@ public Collection copy(CopyContext copyContext, Collection object) { return result; } } - final Object collection = UnsafeOps.getObject(object, offset); + final Object collection = sourceAccessor.getObject(object); return (Collection) factory.apply(copyContext.copyObject(collection)); } } public static final class SynchronizedMapSerializer extends MapSerializer { private final Function factory; - private final long offset; + private final FieldAccessor sourceAccessor; public SynchronizedMapSerializer( - TypeResolver typeResolver, Class cls, Function factory, long offset) { + TypeResolver typeResolver, Class cls, Function factory, FieldAccessor sourceAccessor) { super(typeResolver, cls, false); this.factory = factory; - this.offset = offset; + this.sourceAccessor = sourceAccessor; } @Override public void write(WriteContext writeContext, Map object) { Preconditions.checkArgument(object.getClass() == type); - if (AndroidSupport.IS_ANDROID) { + if (!MemoryUtils.JDK_COLLECTION_FIELD_ACCESS) { synchronized (object) { Map source; if (object instanceof SortedMap) { @@ -175,7 +173,7 @@ public void write(WriteContext writeContext, Map object) { return; } // the ordinal could be replaced by s.th. else (e.g. a explicitly managed "id") - Object unwrapped = UnsafeOps.getObject(object, offset); + Object unwrapped = sourceAccessor.getObject(object); synchronized (object) { writeContext.writeRef(unwrapped); } @@ -183,11 +181,12 @@ public void write(WriteContext writeContext, Map object) { @Override public Map copy(CopyContext copyContext, Map originMap) { - if (AndroidSupport.IS_ANDROID) { + if (!MemoryUtils.JDK_COLLECTION_FIELD_ACCESS) { synchronized (originMap) { Map mutableSource; if (originMap instanceof SortedMap) { - Object comparator = copyContext.copyObject(((SortedMap) originMap).comparator()); + Object comparator = + ComparatorCopy.copy(copyContext, ((SortedMap) originMap).comparator()); mutableSource = new TreeMap((java.util.Comparator) comparator); } else { mutableSource = new HashMap(originMap.size()); @@ -198,7 +197,7 @@ public Map copy(CopyContext copyContext, Map originMap) { return result; } } - final Object unwrappedMap = UnsafeOps.getObject(originMap, offset); + final Object unwrappedMap = sourceAccessor.getObject(originMap); return (Map) factory.apply(copyContext.copyObject(unwrappedMap)); } @@ -225,13 +224,15 @@ private static Serializer createSerializer( typeResolver, factory.f0, factory.f1, - AndroidSupport.IS_ANDROID ? -1 : Offset.SOURCE_COLLECTION_FIELD_OFFSET); + MemoryUtils.JDK_COLLECTION_FIELD_ACCESS + ? SourceAccessors.SOURCE_COLLECTION_ACCESSOR + : null); } else { return new SynchronizedMapSerializer( typeResolver, factory.f0, factory.f1, - AndroidSupport.IS_ANDROID ? -1 : Offset.SOURCE_MAP_FIELD_OFFSET); + MemoryUtils.JDK_COLLECTION_FIELD_ACCESS ? SourceAccessors.SOURCE_MAP_ACCESSOR : null); } } diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/UnmodifiableSerializers.java b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/UnmodifiableSerializers.java index 0bf27e9133..ec96be1999 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/UnmodifiableSerializers.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/UnmodifiableSerializers.java @@ -39,8 +39,8 @@ import org.apache.fory.context.WriteContext; import org.apache.fory.logging.Logger; import org.apache.fory.logging.LoggerFactory; -import org.apache.fory.platform.AndroidSupport; -import org.apache.fory.platform.UnsafeOps; +import org.apache.fory.memory.MemoryUtils; +import org.apache.fory.reflect.FieldAccessor; import org.apache.fory.resolver.TypeInfo; import org.apache.fory.resolver.TypeResolver; import org.apache.fory.serializer.Serializer; @@ -52,18 +52,16 @@ public class UnmodifiableSerializers { private static final Logger LOG = LoggerFactory.getLogger(UnmodifiableSerializers.class); - private static class Offset { - // Graalvm unsafe offset substitution support: Make the call followed by a field store - // directly or by a sign extend node followed directly by a field store. - private static final long SOURCE_COLLECTION_FIELD_OFFSET; - private static final long SOURCE_MAP_FIELD_OFFSET; + private static class SourceAccessors { + private static final FieldAccessor SOURCE_COLLECTION_ACCESSOR; + private static final FieldAccessor SOURCE_MAP_ACCESSOR; static { // UnmodifiableList/Set/Etc.. extends UnmodifiableCollection String clsName = "java.util.Collections$UnmodifiableCollection"; try { - SOURCE_COLLECTION_FIELD_OFFSET = - UnsafeOps.UNSAFE.objectFieldOffset(Class.forName(clsName).getDeclaredField("c")); + SOURCE_COLLECTION_ACCESSOR = + FieldAccessor.createAccessor(Class.forName(clsName).getDeclaredField("c")); } catch (Exception e) { LOG.info("Could not access source collection field in {}", clsName); throw new RuntimeException(e); @@ -71,8 +69,8 @@ private static class Offset { clsName = "java.util.Collections$UnmodifiableMap"; try { // UnmodifiableSortedMap/UnmodifiableNavigableMap extends UnmodifiableMap - SOURCE_MAP_FIELD_OFFSET = - UnsafeOps.UNSAFE.objectFieldOffset(Class.forName(clsName).getDeclaredField("m")); + SOURCE_MAP_ACCESSOR = + FieldAccessor.createAccessor(Class.forName(clsName).getDeclaredField("m")); } catch (Exception e) { LOG.info("Could not access source map field in {}", clsName); throw new RuntimeException(e); @@ -83,19 +81,19 @@ private static class Offset { public static final class UnmodifiableCollectionSerializer extends CollectionSerializer { private final Function factory; - private final long offset; + private final FieldAccessor sourceAccessor; public UnmodifiableCollectionSerializer( - TypeResolver typeResolver, Class cls, Function factory, long offset) { + TypeResolver typeResolver, Class cls, Function factory, FieldAccessor sourceAccessor) { super(typeResolver, cls, false); this.factory = factory; - this.offset = offset; + this.sourceAccessor = sourceAccessor; } @Override public void write(WriteContext writeContext, Collection value) { Preconditions.checkArgument(value.getClass() == type); - if (AndroidSupport.IS_ANDROID) { + if (!MemoryUtils.JDK_COLLECTION_FIELD_ACCESS) { Collection source; if (value instanceof SortedSet) { source = new TreeSet(((SortedSet) value).comparator()); @@ -108,7 +106,7 @@ public void write(WriteContext writeContext, Collection value) { writeContext.writeRef(source, sourceCollectionTypeInfo(typeResolver, type)); return; } - writeContext.writeRef(UnsafeOps.getObject(value, offset)); + writeContext.writeRef(sourceAccessor.getObject(value)); } @Override @@ -118,10 +116,10 @@ public Collection read(ReadContext readContext) { @Override public Collection copy(CopyContext copyContext, Collection object) { - if (AndroidSupport.IS_ANDROID) { + if (!MemoryUtils.JDK_COLLECTION_FIELD_ACCESS) { Collection mutableSource; if (object instanceof SortedSet) { - Object comparator = copyContext.copyObject(((SortedSet) object).comparator()); + Object comparator = ComparatorCopy.copy(copyContext, ((SortedSet) object).comparator()); mutableSource = new TreeSet((java.util.Comparator) comparator); } else if (object instanceof Set) { mutableSource = new HashSet(object.size()); @@ -133,26 +131,25 @@ public Collection copy(CopyContext copyContext, Collection object) { copyElements(copyContext, object, mutableSource); return result; } - return (Collection) - factory.apply(copyContext.copyObject(UnsafeOps.getObject(object, offset))); + return (Collection) factory.apply(copyContext.copyObject(sourceAccessor.getObject(object))); } } public static final class UnmodifiableMapSerializer extends MapSerializer { private final Function factory; - private final long offset; + private final FieldAccessor sourceAccessor; public UnmodifiableMapSerializer( - TypeResolver typeResolver, Class cls, Function factory, long offset) { + TypeResolver typeResolver, Class cls, Function factory, FieldAccessor sourceAccessor) { super(typeResolver, cls, false); this.factory = factory; - this.offset = offset; + this.sourceAccessor = sourceAccessor; } @Override public void write(WriteContext writeContext, Map value) { Preconditions.checkArgument(value.getClass() == type); - if (AndroidSupport.IS_ANDROID) { + if (!MemoryUtils.JDK_COLLECTION_FIELD_ACCESS) { Map source; if (value instanceof SortedMap) { source = new TreeMap(((SortedMap) value).comparator()); @@ -163,15 +160,16 @@ public void write(WriteContext writeContext, Map value) { writeContext.writeRef(source, sourceMapTypeInfo(typeResolver, type)); return; } - writeContext.writeRef(UnsafeOps.getObject(value, offset)); + writeContext.writeRef(sourceAccessor.getObject(value)); } @Override public Map copy(CopyContext copyContext, Map originMap) { - if (AndroidSupport.IS_ANDROID) { + if (!MemoryUtils.JDK_COLLECTION_FIELD_ACCESS) { Map mutableSource; if (originMap instanceof SortedMap) { - Object comparator = copyContext.copyObject(((SortedMap) originMap).comparator()); + Object comparator = + ComparatorCopy.copy(copyContext, ((SortedMap) originMap).comparator()); mutableSource = new TreeMap((java.util.Comparator) comparator); } else { mutableSource = new HashMap(originMap.size()); @@ -181,7 +179,7 @@ public Map copy(CopyContext copyContext, Map originMap) { copyEntry(copyContext, originMap, mutableSource); return result; } - return (Map) factory.apply(copyContext.copyObject(UnsafeOps.getObject(originMap, offset))); + return (Map) factory.apply(copyContext.copyObject(sourceAccessor.getObject(originMap))); } @Override @@ -206,13 +204,15 @@ private static Serializer createSerializer( typeResolver, factory.f0, factory.f1, - AndroidSupport.IS_ANDROID ? -1 : Offset.SOURCE_COLLECTION_FIELD_OFFSET); + MemoryUtils.JDK_COLLECTION_FIELD_ACCESS + ? SourceAccessors.SOURCE_COLLECTION_ACCESSOR + : null); } else { return new UnmodifiableMapSerializer( typeResolver, factory.f0, factory.f1, - AndroidSupport.IS_ANDROID ? -1 : Offset.SOURCE_MAP_FIELD_OFFSET); + MemoryUtils.JDK_COLLECTION_FIELD_ACCESS ? SourceAccessors.SOURCE_MAP_ACCESSOR : null); } } diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/scala/SingletonCollectionSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/scala/SingletonCollectionSerializer.java index 9d96e02274..4ed73cceac 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/scala/SingletonCollectionSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/scala/SingletonCollectionSerializer.java @@ -19,17 +19,16 @@ package org.apache.fory.serializer.scala; +import java.lang.invoke.MethodHandle; import java.lang.reflect.Field; import java.util.Collection; import org.apache.fory.context.ReadContext; import org.apache.fory.context.WriteContext; 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.resolver.TypeResolver; import org.apache.fory.serializer.collection.CollectionLikeSerializer; -import org.apache.fory.util.Preconditions; /** * Singleton serializer for scala collection. We need this serializer for fory jit serialization, @@ -39,8 +38,7 @@ @SuppressWarnings("rawtypes") public class SingletonCollectionSerializer extends CollectionLikeSerializer { private final Field field; - private Object base = null; - private long offset = -1; + private MethodHandle accessor; public SingletonCollectionSerializer(TypeResolver typeResolver, Class cls) { super(typeResolver, cls, false); @@ -78,13 +76,24 @@ public Object read(ReadContext readContext) { throw new ForyException("Failed to read Scala singleton field: " + type, e); } } - long offset = this.offset; - if (offset == -1) { - Preconditions.checkArgument(!GraalvmSupport.isGraalBuildTime()); - offset = this.offset = UnsafeOps.UNSAFE.staticFieldOffset(field); - base = UnsafeOps.UNSAFE.staticFieldBase(field); + MethodHandle accessor = this.accessor; + if (accessor == null) { + accessor = this.accessor = staticGetter(); + } + try { + return accessor.invoke(); + } catch (Throwable e) { + throw new ForyException("Failed to read Scala singleton field: " + type, e); + } + } + + private MethodHandle staticGetter() { + try { + return _JDKAccess._trustedLookup(field.getDeclaringClass()) + .findStaticGetter(field.getDeclaringClass(), field.getName(), field.getType()); + } catch (NoSuchFieldException | IllegalAccessException | RuntimeException e) { + throw new ForyException("Failed to access Scala singleton field: " + type, e); } - return UnsafeOps.getObject(base, offset); } @Override diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/scala/SingletonMapSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/scala/SingletonMapSerializer.java index f409d811b9..58813c094b 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/scala/SingletonMapSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/scala/SingletonMapSerializer.java @@ -19,17 +19,16 @@ package org.apache.fory.serializer.scala; +import java.lang.invoke.MethodHandle; import java.lang.reflect.Field; import java.util.Map; import org.apache.fory.context.ReadContext; import org.apache.fory.context.WriteContext; 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.resolver.TypeResolver; import org.apache.fory.serializer.collection.MapLikeSerializer; -import org.apache.fory.util.Preconditions; /** * Singleton serializer for scala map. We need this serializer for fory jit serialization, otherwise @@ -39,8 +38,7 @@ @SuppressWarnings("rawtypes") public class SingletonMapSerializer extends MapLikeSerializer { private final Field field; - private Object base = null; - private long offset = -1; + private MethodHandle accessor; public SingletonMapSerializer(TypeResolver typeResolver, Class cls) { super(typeResolver, cls, false); @@ -78,13 +76,24 @@ public Object read(ReadContext readContext) { throw new ForyException("Failed to read Scala singleton field: " + type, e); } } - long offset = this.offset; - if (offset == -1) { - Preconditions.checkArgument(!GraalvmSupport.isGraalBuildTime()); - offset = this.offset = UnsafeOps.UNSAFE.staticFieldOffset(field); - base = UnsafeOps.UNSAFE.staticFieldBase(field); + MethodHandle accessor = this.accessor; + if (accessor == null) { + accessor = this.accessor = staticGetter(); + } + try { + return accessor.invoke(); + } catch (Throwable e) { + throw new ForyException("Failed to read Scala singleton field: " + type, e); + } + } + + private MethodHandle staticGetter() { + try { + return _JDKAccess._trustedLookup(field.getDeclaringClass()) + .findStaticGetter(field.getDeclaringClass(), field.getName(), field.getType()); + } catch (NoSuchFieldException | IllegalAccessException | RuntimeException e) { + throw new ForyException("Failed to access Scala singleton field: " + type, e); } - return UnsafeOps.getObject(base, offset); } @Override diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/scala/SingletonObjectSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/scala/SingletonObjectSerializer.java index acd991ab93..7c62236a5e 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/scala/SingletonObjectSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/scala/SingletonObjectSerializer.java @@ -19,16 +19,15 @@ package org.apache.fory.serializer.scala; +import java.lang.invoke.MethodHandle; import java.lang.reflect.Field; import org.apache.fory.context.ReadContext; import org.apache.fory.context.WriteContext; 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.resolver.TypeResolver; import org.apache.fory.serializer.Serializer; -import org.apache.fory.util.Preconditions; /** * Serializer for scala @@ -37,8 +36,7 @@ @SuppressWarnings("rawtypes") public class SingletonObjectSerializer extends Serializer { private final Field field; - private Object base = null; - private long offset = -1; + private MethodHandle accessor; public SingletonObjectSerializer(TypeResolver typeResolver, Class type) { super(typeResolver.getConfig(), type); @@ -71,12 +69,23 @@ public Object read(ReadContext readContext) { throw new ForyException("Failed to read Scala singleton field: " + type, e); } } - long offset = this.offset; - if (offset == -1) { - Preconditions.checkArgument(!GraalvmSupport.isGraalBuildTime()); - offset = this.offset = UnsafeOps.UNSAFE.staticFieldOffset(field); - base = UnsafeOps.UNSAFE.staticFieldBase(field); + MethodHandle accessor = this.accessor; + if (accessor == null) { + accessor = this.accessor = staticGetter(); + } + try { + return accessor.invoke(); + } catch (Throwable e) { + throw new ForyException("Failed to read Scala singleton field: " + type, e); + } + } + + private MethodHandle staticGetter() { + try { + return _JDKAccess._trustedLookup(field.getDeclaringClass()) + .findStaticGetter(field.getDeclaringClass(), field.getName(), field.getType()); + } catch (NoSuchFieldException | IllegalAccessException | RuntimeException e) { + throw new ForyException("Failed to access Scala singleton field: " + type, e); } - return UnsafeOps.getObject(base, offset); } } diff --git a/java/fory-core/src/main/java/org/apache/fory/type/BFloat16Array.java b/java/fory-core/src/main/java/org/apache/fory/type/BFloat16Array.java index c187e6720a..21eae6959a 100644 --- a/java/fory-core/src/main/java/org/apache/fory/type/BFloat16Array.java +++ b/java/fory-core/src/main/java/org/apache/fory/type/BFloat16Array.java @@ -45,6 +45,10 @@ public BFloat16Array(BFloat16[] values) { } } + private BFloat16Array(short[] bits) { + this.bits = bits; + } + private BFloat16Array(short[] bits, boolean copy) { this.bits = copy ? Arrays.copyOf(bits, bits.length) : bits; } diff --git a/java/fory-core/src/main/java/org/apache/fory/type/Float16Array.java b/java/fory-core/src/main/java/org/apache/fory/type/Float16Array.java index f6f8f3d3a5..e724a2dbae 100644 --- a/java/fory-core/src/main/java/org/apache/fory/type/Float16Array.java +++ b/java/fory-core/src/main/java/org/apache/fory/type/Float16Array.java @@ -45,6 +45,10 @@ public Float16Array(Float16[] values) { } } + private Float16Array(short[] bits) { + this.bits = bits; + } + private Float16Array(short[] bits, boolean copy) { this.bits = copy ? Arrays.copyOf(bits, bits.length) : bits; } diff --git a/java/fory-core/src/main/java/org/apache/fory/type/TypeUtils.java b/java/fory-core/src/main/java/org/apache/fory/type/TypeUtils.java index 8d0ded4193..ab6adc373b 100644 --- a/java/fory-core/src/main/java/org/apache/fory/type/TypeUtils.java +++ b/java/fory-core/src/main/java/org/apache/fory/type/TypeUtils.java @@ -872,7 +872,8 @@ public static boolean isBean(TypeRef typeRef, TypeResolutionContext ctx) { || ctx.getCustomTypeRegistry().isExtraSupportedType(typeRef)) { return false; } - // if ReflectionUtils.hasNoArgConstructor(cls) return false, we use Unsafe to create object. + // Bean shape detection is independent of the later object-creation strategy. Records use the + // record path; ordinary beans are created later by TypeResolver's object-instantiator owner. // bean class can be static nested class, but can't be not a non-static inner class if (cls.getEnclosingClass() != null && !Modifier.isStatic(cls.getModifiers())) { return false; diff --git a/java/fory-core/src/main/java/org/apache/fory/util/ClassLoaderUtils.java b/java/fory-core/src/main/java/org/apache/fory/util/ClassLoaderUtils.java index ac5a382297..4f5af9859b 100644 --- a/java/fory-core/src/main/java/org/apache/fory/util/ClassLoaderUtils.java +++ b/java/fory-core/src/main/java/org/apache/fory/util/ClassLoaderUtils.java @@ -37,7 +37,7 @@ import org.apache.fory.logging.Logger; import org.apache.fory.logging.LoggerFactory; import org.apache.fory.platform.AndroidSupport; -import org.apache.fory.util.unsafe.DefineClass; +import org.apache.fory.platform.internal.DefineClass; /** ClassLoader utility for defining class and loading class by strategies. */ public class ClassLoaderUtils { diff --git a/java/fory-core/src/main/java/org/apache/fory/util/DefaultValueUtils.java b/java/fory-core/src/main/java/org/apache/fory/util/DefaultValueUtils.java index 554a8d7f89..e5d43cc9aa 100644 --- a/java/fory-core/src/main/java/org/apache/fory/util/DefaultValueUtils.java +++ b/java/fory-core/src/main/java/org/apache/fory/util/DefaultValueUtils.java @@ -34,13 +34,12 @@ import org.apache.fory.logging.Logger; import org.apache.fory.logging.LoggerFactory; import org.apache.fory.platform.AndroidSupport; -import org.apache.fory.platform.UnsafeOps; +import org.apache.fory.platform.internal._JDKAccess; import org.apache.fory.reflect.FieldAccessor; import org.apache.fory.resolver.TypeResolver; import org.apache.fory.type.ScalaTypes; import org.apache.fory.type.TypeUtils; import org.apache.fory.type.Types; -import org.apache.fory.util.unsafe._JDKAccess; /** * Utility class for detecting Scala classes with default values and their default value methods. @@ -86,6 +85,10 @@ public FieldAccessor getFieldAccessor() { return fieldAccessor; } + public Class getDeclaringClass() { + return fieldAccessor == null ? null : fieldAccessor.getField().getDeclaringClass(); + } + public int getDispatchId() { return dispatchId; } @@ -415,45 +418,80 @@ private static Object convertToType(Object value, int dispatchId) { */ public static void setDefaultValues(Object obj, DefaultValueField[] defaultValueFields) { for (DefaultValueField defaultField : defaultValueFields) { - FieldAccessor fieldAccessor = defaultField.getFieldAccessor(); - if (fieldAccessor != null) { - Object defaultValue = defaultField.getDefaultValue(); - if (AndroidSupport.IS_ANDROID) { - fieldAccessor.set(obj, defaultValue); - continue; - } - long fieldOffset = fieldAccessor.getFieldOffset(); - switch (defaultField.dispatchId) { - case Types.BOOL: - UnsafeOps.putBoolean(obj, fieldOffset, (Boolean) defaultValue); - break; - case Types.INT8: - UnsafeOps.putByte(obj, fieldOffset, (Byte) defaultValue); - break; - case Types.INT16: - UnsafeOps.putShort(obj, fieldOffset, (Short) defaultValue); - break; - case Types.INT32: - case Types.VARINT32: - UnsafeOps.putInt(obj, fieldOffset, (Integer) defaultValue); - break; - case Types.INT64: - case Types.VARINT64: - case Types.TAGGED_INT64: - UnsafeOps.putLong(obj, fieldOffset, (Long) defaultValue); - break; - case Types.FLOAT32: - UnsafeOps.putFloat(obj, fieldOffset, (Float) defaultValue); - break; - case Types.FLOAT64: - UnsafeOps.putDouble(obj, fieldOffset, (Double) defaultValue); - break; - default: - // Object type (including String, char, boxed types not covered above) - fieldAccessor.putObject(obj, defaultValue); - } + setDefaultValue(obj, defaultField); + } + } + + public static void setDefaultValues( + Object obj, DefaultValueField[] defaultValueFields, String[] skippedFieldNames) { + setDefaultValues(obj, defaultValueFields, skippedFieldNames, null); + } + + public static void setDefaultValues( + Object obj, + DefaultValueField[] defaultValueFields, + String[] skippedFieldNames, + Class[] skippedDeclaringClasses) { + for (DefaultValueField defaultField : defaultValueFields) { + if (!contains(skippedFieldNames, skippedDeclaringClasses, defaultField)) { + setDefaultValue(obj, defaultField); + } + } + } + + private static boolean contains( + String[] values, Class[] declaringClasses, DefaultValueField defaultField) { + if (values == null) { + return false; + } + Class declaringClass = defaultField.getDeclaringClass(); + for (int i = 0; i < values.length; i++) { + if (values[i].equals(defaultField.fieldName) + && (declaringClasses == null + || i >= declaringClasses.length + || declaringClasses[i] == null + || declaringClasses[i] == declaringClass)) { + return true; } } + return false; + } + + private static void setDefaultValue(Object obj, DefaultValueField defaultField) { + FieldAccessor fieldAccessor = defaultField.getFieldAccessor(); + if (fieldAccessor == null) { + return; + } + Object defaultValue = defaultField.getDefaultValue(); + switch (defaultField.dispatchId) { + case Types.BOOL: + fieldAccessor.putBoolean(obj, (Boolean) defaultValue); + break; + case Types.INT8: + fieldAccessor.putByte(obj, (Byte) defaultValue); + break; + case Types.INT16: + fieldAccessor.putShort(obj, (Short) defaultValue); + break; + case Types.INT32: + case Types.VARINT32: + fieldAccessor.putInt(obj, (Integer) defaultValue); + break; + case Types.INT64: + case Types.VARINT64: + case Types.TAGGED_INT64: + fieldAccessor.putLong(obj, (Long) defaultValue); + break; + case Types.FLOAT32: + fieldAccessor.putFloat(obj, (Float) defaultValue); + break; + case Types.FLOAT64: + fieldAccessor.putDouble(obj, (Double) defaultValue); + break; + default: + // Object type (including String, char, boxed types not covered above) + fieldAccessor.putObject(obj, defaultValue); + } } public static Object getScalaDefaultValue(Class cls, String fieldName) { diff --git a/java/fory-core/src/main/java/org/apache/fory/util/StringUtils.java b/java/fory-core/src/main/java/org/apache/fory/util/StringUtils.java index 43cb809407..00331df645 100644 --- a/java/fory-core/src/main/java/org/apache/fory/util/StringUtils.java +++ b/java/fory-core/src/main/java/org/apache/fory/util/StringUtils.java @@ -26,7 +26,6 @@ import java.util.Map; import java.util.Random; import org.apache.fory.memory.NativeByteOrder; -import org.apache.fory.platform.UnsafeOps; public class StringUtils { // A long mask used to clear all-higher bits of char in a super-word way. @@ -278,40 +277,6 @@ public static String lowerCamelToLowerUnderscore(String lowerCamel) { return builder.toString(); } - public static boolean isLatin(char[] chars) { - return isLatin(chars, 0); - } - - public static boolean isLatin(char[] chars, int start) { - if (start > chars.length) { - return false; - } - int byteOffset = start << 1; - int numChars = chars.length; - int vectorizedLen = numChars >> 2; - int vectorizedChars = vectorizedLen << 2; - int endOffset = UnsafeOps.CHAR_ARRAY_OFFSET + (vectorizedChars << 1); - boolean isLatin = true; - for (int offset = UnsafeOps.CHAR_ARRAY_OFFSET + byteOffset; offset < endOffset; offset += 8) { - // check 4 chars in a vectorized way, 4 times faster than scalar check loop. - // See benchmark in CompressStringSuite.latinSuperWordCheck. - long multiChars = UnsafeOps.getLong(chars, offset); - if ((multiChars & MULTI_CHARS_NON_LATIN_MASK) != 0) { - isLatin = false; - break; - } - } - if (isLatin) { - for (int i = vectorizedChars; i < numChars; i++) { - if (chars[i] > 0xFF) { - isLatin = false; - break; - } - } - } - return isLatin; - } - /** * Split a string from the right side, similar to Python's rsplit. * diff --git a/java/fory-core/src/main/java/org/apache/fory/util/function/Functions.java b/java/fory-core/src/main/java/org/apache/fory/util/function/Functions.java index 23c8d14270..b705b606ff 100644 --- a/java/fory-core/src/main/java/org/apache/fory/util/function/Functions.java +++ b/java/fory-core/src/main/java/org/apache/fory/util/function/Functions.java @@ -35,11 +35,11 @@ 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.reflect.ReflectionUtils; import org.apache.fory.util.Preconditions; import org.apache.fory.util.record.RecordComponent; import org.apache.fory.util.record.RecordUtils; -import org.apache.fory.util.unsafe._JDKAccess; /** Utility for lambda functions. */ public class Functions { diff --git a/java/fory-core/src/main/java/org/apache/fory/util/record/RecordUtils.java b/java/fory-core/src/main/java/org/apache/fory/util/record/RecordUtils.java index e7dd11fd3a..215a92152e 100644 --- a/java/fory-core/src/main/java/org/apache/fory/util/record/RecordUtils.java +++ b/java/fory-core/src/main/java/org/apache/fory/util/record/RecordUtils.java @@ -35,7 +35,7 @@ import org.apache.fory.collection.Tuple2; import org.apache.fory.exception.ForyException; import org.apache.fory.platform.AndroidSupport; -import org.apache.fory.util.unsafe._JDKAccess; +import org.apache.fory.platform.internal._JDKAccess; /** Utils for java.lang.Record. */ @SuppressWarnings({"rawtypes"}) diff --git a/java/fory-core/src/main/java/org/apache/fory/util/unsafe/DefineClass.java b/java/fory-core/src/main/java/org/apache/fory/util/unsafe/DefineClass.java deleted file mode 100644 index eca71ea811..0000000000 --- a/java/fory-core/src/main/java/org/apache/fory/util/unsafe/DefineClass.java +++ /dev/null @@ -1,81 +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.util.unsafe; - -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.MethodType; -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; - - 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); - } - } -} diff --git a/java/fory-core/src/main/java16/module-info.java b/java/fory-core/src/main/java16/module-info.java index 40c773b6ac..ce48341727 100644 --- a/java/fory-core/src/main/java16/module-info.java +++ b/java/fory-core/src/main/java16/module-info.java @@ -54,6 +54,4 @@ exports org.apache.fory.util; exports org.apache.fory.util.function; exports org.apache.fory.util.record; - exports org.apache.fory.util.unsafe to - org.apache.fory.format; } diff --git a/java/fory-core/src/main/java25/module-info.java b/java/fory-core/src/main/java25/module-info.java new file mode 100644 index 0000000000..3abd96aac0 --- /dev/null +++ b/java/fory-core/src/main/java25/module-info.java @@ -0,0 +1,56 @@ +/* + * 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. + */ + +module org.apache.fory.core { + requires java.logging; + + requires static java.sql; + requires static com.google.common; + requires static org.slf4j; + requires static jsr305; + requires static jdk.incubator.vector; + + exports org.apache.fory; + exports org.apache.fory.annotation; + exports org.apache.fory.builder; + exports org.apache.fory.codegen; + exports org.apache.fory.collection; + exports org.apache.fory.config; + exports org.apache.fory.context; + exports org.apache.fory.exception; + exports org.apache.fory.io; + exports org.apache.fory.logging; + exports org.apache.fory.memory; + exports org.apache.fory.meta; + exports org.apache.fory.platform; + exports org.apache.fory.pool; + exports org.apache.fory.reflect; + exports org.apache.fory.resolver; + exports org.apache.fory.serializer; + exports org.apache.fory.serializer.collection; + exports org.apache.fory.serializer.converter; + exports org.apache.fory.serializer.scala; + exports org.apache.fory.serializer.struct; + exports org.apache.fory.type; + exports org.apache.fory.type.union; + exports org.apache.fory.type.unsigned; + exports org.apache.fory.util; + exports org.apache.fory.util.function; + exports org.apache.fory.util.record; +} diff --git a/java/fory-core/src/main/java25/org/apache/fory/builder/UnsafeCodegenSupport.java b/java/fory-core/src/main/java25/org/apache/fory/builder/UnsafeCodegenSupport.java new file mode 100644 index 0000000000..ae3d46c34e --- /dev/null +++ b/java/fory-core/src/main/java25/org/apache/fory/builder/UnsafeCodegenSupport.java @@ -0,0 +1,49 @@ +/* + * 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 org.apache.fory.annotation.Internal; + +/** JDK25 replacement that keeps builder classes free of Unsafe runtime linkage. */ +@Internal +public final class UnsafeCodegenSupport { + private UnsafeCodegenSupport() {} + + public static Object unsafe() { + throw unsupported(); + } + + public static long objectFieldOffset(Field field) { + throw unsupported(); + } + + static String unsafeTypeName() { + throw unsupported(); + } + + public static String unsafeInitCode() { + throw unsupported(); + } + + private static UnsupportedOperationException unsupported() { + return new UnsupportedOperationException("Generated Unsafe access is unsupported on JDK25+"); + } +} diff --git a/java/fory-core/src/main/java25/org/apache/fory/memory/LittleEndian.java b/java/fory-core/src/main/java25/org/apache/fory/memory/LittleEndian.java new file mode 100644 index 0000000000..7d6e524826 --- /dev/null +++ b/java/fory-core/src/main/java25/org/apache/fory/memory/LittleEndian.java @@ -0,0 +1,72 @@ +/* + * 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.memory; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.nio.ByteOrder; + +public class LittleEndian { + private static final VarHandle BYTE_ARRAY_LONG = + MethodHandles.byteArrayViewVarHandle(long[].class, ByteOrder.LITTLE_ENDIAN); + + public static int putVarUint36Small(byte[] arr, int index, long v) { + if (v >>> 7 == 0) { + arr[index] = (byte) v; + return 1; + } + if (v >>> 14 == 0) { + arr[index++] = (byte) ((v & 0x7F) | 0x80); + arr[index] = (byte) (v >>> 7); + return 2; + } + return bigWriteUint36(arr, index, v); + } + + private static int bigWriteUint36(byte[] arr, int index, long v) { + if (v >>> 21 == 0) { + arr[index++] = (byte) ((v & 0x7F) | 0x80); + arr[index++] = (byte) (v >>> 7 | 0x80); + arr[index] = (byte) (v >>> 14); + return 3; + } + if (v >>> 28 == 0) { + arr[index++] = (byte) ((v & 0x7F) | 0x80); + arr[index++] = (byte) (v >>> 7 | 0x80); + arr[index++] = (byte) (v >>> 14 | 0x80); + arr[index] = (byte) (v >>> 21); + return 4; + } + arr[index++] = (byte) ((v & 0x7F) | 0x80); + arr[index++] = (byte) (v >>> 7 | 0x80); + arr[index++] = (byte) (v >>> 14 | 0x80); + arr[index++] = (byte) (v >>> 21 | 0x80); + arr[index] = (byte) (v >>> 28); + return 5; + } + + public static long getInt64(byte[] o, int index) { + return (long) BYTE_ARRAY_LONG.get(o, index); + } + + public static void putInt64(byte[] o, int index, long value) { + BYTE_ARRAY_LONG.set(o, index, value); + } +} diff --git a/java/fory-core/src/main/java25/org/apache/fory/memory/MemoryBuffer.java b/java/fory-core/src/main/java25/org/apache/fory/memory/MemoryBuffer.java new file mode 100644 index 0000000000..4e9e410e7e --- /dev/null +++ b/java/fory-core/src/main/java25/org/apache/fory/memory/MemoryBuffer.java @@ -0,0 +1,3924 @@ +/* + * 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.memory; + +import static org.apache.fory.util.Preconditions.checkArgument; +import static org.apache.fory.util.Preconditions.checkNotNull; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; +import org.apache.fory.annotation.CodegenInvoke; +import org.apache.fory.io.AbstractStreamReader; +import org.apache.fory.io.ForyStreamReader; + +/** + * A class for operations on memory managed by Fory. The buffer may be backed by heap memory (byte + * array) or by off-heap memory. Note that the buffer can auto grow on write operations and change + * into a heap buffer when growing. + * + *

This is a byte buffer similar class with more features: + * + *

+ * + *

Note that this class is designed to final so that all the methods in this class can be inlined + * by the just-in-time compiler. + * + *

TODO(chaokunyang) Let grow/readerIndex/writerIndex handled in this class and Make immutable + * part as separate class, and use composition in this class. In this way, all fields can be final + * and access will be much faster. + * + *

Warning: The instance of this class should not be held at GraalVM build time; build-time heap + * buffers do not represent the runtime heap layout. + * + *

In the Java 25 multi-release implementation, absolute random-access get/put methods are + * internal fast paths. Callers must pass legal indices and ranges; read/write entry points perform + * logical {@link MemoryBuffer} range validation before reaching these methods. The implementation + * relies on {@link VarHandle}, array, and {@link ByteBuffer} access only for JVM memory safety and + * does not repeat root Unsafe-style logical bounds checks. + * + *

Note(chaokunyang): Buffer operations are very common, and jvm inline and branch elimination is + * not reliable even in c2 compiler, so we try to inline and avoid checks as we can manually. jvm + * jit may stop inline for some reasons: NodeCountInliningCutoff, + * DesiredMethodLimit,MaxRecursiveInlineLevel,FreqInlineSize,MaxInlineSize + */ +public final class MemoryBuffer { + public static final int BUFFER_GROW_STEP_THRESHOLD = 100 * 1024 * 1024; + private static final boolean LITTLE_ENDIAN = NativeByteOrder.IS_LITTLE_ENDIAN; + private static final boolean UNALIGNED = true; + private static final ByteOrder NATIVE_ORDER = ByteOrder.nativeOrder(); + private static final int BOOLEAN_ARRAY_OFFSET = 0; + private static final int BYTE_ARRAY_OFFSET = 0; + private static final int CHAR_ARRAY_OFFSET = 0; + private static final int SHORT_ARRAY_OFFSET = 0; + private static final int INT_ARRAY_OFFSET = 0; + private static final int LONG_ARRAY_OFFSET = 0; + private static final int FLOAT_ARRAY_OFFSET = 0; + private static final int DOUBLE_ARRAY_OFFSET = 0; + private static final VarHandle BYTE_ARRAY_CHAR = + MethodHandles.byteArrayViewVarHandle(char[].class, NATIVE_ORDER); + private static final VarHandle BYTE_ARRAY_SHORT = + MethodHandles.byteArrayViewVarHandle(short[].class, NATIVE_ORDER); + private static final VarHandle BYTE_ARRAY_INT = + MethodHandles.byteArrayViewVarHandle(int[].class, NATIVE_ORDER); + private static final VarHandle BYTE_ARRAY_LONG = + MethodHandles.byteArrayViewVarHandle(long[].class, NATIVE_ORDER); + private static final VarHandle BYTE_BUFFER_CHAR = + MethodHandles.byteBufferViewVarHandle(char[].class, NATIVE_ORDER); + private static final VarHandle BYTE_BUFFER_SHORT = + MethodHandles.byteBufferViewVarHandle(short[].class, NATIVE_ORDER); + private static final VarHandle BYTE_BUFFER_INT = + MethodHandles.byteBufferViewVarHandle(int[].class, NATIVE_ORDER); + private static final VarHandle BYTE_BUFFER_LONG = + MethodHandles.byteBufferViewVarHandle(long[].class, NATIVE_ORDER); + // Global allocator instance that can be customized + private static volatile MemoryAllocator globalAllocator = new DefaultMemoryAllocator(); + + // 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 + // to undefined addresses outside the heap and may in out-of-order execution cases cause + // buffer faults. + byte[] heapMemory; + int heapOffset; + // If the data is off the heap, `offHeapBuffer` will be non-null, and it's the direct byte buffer + // that allocated on the off-heap memory. + // This memory buffer holds a reference to that buffer, so as long as this memory buffer lives, + // the memory will not be released. + ByteBuffer offHeapBuffer; + ByteBuffer nativeOffHeapBuffer; + // The readable/writeable range is [address, addressLimit). + // If the data in on the heap, this is the relative offset to the `heapMemory` byte array. + // If the data is off the heap, this is the logical byte index into `offHeapBuffer`. + long address; + // The address one byte after the last addressable byte, i.e. `address + size` while the + // buffer is not disposed. + long addressLimit; + // The size in bytes of the memory buffer. + int size; + int readerIndex; + int writerIndex; + final ForyStreamReader streamReader; + + // Android branches in this class are intentional method-boundary exits. + // Do not delete them or fold them into the JVM path: each branch must make exactly one MemoryOps + // call, while MemoryOps owns Android heap index math and reader/writer updates. + + /** + * Creates a new memory buffer that represents the memory of the byte array. + * + * @param buffer The byte array whose memory is represented by this memory buffer. + * @param offset The offset of the sub array to be used; must be non-negative and no larger than + * array.length. + * @param length buffer size + */ + private MemoryBuffer(byte[] buffer, int offset, int length) { + this(buffer, offset, length, null); + } + + /** + * Creates a new memory buffer that represents the memory of the byte array. + * + * @param buffer The byte array whose memory is represented by this memory buffer. + * @param offset The offset of the sub array to be used; must be non-negative and no larger than + * array.length. + * @param length buffer size + * @param streamReader a reader for reading from a stream. + */ + private MemoryBuffer(byte[] buffer, int offset, int length, ForyStreamReader streamReader) { + checkArgument(offset >= 0 && length >= 0); + if (offset + length > buffer.length) { + throw new IllegalArgumentException( + String.format("%d exceeds buffer size %d", offset + length, buffer.length)); + } + initHeapBuffer(buffer, offset, length); + if (streamReader != null) { + this.streamReader = streamReader; + } else { + this.streamReader = new BoundChecker(); + } + } + + /** + * Creates a new memory buffer that represents the native memory at the absolute address given by + * the pointer. + * + * @param offHeapAddress The address of the memory represented by this memory buffer. + * @param size The size of this memory buffer. + * @param offHeapBuffer The byte buffer whose memory is represented by this memory buffer which + * may be null if the memory is not allocated by `DirectByteBuffer`. Hold this buffer to avoid + * the memory being released. + */ + private MemoryBuffer(long offHeapAddress, int size, ByteBuffer offHeapBuffer) { + this(offHeapAddress, size, offHeapBuffer, null); + } + + /** + * Creates a new memory buffer that represents the native memory at the absolute address given by + * the pointer. + * + * @param offHeapAddress The address of the memory represented by this memory buffer. + * @param size The size of this memory buffer. + * @param offHeapBuffer The byte buffer whose memory is represented by this memory buffer which + * may be null if the memory is not allocated by `DirectByteBuffer`. Hold this buffer to avoid + * the memory being released. + * @param streamReader a reader for reading from a stream. + */ + private MemoryBuffer( + long offHeapAddress, int size, ByteBuffer offHeapBuffer, ForyStreamReader streamReader) { + initOffHeapBuffer(offHeapAddress, size, offHeapBuffer); + if (streamReader != null) { + this.streamReader = streamReader; + } else { + this.streamReader = new BoundChecker(); + } + } + + private void initOffHeapBuffer(long offHeapAddress, int size, ByteBuffer offHeapBuffer) { + checkArgument(offHeapAddress >= 0 && size >= 0); + checkNotNull(offHeapBuffer, "JDK25 MemoryBuffer requires a ByteBuffer owner for off-heap data"); + checkArgument( + offHeapBuffer.isDirect(), "Only direct ByteBuffers can back off-heap MemoryBuffer"); + this.offHeapBuffer = offHeapBuffer; + ByteBuffer nativeBuffer = offHeapBuffer.duplicate().order(NATIVE_ORDER); + // Stream readers can expand the owner buffer limit after this duplicate is created. Keep the + // absolute-access view capacity-wide so JDK25 public ByteBuffer checks match the logical buffer + // size tracked by MemoryBuffer. + nativeBuffer.clear(); + this.nativeOffHeapBuffer = nativeBuffer; + this.heapMemory = null; + this.address = offHeapAddress; + this.addressLimit = this.address + size; + 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."); + throw new UnsupportedOperationException("Raw direct-buffer addresses are not exposed on JDK25"); + } + + public void initByteBuffer(ByteBuffer buffer, int size) { + if (buffer.isDirect()) { + initOffHeapBuffer(0, size, buffer); + } else if (buffer.hasArray()) { + initHeapBuffer(buffer.array(), buffer.arrayOffset(), size); + } else { + throw new IllegalArgumentException("ByteBuffer must be direct or expose an array"); + } + } + + private class BoundChecker extends AbstractStreamReader { + @Override + public int fillBuffer(int minFillSize) { + throw new IndexOutOfBoundsException( + String.format( + "readerIndex(%d) + length(%d) exceeds size(%d): %s", + readerIndex, minFillSize, size, this)); + } + + @Override + public void readTo(byte[] dst, int dstIndex, int length) { + throwIndexOOBExceptionForRead(length); + } + + @Override + public void readToByteBuffer(ByteBuffer dst, int length) { + throwIndexOOBExceptionForRead(length); + } + + @Override + public int readToByteBuffer(ByteBuffer dst) { + throwIndexOOBExceptionForRead(dst.remaining()); + return 0; + } + + @Override + public MemoryBuffer getBuffer() { + return MemoryBuffer.this; + } + } + + public void initHeapBuffer(byte[] buffer, int offset, int length) { + if (buffer == null) { + throw new NullPointerException("buffer"); + } + this.heapMemory = buffer; + this.heapOffset = offset; + this.offHeapBuffer = null; + this.nativeOffHeapBuffer = null; + final long startPos = BYTE_ARRAY_OFFSET + offset; + this.address = startPos; + this.size = length; + this.addressLimit = startPos + length; + } + + private byte loadByte(long pos) { + byte[] heap = heapMemory; + if (heap != null) { + return heap[(int) pos]; + } + return nativeOffHeapBuffer.get((int) pos); + } + + private void storeByte(long pos, byte value) { + byte[] heap = heapMemory; + if (heap != null) { + heap[(int) pos] = value; + } else { + nativeOffHeapBuffer.put((int) pos, value); + } + } + + private char loadChar(long pos) { + byte[] heap = heapMemory; + if (heap != null) { + return (char) BYTE_ARRAY_CHAR.get(heap, (int) pos); + } + return (char) BYTE_BUFFER_CHAR.get(nativeOffHeapBuffer, (int) pos); + } + + private void storeChar(long pos, char value) { + byte[] heap = heapMemory; + if (heap != null) { + BYTE_ARRAY_CHAR.set(heap, (int) pos, value); + } else { + BYTE_BUFFER_CHAR.set(nativeOffHeapBuffer, (int) pos, value); + } + } + + private short loadShort(long pos) { + byte[] heap = heapMemory; + if (heap != null) { + return (short) BYTE_ARRAY_SHORT.get(heap, (int) pos); + } + return (short) BYTE_BUFFER_SHORT.get(nativeOffHeapBuffer, (int) pos); + } + + private void storeShort(long pos, short value) { + byte[] heap = heapMemory; + if (heap != null) { + BYTE_ARRAY_SHORT.set(heap, (int) pos, value); + } else { + BYTE_BUFFER_SHORT.set(nativeOffHeapBuffer, (int) pos, value); + } + } + + private int loadInt(long pos) { + byte[] heap = heapMemory; + if (heap != null) { + return (int) BYTE_ARRAY_INT.get(heap, (int) pos); + } + return (int) BYTE_BUFFER_INT.get(nativeOffHeapBuffer, (int) pos); + } + + private void storeInt(long pos, int value) { + byte[] heap = heapMemory; + if (heap != null) { + BYTE_ARRAY_INT.set(heap, (int) pos, value); + } else { + BYTE_BUFFER_INT.set(nativeOffHeapBuffer, (int) pos, value); + } + } + + private long loadLong(long pos) { + byte[] heap = heapMemory; + if (heap != null) { + return (long) BYTE_ARRAY_LONG.get(heap, (int) pos); + } + return (long) BYTE_BUFFER_LONG.get(nativeOffHeapBuffer, (int) pos); + } + + private void storeLong(long pos, long value) { + byte[] heap = heapMemory; + if (heap != null) { + BYTE_ARRAY_LONG.set(heap, (int) pos, value); + } else { + BYTE_BUFFER_LONG.set(nativeOffHeapBuffer, (int) pos, value); + } + } + + private void readBytesToArray(long srcOffset, byte[] target, int targetOffset, int numBytes) { + byte[] heap = heapMemory; + if (heap != null) { + System.arraycopy(heap, toIntIndex(srcOffset), target, targetOffset, numBytes); + } else { + nativeOffHeapBuffer.get(toIntIndex(srcOffset), target, targetOffset, numBytes); + } + } + + private void writeBytesFromArray( + long targetOffset, byte[] source, int sourceOffset, int numBytes) { + byte[] heap = heapMemory; + if (heap != null) { + System.arraycopy(source, sourceOffset, heap, toIntIndex(targetOffset), numBytes); + } else { + nativeOffHeapBuffer.put(toIntIndex(targetOffset), source, sourceOffset, numBytes); + } + } + + private void readBooleansToArray( + long srcOffset, boolean[] target, int targetOffset, int numBytes) { + for (int i = 0; i < numBytes; i++) { + target[targetOffset + i] = loadByte(srcOffset + i) != 0; + } + } + + private void writeBooleansFromArray( + long targetOffset, boolean[] source, int sourceOffset, int numBytes) { + for (int i = 0; i < numBytes; i++) { + storeByte(targetOffset + i, source[sourceOffset + i] ? (byte) 1 : (byte) 0); + } + } + + private void readCharsToArray(long srcOffset, char[] target, int targetOffset, int numBytes) { + int elements = numBytes >>> 1; + int pos = toIntIndex(srcOffset); + byte[] heap = heapMemory; + if (heap != null) { + for (int i = 0; i < elements; i++, pos += 2) { + target[targetOffset + i] = (char) BYTE_ARRAY_CHAR.get(heap, pos); + } + } else { + ByteBuffer direct = nativeOffHeapBuffer; + for (int i = 0; i < elements; i++, pos += 2) { + target[targetOffset + i] = (char) BYTE_BUFFER_CHAR.get(direct, pos); + } + } + } + + private void writeCharsFromArray( + long targetOffset, char[] source, int sourceOffset, int numBytes) { + int elements = numBytes >>> 1; + int pos = toIntIndex(targetOffset); + byte[] heap = heapMemory; + if (heap != null) { + for (int i = 0; i < elements; i++, pos += 2) { + BYTE_ARRAY_CHAR.set(heap, pos, source[sourceOffset + i]); + } + } else { + ByteBuffer direct = nativeOffHeapBuffer; + for (int i = 0; i < elements; i++, pos += 2) { + BYTE_BUFFER_CHAR.set(direct, pos, source[sourceOffset + i]); + } + } + } + + private void readShortsToArray(long srcOffset, short[] target, int targetOffset, int numBytes) { + int elements = numBytes >>> 1; + int pos = toIntIndex(srcOffset); + byte[] heap = heapMemory; + if (heap != null) { + for (int i = 0; i < elements; i++, pos += 2) { + target[targetOffset + i] = (short) BYTE_ARRAY_SHORT.get(heap, pos); + } + } else { + ByteBuffer direct = nativeOffHeapBuffer; + for (int i = 0; i < elements; i++, pos += 2) { + target[targetOffset + i] = (short) BYTE_BUFFER_SHORT.get(direct, pos); + } + } + } + + private void writeShortsFromArray( + long targetOffset, short[] source, int sourceOffset, int numBytes) { + int elements = numBytes >>> 1; + int pos = toIntIndex(targetOffset); + byte[] heap = heapMemory; + if (heap != null) { + for (int i = 0; i < elements; i++, pos += 2) { + BYTE_ARRAY_SHORT.set(heap, pos, source[sourceOffset + i]); + } + } else { + ByteBuffer direct = nativeOffHeapBuffer; + for (int i = 0; i < elements; i++, pos += 2) { + BYTE_BUFFER_SHORT.set(direct, pos, source[sourceOffset + i]); + } + } + } + + private void readIntsToArray(long srcOffset, int[] target, int targetOffset, int numBytes) { + int elements = numBytes >>> 2; + int pos = toIntIndex(srcOffset); + byte[] heap = heapMemory; + if (heap != null) { + for (int i = 0; i < elements; i++, pos += 4) { + target[targetOffset + i] = (int) BYTE_ARRAY_INT.get(heap, pos); + } + } else { + ByteBuffer direct = nativeOffHeapBuffer; + for (int i = 0; i < elements; i++, pos += 4) { + target[targetOffset + i] = (int) BYTE_BUFFER_INT.get(direct, pos); + } + } + } + + private void writeIntsFromArray(long targetOffset, int[] source, int sourceOffset, int numBytes) { + int elements = numBytes >>> 2; + int pos = toIntIndex(targetOffset); + byte[] heap = heapMemory; + if (heap != null) { + for (int i = 0; i < elements; i++, pos += 4) { + BYTE_ARRAY_INT.set(heap, pos, source[sourceOffset + i]); + } + } else { + ByteBuffer direct = nativeOffHeapBuffer; + for (int i = 0; i < elements; i++, pos += 4) { + BYTE_BUFFER_INT.set(direct, pos, source[sourceOffset + i]); + } + } + } + + private void readLongsToArray(long srcOffset, long[] target, int targetOffset, int numBytes) { + int elements = numBytes >>> 3; + int pos = toIntIndex(srcOffset); + byte[] heap = heapMemory; + if (heap != null) { + for (int i = 0; i < elements; i++, pos += 8) { + target[targetOffset + i] = (long) BYTE_ARRAY_LONG.get(heap, pos); + } + } else { + ByteBuffer direct = nativeOffHeapBuffer; + for (int i = 0; i < elements; i++, pos += 8) { + target[targetOffset + i] = (long) BYTE_BUFFER_LONG.get(direct, pos); + } + } + } + + private void writeLongsFromArray( + long targetOffset, long[] source, int sourceOffset, int numBytes) { + int elements = numBytes >>> 3; + int pos = toIntIndex(targetOffset); + byte[] heap = heapMemory; + if (heap != null) { + for (int i = 0; i < elements; i++, pos += 8) { + BYTE_ARRAY_LONG.set(heap, pos, source[sourceOffset + i]); + } + } else { + ByteBuffer direct = nativeOffHeapBuffer; + for (int i = 0; i < elements; i++, pos += 8) { + BYTE_BUFFER_LONG.set(direct, pos, source[sourceOffset + i]); + } + } + } + + private void readFloatsToArray(long srcOffset, float[] target, int targetOffset, int numBytes) { + int elements = numBytes >>> 2; + int pos = toIntIndex(srcOffset); + byte[] heap = heapMemory; + if (heap != null) { + for (int i = 0; i < elements; i++, pos += 4) { + target[targetOffset + i] = Float.intBitsToFloat((int) BYTE_ARRAY_INT.get(heap, pos)); + } + } else { + ByteBuffer direct = nativeOffHeapBuffer; + for (int i = 0; i < elements; i++, pos += 4) { + target[targetOffset + i] = Float.intBitsToFloat((int) BYTE_BUFFER_INT.get(direct, pos)); + } + } + } + + private void writeFloatsFromArray( + long targetOffset, float[] source, int sourceOffset, int numBytes) { + int elements = numBytes >>> 2; + int pos = toIntIndex(targetOffset); + byte[] heap = heapMemory; + if (heap != null) { + for (int i = 0; i < elements; i++, pos += 4) { + BYTE_ARRAY_INT.set(heap, pos, Float.floatToRawIntBits(source[sourceOffset + i])); + } + } else { + ByteBuffer direct = nativeOffHeapBuffer; + for (int i = 0; i < elements; i++, pos += 4) { + BYTE_BUFFER_INT.set(direct, pos, Float.floatToRawIntBits(source[sourceOffset + i])); + } + } + } + + private void readDoublesToArray(long srcOffset, double[] target, int targetOffset, int numBytes) { + int elements = numBytes >>> 3; + int pos = toIntIndex(srcOffset); + byte[] heap = heapMemory; + if (heap != null) { + for (int i = 0; i < elements; i++, pos += 8) { + target[targetOffset + i] = Double.longBitsToDouble((long) BYTE_ARRAY_LONG.get(heap, pos)); + } + } else { + ByteBuffer direct = nativeOffHeapBuffer; + for (int i = 0; i < elements; i++, pos += 8) { + target[targetOffset + i] = + Double.longBitsToDouble((long) BYTE_BUFFER_LONG.get(direct, pos)); + } + } + } + + private void writeDoublesFromArray( + long targetOffset, double[] source, int sourceOffset, int numBytes) { + int elements = numBytes >>> 3; + int pos = toIntIndex(targetOffset); + byte[] heap = heapMemory; + if (heap != null) { + for (int i = 0; i < elements; i++, pos += 8) { + BYTE_ARRAY_LONG.set(heap, pos, Double.doubleToRawLongBits(source[sourceOffset + i])); + } + } else { + ByteBuffer direct = nativeOffHeapBuffer; + for (int i = 0; i < elements; i++, pos += 8) { + BYTE_BUFFER_LONG.set(direct, pos, Double.doubleToRawLongBits(source[sourceOffset + i])); + } + } + } + + private static int toIntIndex(long offset) { + if (offset < 0 || offset > Integer.MAX_VALUE) { + throw new IndexOutOfBoundsException("offset out of int range: " + offset); + } + return (int) offset; + } + + private static int putVarUInt32Heap(byte[] heap, int index, int value) { + if (value >>> 7 == 0) { + heap[index] = (byte) value; + return 1; + } + heap[index++] = (byte) ((value & 0x7F) | 0x80); + if (value >>> 14 == 0) { + heap[index] = (byte) (value >>> 7); + return 2; + } + heap[index++] = (byte) ((value >>> 7) | 0x80); + if (value >>> 21 == 0) { + heap[index] = (byte) (value >>> 14); + return 3; + } + heap[index++] = (byte) ((value >>> 14) | 0x80); + if (value >>> 28 == 0) { + heap[index] = (byte) (value >>> 21); + return 4; + } + heap[index++] = (byte) ((value >>> 21) | 0x80); + heap[index] = (byte) (value >>> 28); + return 5; + } + + // ------------------------------------------------------------------------ + // Memory buffer Operations + // ------------------------------------------------------------------------ + + /** + * Gets the size of the memory buffer, in bytes. + * + * @return The size of the memory buffer. + */ + public int size() { + return size; + } + + public void increaseSize(int diff) { + this.addressLimit = address + (size += diff); + } + + /** + * Checks whether this memory buffer is backed by off-heap memory. + * + * @return true, if the memory buffer is backed by off-heap memory, false if it + * is backed by heap memory. + */ + public boolean isOffHeap() { + return heapMemory == null; + } + + /** + * Returns true, if the memory buffer is backed by heap memory and memory buffer can + * write to the whole memory region of underlying byte array. + */ + public boolean isHeapFullyWriteable() { + return heapMemory != null && heapOffset == 0; + } + + /** + * Get the heap byte array object. + * + * @return Return non-null if the memory is on the heap, and return null, if the memory if off the + * heap. + */ + public byte[] getHeapMemory() { + return heapMemory; + } + + /** + * Gets the buffer that owns the memory of this memory buffer. + * + * @return The byte buffer that owns the memory of this memory buffer. + */ + public ByteBuffer getOffHeapBuffer() { + if (offHeapBuffer != null) { + return offHeapBuffer; + } else { + throw new IllegalStateException("Memory buffer does not represent off heap ByteBuffer"); + } + } + + /** + * Returns the byte array of on-heap memory buffers. + * + * @return underlying byte array + * @throws IllegalStateException if the memory buffer does not represent on-heap memory + */ + public byte[] getArray() { + if (heapMemory != null) { + return heapMemory; + } else { + throw new IllegalStateException("Memory buffer does not represent heap memory"); + } + } + + // ------------------------------------------------------------------------ + // Random Access get() and put() methods + // ------------------------------------------------------------------------ + + /** Copies from a caller-validated absolute buffer index into {@code dst}. */ + public void get(int index, byte[] dst) { + get(index, dst, 0, dst.length); + } + + /** + * Copies from a caller-validated absolute buffer range into {@code dst}. + * + *

This Java 25 path intentionally does not duplicate {@link MemoryBuffer} range checks; callers + * that derive {@code index}, {@code offset}, or {@code length} from wire data must validate them + * before calling this method. + */ + public void get(int index, byte[] dst, int offset, int length) { + final long pos = address + index; + final byte[] heapMemory = this.heapMemory; + if (heapMemory != null) { + // Keep heap-to-heap bulk copies on the JDK intrinsic path. + System.arraycopy(heapMemory, heapOffset + index, dst, offset, length); + } else { + readBytesToArray(pos, dst, BYTE_ARRAY_OFFSET + offset, length); + } + } + + /** + * Copies from a caller-validated absolute buffer range into {@code target}. + * + *

Logical range validation belongs to the caller. This method relies on the backing Java array + * or {@link ByteBuffer} accessors only to prevent invalid JVM memory access. + */ + public void get(int offset, ByteBuffer target, int numBytes) { + final int targetPos = target.position(); + if (target.isDirect()) { + ByteBuffer duplicate = target.duplicate(); + ByteBufferUtil.position(duplicate, targetPos); + if (heapMemory != null) { + duplicate.put(heapMemory, heapOffset + offset, numBytes); + } else { + ByteBuffer source = nativeOffHeapBuffer.duplicate(); + int sourcePos = toIntIndex(address + offset); + ByteBufferUtil.position(source, sourcePos); + source.limit(sourcePos + numBytes); + duplicate.put(source.slice()); + } + } else { + assert target.hasArray(); + get(offset, target.array(), targetPos + target.arrayOffset(), numBytes); + } + if (target.position() == targetPos) { + ByteBufferUtil.position(target, targetPos + numBytes); + } + } + + /** + * Copies from {@code source} into a caller-validated absolute buffer range. + * + *

Logical range validation belongs to the caller. This method relies on the backing Java array + * or {@link ByteBuffer} accessors only to prevent invalid JVM memory access. + */ + public void put(int offset, ByteBuffer source, int numBytes) { + final int sourcePos = source.position(); + if (source.isDirect()) { + ByteBuffer duplicate = source.duplicate(); + ByteBufferUtil.position(duplicate, sourcePos); + duplicate.limit(sourcePos + numBytes); + if (heapMemory != null) { + duplicate.get(heapMemory, heapOffset + offset, numBytes); + } else { + ByteBuffer target = nativeOffHeapBuffer.duplicate(); + int targetPos = toIntIndex(address + offset); + ByteBufferUtil.position(target, targetPos); + target.limit(targetPos + numBytes); + target.slice().put(duplicate.slice()); + } + } else { + assert source.hasArray(); + put(offset, source.array(), sourcePos + source.arrayOffset(), numBytes); + } + if (source.position() == sourcePos) { + ByteBufferUtil.position(source, sourcePos + numBytes); + } + } + + /** Copies {@code src} into a caller-validated absolute buffer index. */ + public void put(int index, byte[] src) { + put(index, src, 0, src.length); + } + + /** + * Copies {@code src} into a caller-validated absolute buffer range. + * + *

This Java 25 path intentionally does not duplicate {@link MemoryBuffer} range checks; callers + * that derive {@code index}, {@code offset}, or {@code length} from wire data must validate them + * before calling this method. + */ + public void put(int index, byte[] src, int offset, int length) { + final long pos = address + index; + final byte[] heapMemory = this.heapMemory; + if (heapMemory != null) { + // Keep heap-to-heap bulk copies on the JDK intrinsic path. + System.arraycopy(src, offset, heapMemory, heapOffset + index, length); + } else { + writeBytesFromArray(pos, src, BYTE_ARRAY_OFFSET + offset, length); + } + } + + public byte getByte(int index) { + final long pos = address + index; + return loadByte(pos); + } + + // In the Java25 overlay, `_unsafe*` preserves the root MemoryBuffer unchecked-access naming. + // These methods use indexed array, ByteBuffer, and VarHandle access, not the JDK Unsafe API. + // CHECKSTYLE.OFF:MethodName + public byte _unsafeGetByte(int index) { + // CHECKSTYLE.ON:MethodName + return loadByte(address + index); + } + + public void putByte(int index, int b) { + final long pos = address + index; + storeByte(pos, (byte) b); + } + + public void putByte(int index, byte b) { + final long pos = address + index; + storeByte(pos, b); + } + + // CHECKSTYLE.OFF:MethodName + public void _unsafePutByte(int index, byte b) { + // CHECKSTYLE.ON:MethodName + storeByte(address + index, b); + } + + public boolean getBoolean(int index) { + final long pos = address + index; + return loadByte(pos) != 0; + } + + // CHECKSTYLE.OFF:MethodName + public boolean _unsafeGetBoolean(int index) { + // CHECKSTYLE.ON:MethodName + return loadByte(address + index) != 0; + } + + public void putBoolean(int index, boolean value) { + final long pos = address + index; + storeByte(pos, (value ? (byte) 1 : (byte) 0)); + } + + // CHECKSTYLE.OFF:MethodName + public void _unsafePutBoolean(int index, boolean value) { + // CHECKSTYLE.ON:MethodName + storeByte(address + index, (value ? (byte) 1 : (byte) 0)); + } + + public char getChar(int index) { + final long pos = address + index; + char c = loadChar(pos); + return LITTLE_ENDIAN ? c : Character.reverseBytes(c); + } + + // CHECKSTYLE.OFF:MethodName + public char _unsafeGetChar(int index) { + // CHECKSTYLE.ON:MethodName + char c = loadChar(address + index); + return LITTLE_ENDIAN ? c : Character.reverseBytes(c); + } + + public void putChar(int index, char value) { + final long pos = address + index; + if (!LITTLE_ENDIAN) { + value = Character.reverseBytes(value); + } + storeChar(pos, value); + } + + // CHECKSTYLE.OFF:MethodName + public void _unsafePutChar(int index, char value) { + // CHECKSTYLE.ON:MethodName + if (!LITTLE_ENDIAN) { + value = Character.reverseBytes(value); + } + storeChar(address + index, value); + } + + public short getInt16(int index) { + final long pos = address + index; + short v = loadShort(pos); + return LITTLE_ENDIAN ? v : Short.reverseBytes(v); + } + + public void putInt16(int index, short value) { + final long pos = address + index; + if (!LITTLE_ENDIAN) { + value = Short.reverseBytes(value); + } + storeShort(pos, value); + } + + // CHECKSTYLE.OFF:MethodName + public short _unsafeGetInt16(int index) { + // CHECKSTYLE.ON:MethodName + short v = loadShort(address + index); + return LITTLE_ENDIAN ? v : Short.reverseBytes(v); + } + + // CHECKSTYLE.OFF:MethodName + public void _unsafePutInt16(int index, short value) { + // CHECKSTYLE.ON:MethodName + if (!LITTLE_ENDIAN) { + value = Short.reverseBytes(value); + } + storeShort(address + index, value); + } + + public int getInt32(int index) { + final long pos = address + index; + int v = loadInt(pos); + return LITTLE_ENDIAN ? v : Integer.reverseBytes(v); + } + + public void putInt32(int index, int value) { + final long pos = address + index; + if (!LITTLE_ENDIAN) { + value = Integer.reverseBytes(value); + } + storeInt(pos, value); + } + + // CHECKSTYLE.OFF:MethodName + public int _unsafeGetInt32(int index) { + // CHECKSTYLE.ON:MethodName + int v = loadInt(address + index); + return LITTLE_ENDIAN ? v : Integer.reverseBytes(v); + } + + // CHECKSTYLE.OFF:MethodName + public void _unsafePutInt32(int index, int value) { + // CHECKSTYLE.ON:MethodName + if (!LITTLE_ENDIAN) { + value = Integer.reverseBytes(value); + } + storeInt(address + index, value); + } + + public long getInt64(int index) { + final long pos = address + index; + long v = loadLong(pos); + return LITTLE_ENDIAN ? v : Long.reverseBytes(v); + } + + public void putInt64(int index, long value) { + final long pos = address + index; + if (!LITTLE_ENDIAN) { + value = Long.reverseBytes(value); + } + storeLong(pos, value); + } + + // CHECKSTYLE.OFF:MethodName + public long _unsafeGetInt64(int index) { + // CHECKSTYLE.ON:MethodName + long v = loadLong(address + index); + return LITTLE_ENDIAN ? v : Long.reverseBytes(v); + } + + // CHECKSTYLE.OFF:MethodName + public void _unsafePutInt64(int index, long value) { + // CHECKSTYLE.ON:MethodName + if (!LITTLE_ENDIAN) { + value = Long.reverseBytes(value); + } + storeLong(address + index, value); + } + + public float getFloat32(int index) { + final long pos = address + index; + int v = loadInt(pos); + if (!LITTLE_ENDIAN) { + v = Integer.reverseBytes(v); + } + return Float.intBitsToFloat(v); + } + + public void putFloat32(int index, float value) { + final long pos = address + index; + int v = Float.floatToRawIntBits(value); + if (!LITTLE_ENDIAN) { + v = Integer.reverseBytes(v); + } + storeInt(pos, v); + } + + public double getFloat64(int index) { + final long pos = address + index; + long v = loadLong(pos); + if (!LITTLE_ENDIAN) { + v = Long.reverseBytes(v); + } + return Double.longBitsToDouble(v); + } + + public void putFloat64(int index, double value) { + final long pos = address + index; + long v = Double.doubleToRawLongBits(value); + if (!LITTLE_ENDIAN) { + v = Long.reverseBytes(v); + } + storeLong(pos, v); + } + + // Check should be done outside to avoid this method got into the critical path. + private void throwOOBException() { + throw new IndexOutOfBoundsException( + String.format("size: %d, address %s, addressLimit %d", size, address, addressLimit)); + } + + // ------------------------------------------------------------------------- + // Write Methods + // ------------------------------------------------------------------------- + + /** Returns the {@code writerIndex} of this buffer. */ + public int writerIndex() { + return writerIndex; + } + + /** + * Sets the {@code writerIndex} of this buffer. + * + * @throws IndexOutOfBoundsException if the specified {@code writerIndex} is less than {@code 0} + * or greater than {@code this.size} + */ + public void writerIndex(int writerIndex) { + if (writerIndex < 0 || writerIndex > size) { + throwOOBExceptionForWriteIndex(writerIndex); + } + this.writerIndex = writerIndex; + } + + private void throwOOBExceptionForWriteIndex(int writerIndex) { + throw new IndexOutOfBoundsException( + String.format( + "writerIndex: %d (expected: 0 <= writerIndex <= size(%d))", writerIndex, size)); + } + + // CHECKSTYLE.OFF:MethodName + public void _unsafeWriterIndex(int writerIndex) { + // CHECKSTYLE.ON:MethodName + this.writerIndex = writerIndex; + } + + /** Returns heap index for writer index if buffer is a heap buffer. */ + // CHECKSTYLE.OFF:MethodName + public int _unsafeHeapWriterIndex() { + // CHECKSTYLE.ON:MethodName + return writerIndex + heapOffset; + } + + // CHECKSTYLE.OFF:MethodName + public long _unsafeWriterAddress() { + // CHECKSTYLE.ON:MethodName + checkHeapAddressAccess(); + return address + writerIndex; + } + + // CHECKSTYLE.OFF:MethodName + public void _increaseWriterIndexUnsafe(int diff) { + // CHECKSTYLE.ON:MethodName + this.writerIndex = writerIndex + diff; + } + + /** Increase writer index and grow buffer if needed. */ + public void increaseWriterIndex(int diff) { + int writerIdx = writerIndex + diff; + ensure(writerIdx); + this.writerIndex = writerIdx; + } + + public void writeBoolean(boolean value) { + final int writerIdx = writerIndex; + final int newIdx = writerIdx + 1; + ensure(newIdx); + final long pos = address + writerIdx; + storeByte(pos, (byte) (value ? 1 : 0)); + writerIndex = newIdx; + } + + // CHECKSTYLE.OFF:MethodName + public void _unsafeWriteByte(byte value) { + // CHECKSTYLE.ON:MethodName + final int writerIdx = writerIndex; + final int newIdx = writerIdx + 1; + final long pos = address + writerIdx; + storeByte(pos, value); + writerIndex = newIdx; + } + + public void writeUInt8(int value) { + writeByte((byte) value); + } + + public void writeByte(byte value) { + final int writerIdx = writerIndex; + final int newIdx = writerIdx + 1; + ensure(newIdx); + final long pos = address + writerIdx; + storeByte(pos, value); + writerIndex = newIdx; + } + + public void writeByte(int value) { + writeByte((byte) value); + } + + public void writeChar(char value) { + final int writerIdx = writerIndex; + final int newIdx = writerIdx + 2; + ensure(newIdx); + final long pos = address + writerIdx; + if (!LITTLE_ENDIAN) { + value = Character.reverseBytes(value); + } + storeChar(pos, value); + writerIndex = newIdx; + } + + public void writeInt16(short value) { + final int writerIdx = writerIndex; + final int newIdx = writerIdx + 2; + ensure(newIdx); + if (!LITTLE_ENDIAN) { + value = Short.reverseBytes(value); + } + storeShort(address + writerIdx, value); + writerIndex = newIdx; + } + + public void writeInt32(int value) { + final int writerIdx = writerIndex; + final int newIdx = writerIdx + 4; + ensure(newIdx); + if (!LITTLE_ENDIAN) { + value = Integer.reverseBytes(value); + } + storeInt(address + writerIdx, value); + writerIndex = newIdx; + } + + public void writeInt64(long value) { + final int writerIdx = writerIndex; + final int newIdx = writerIdx + 8; + ensure(newIdx); + if (!LITTLE_ENDIAN) { + value = Long.reverseBytes(value); + } + storeLong(address + writerIdx, value); + writerIndex = newIdx; + } + + public void writeFloat32(float value) { + final int writerIdx = writerIndex; + final int newIdx = writerIdx + 4; + ensure(newIdx); + int v = Float.floatToRawIntBits(value); + if (!LITTLE_ENDIAN) { + v = Integer.reverseBytes(v); + } + storeInt(address + writerIdx, v); + writerIndex = newIdx; + } + + public void writeFloat64(double value) { + final int writerIdx = writerIndex; + final int newIdx = writerIdx + 8; + ensure(newIdx); + long v = Double.doubleToRawLongBits(value); + if (!LITTLE_ENDIAN) { + v = Long.reverseBytes(v); + } + storeLong(address + writerIdx, v); + writerIndex = newIdx; + } + + /** + * Write int using variable length encoding. If the value is positive, use {@link #writeVarUInt32} + * to save one bit. + */ + public int writeVarInt32(int v) { + ensure(writerIndex + 8); + // Zigzag encoding: maps negative values to positive values + // This works entirely in int without conversion to long + int varintBytes = _unsafePutVarUInt32(writerIndex, (v << 1) ^ (v >> 31)); + writerIndex += varintBytes; + return varintBytes; + } + + /** + * For implementation efficiency, this method needs at most 8 bytes for writing 5 bytes using long + * to avoid using two memory operations. + */ + @CodegenInvoke + // CHECKSTYLE.OFF:MethodName + public int _unsafeWriteVarInt32(int v) { + // CHECKSTYLE.ON:MethodName + // Zigzag encoding ensures negatives close to zero are encoded in few bytes + int varintBytes = _unsafePutVarUInt32(writerIndex, (v << 1) ^ (v >> 31)); + writerIndex += varintBytes; + return varintBytes; + } + + /** + * Writes a 1-5 byte int. + * + * @return The number of bytes written. + */ + public int writeVarUInt32(int v) { + // ensure at least 8 bytes are writable at once, so jvm-jit + // generated code is smaller. Otherwise, the reference writer fast path + // may be `callee is too large`/`already compiled into a big method` + ensure(writerIndex + 8); + int varintBytes = _unsafePutVarUInt32(writerIndex, v); + writerIndex += varintBytes; + return varintBytes; + } + + /** + * For implementation efficiency, this method needs at most 8 bytes for writing 5 bytes using long + * to avoid using two memory operations. + */ + // CHECKSTYLE.OFF:MethodName + public int _unsafeWriteVarUInt32(int v) { + // CHECKSTYLE.ON:MethodName + int varintBytes = _unsafePutVarUInt32(writerIndex, v); + writerIndex += varintBytes; + return varintBytes; + } + + /** + * Fast method for write an unsigned varint which is mostly a small value in 7 bits value in [0, + * 127). When the value is equal or greater than 127, the write will be a little slower. + */ + public int writeVarUInt32Small7(int value) { + ensure(writerIndex + 8); + if (value >>> 7 == 0) { + storeByte(address + writerIndex++, (byte) value); + return 1; + } + return continueWriteVarUInt32Small7(value); + } + + // Generated serializers depend on these small-varint JVM paths staying small enough for C2 + // inlining. Android exits through MemoryOps above; keep little-endian 1/2-byte stores local and + // move only cold 3+ byte or big-endian cases into helpers. + private int continueWriteVarUInt32Small7(int value) { + int writerIdx = writerIndex; + byte[] heap = heapMemory; + if (heap != null) { + int diff = putVarUInt32Heap(heap, (int) (address + writerIdx), value); + writerIndex += diff; + return diff; + } + ByteBuffer direct = nativeOffHeapBuffer; + if (direct != null) { + int diff = putVarUInt32Direct(direct, writerIdx, value); + writerIndex += diff; + return diff; + } + int encoded = (value & 0x7F); + encoded |= (((value & 0x3f80) << 1) | 0x80); + if (!LITTLE_ENDIAN) { + int diff = putVarUInt32BigEndian(writerIdx, encoded, value); + writerIndex += diff; + return diff; + } + if (value >>> 14 == 0) { + storeInt(address + writerIdx, encoded); + writerIndex += 2; + return 2; + } + int diff = continuePutVarUInt32(writerIdx, encoded, value); + writerIndex += diff; + return diff; + } + + /** + * Writes an unsigned 32-bit varint at the given index using int operations. Caller must ensure + * there are at least 8 bytes available for writing. This method avoids int-to-long conversion + * overhead for the common cases (1-4 bytes). + * + * @param index the position to write at + * @param value the unsigned 32-bit value (high bit may be set) + * @return the number of bytes written (1-5) + */ + // CHECKSTYLE.OFF:MethodName + public int _unsafePutVarUInt32(int index, int value) { + // CHECKSTYLE.ON:MethodName + byte[] heap = heapMemory; + if (heap != null) { + return putVarUInt32Heap(heap, (int) (address + index), value); + } + ByteBuffer direct = nativeOffHeapBuffer; + if (direct != null) { + return putVarUInt32Direct(direct, index, value); + } + int encoded = (value & 0x7F); + if (value >>> 7 == 0) { + storeByte(address + index, (byte) value); + return 1; + } + // bit 8 `set` indicates have next data bytes. + // 0x3f80: 0b1111111 << 7 + encoded |= (((value & 0x3f80) << 1) | 0x80); + if (!LITTLE_ENDIAN) { + return putVarUInt32BigEndian(index, encoded, value); + } + if (value >>> 14 == 0) { + storeInt(address + index, encoded); + return 2; + } + return continuePutVarUInt32(index, encoded, value); + } + + private int putVarUInt32Direct(ByteBuffer direct, int index, int value) { + int pos = (int) (address + index); + int encoded = (value & 0x7F); + if (value >>> 7 == 0) { + direct.put(pos, (byte) value); + return 1; + } + encoded |= (((value & 0x3f80) << 1) | 0x80); + if (!LITTLE_ENDIAN) { + return putVarUInt32BigEndian(index, encoded, value); + } + if (value >>> 14 == 0) { + BYTE_BUFFER_INT.set(direct, pos, encoded); + return 2; + } + return continuePutVarUInt32Direct(direct, pos, encoded, value); + } + + private static int continuePutVarUInt32Direct( + ByteBuffer direct, int pos, int encoded, int value) { + encoded |= (((value & 0x1fc000) << 2) | 0x8000); + if (value >>> 21 == 0) { + BYTE_BUFFER_INT.set(direct, pos, encoded); + return 3; + } + encoded |= ((value & 0xfe00000) << 3) | 0x800000; + if (value >>> 28 == 0) { + BYTE_BUFFER_INT.set(direct, pos, encoded); + return 4; + } + long encodedLong = Integer.toUnsignedLong(encoded) | 0x80000000L; + encodedLong |= (long) (value >>> 28) << 32; + BYTE_BUFFER_LONG.set(direct, pos, encodedLong); + return 5; + } + + private int continuePutVarUInt32(int index, int encoded, int value) { + // 0x1fc000: 0b1111111 << 14 + encoded |= (((value & 0x1fc000) << 2) | 0x8000); + if (value >>> 21 == 0) { + storeInt(address + index, encoded); + return 3; + } + // 0xfe00000: 0b1111111 << 21 + encoded |= ((value & 0xfe00000) << 3) | 0x800000; + if (value >>> 28 == 0) { + storeInt(address + index, encoded); + return 4; + } + // 5-byte case: bits 28-31 go to the 5th byte + // Need long for the final write to include the 5th byte + long encodedLong = Integer.toUnsignedLong(encoded) | 0x80000000L; + encodedLong |= (long) (value >>> 28) << 32; + storeLong(address + index, encodedLong); + return 5; + } + + private int putVarUInt32BigEndian(int index, int encoded, int value) { + if (value >>> 14 == 0) { + storeInt(address + index, Integer.reverseBytes(encoded)); + return 2; + } + return continuePutVarUInt32BigEndian(index, encoded, value); + } + + private int continuePutVarUInt32BigEndian(int index, int encoded, int value) { + // 0x1fc000: 0b1111111 << 14 + encoded |= (((value & 0x1fc000) << 2) | 0x8000); + if (value >>> 21 == 0) { + storeInt(address + index, Integer.reverseBytes(encoded)); + return 3; + } + // 0xfe00000: 0b1111111 << 21 + encoded |= ((value & 0xfe00000) << 3) | 0x800000; + if (value >>> 28 == 0) { + storeInt(address + index, Integer.reverseBytes(encoded)); + return 4; + } + // 5-byte case: bits 28-31 go to the 5th byte + // Need long for the final write to include the 5th byte + long encodedLong = Integer.toUnsignedLong(encoded) | 0x80000000L; + encodedLong |= (long) (value >>> 28) << 32; + storeLong(address + index, Long.reverseBytes(encodedLong)); + return 5; + } + + /** + * Caller must ensure there must be at least 8 bytes for writing, otherwise the crash may occur. + * Don't pass int value to avoid sign extension. + */ + // CHECKSTYLE.OFF:MethodName + public int _unsafePutVarUint36Small(int index, long value) { + // CHECKSTYLE.ON:MethodName + long encoded = (value & 0x7F); + if (value >>> 7 == 0) { + storeByte(address + index, (byte) value); + return 1; + } + // bit 8 `set` indicates have next data bytes. + // 0x3f80: 0b1111111 << 7 + encoded |= (((value & 0x3f80) << 1) | 0x80); + if (!LITTLE_ENDIAN) { + return putVarUint36SmallBigEndian(index, encoded, value); + } + if (value >>> 14 == 0) { + storeInt(address + index, (int) encoded); + return 2; + } + return continuePutVarUint36Small(index, encoded, value); + } + + private int continuePutVarUint36Small(int index, long encoded, long value) { + // 0x1fc000: 0b1111111 << 14 + encoded |= (((value & 0x1fc000) << 2) | 0x8000); + if (value >>> 21 == 0) { + storeInt(address + index, (int) encoded); + return 3; + } + // 0xfe00000: 0b1111111 << 21 + encoded |= ((value & 0xfe00000) << 3) | 0x800000; + if (value >>> 28 == 0) { + storeInt(address + index, (int) encoded); + return 4; + } + // 0xff0000000: 0b11111111 << 28. Note eight `1` here instead of seven. + encoded |= ((value & 0xff0000000L) << 4) | 0x80000000L; + storeLong(address + index, encoded); + return 5; + } + + private int putVarUint36SmallBigEndian(int index, long encoded, long value) { + if (value >>> 14 == 0) { + storeInt(address + index, Integer.reverseBytes((int) encoded)); + return 2; + } + return continuePutVarUint36SmallBigEndian(index, encoded, value); + } + + private int continuePutVarUint36SmallBigEndian(int index, long encoded, long value) { + // 0x1fc000: 0b1111111 << 14 + encoded |= (((value & 0x1fc000) << 2) | 0x8000); + if (value >>> 21 == 0) { + storeInt(address + index, Integer.reverseBytes((int) encoded)); + return 3; + } + // 0xfe00000: 0b1111111 << 21 + encoded |= ((value & 0xfe00000) << 3) | 0x800000; + if (value >>> 28 == 0) { + storeInt(address + index, Integer.reverseBytes((int) encoded)); + return 4; + } + // 0xff0000000: 0b11111111 << 28. Note eight `1` here instead of seven. + encoded |= ((value & 0xff0000000L) << 4) | 0x80000000L; + storeLong(address + index, Long.reverseBytes(encoded)); + return 5; + } + + /** + * Writes a 1-9 byte int, padding necessary bytes to align `writerIndex` to 4-byte. + * + * @return The number of bytes written. + */ + public int writeVarUInt32Aligned(int value) { + // Mask first 6 bits, + // bit 7 `unset` indicates have next padding bytes, + // bit 8 `set` indicates have next data bytes. + if (value >>> 6 == 0) { + return writeVarUInt32Aligned1(value); + } + if (value >>> 12 == 0) { // 2 byte data + return writeVarUInt32Aligned2(value); + } + if (value >>> 18 == 0) { // 3 byte data + return writeVarUInt32Aligned3(value); + } + if (value >>> 24 == 0) { // 4 byte data + return writeVarUInt32Aligned4(value); + } + if (value >>> 30 == 0) { // 5 byte data + return writeVarUInt32Aligned5(value); + } + // 6 byte data + return writeVarUInt32Aligned6(value); + } + + private int writeVarUInt32Aligned1(int value) { + final int writerIdx = writerIndex; + int numPaddingBytes = 4 - writerIdx % 4; + ensure(writerIdx + 5); // 1 byte + 4 bytes(zero out), padding range in (zero out) + int first = (value & 0x3F); + final long pos = address + writerIdx; + if (numPaddingBytes == 1) { + // bit 7 `set` indicates not have padding bytes. + // bit 8 `set` indicates have next data bytes. + storeByte(pos, (byte) (first | 0x40)); + writerIndex = (writerIdx + 1); + return 1; + } else { + storeByte(pos, (byte) first); + // zero out 4 bytes, so that `bit 7` value can be trusted. + storeInt(pos + 1, 0); + storeByte(pos + numPaddingBytes - 1, (byte) (0x40)); + writerIndex = writerIdx + numPaddingBytes; + return numPaddingBytes; + } + } + + private int writeVarUInt32Aligned2(int value) { + final int writerIdx = writerIndex; + int numPaddingBytes = 4 - writerIdx % 4; + ensure(writerIdx + 6); // 2 byte + 4 bytes(zero out), padding range in (zero out) + int first = (value & 0x3F); + final long pos = address + writerIdx; + storeByte(pos, (byte) (first | 0x80)); + if (numPaddingBytes == 2) { + // bit 7 `set` indicates not have padding bytes. + // bit 8 `set` indicates have next data bytes. + storeByte(pos + 1, (byte) ((value >>> 6) | 0x40)); + writerIndex = writerIdx + 2; + return 2; + } else { + storeByte(pos + 1, (byte) (value >>> 6)); + // zero out 4 bytes, so that `bit 7` value can be trusted. + storeInt(pos + 2, 0); + if (numPaddingBytes > 2) { + storeByte(pos + numPaddingBytes - 1, (byte) (0x40)); + writerIndex = writerIdx + numPaddingBytes; + return numPaddingBytes; + } else { + storeByte(pos + 4, (byte) (0x40)); + writerIndex = writerIdx + numPaddingBytes + 4; + return numPaddingBytes + 4; + } + } + } + + private int writeVarUInt32Aligned3(int value) { + final int writerIdx = writerIndex; + int numPaddingBytes = 4 - writerIdx % 4; + ensure(writerIdx + 7); // 3 byte + 4 bytes(zero out), padding range in (zero out) + int first = (value & 0x3F); + final long pos = address + writerIdx; + storeByte(pos, (byte) (first | 0x80)); + storeByte(pos + 1, (byte) ((value >>> 6) | 0x80)); + if (numPaddingBytes == 3) { + // bit 7 `set` indicates not have padding bytes. + // bit 8 `set` indicates have next data bytes. + storeByte(pos + 2, (byte) ((value >>> 12) | 0x40)); + writerIndex = writerIdx + 3; + return 3; + } else { + storeByte(pos + 2, (byte) (value >>> 12)); + // zero out 4 bytes, so that `bit 7` value can be trusted. + storeInt(pos + 3, 0); + if (numPaddingBytes == 4) { + storeByte(pos + numPaddingBytes - 1, (byte) (0x40)); + writerIndex = writerIdx + numPaddingBytes; + return numPaddingBytes; + } else { + storeByte(pos + numPaddingBytes + 3, (byte) (0x40)); + writerIndex = writerIdx + numPaddingBytes + 4; + return numPaddingBytes + 4; + } + } + } + + private int writeVarUInt32Aligned4(int value) { + final int writerIdx = writerIndex; + int numPaddingBytes = 4 - writerIdx % 4; + ensure(writerIdx + 8); // 4 byte + 4 bytes(zero out), padding range in (zero out) + int first = (value & 0x3F); + final long pos = address + writerIdx; + storeByte(pos, (byte) (first | 0x80)); + storeByte(pos + 1, (byte) (value >>> 6 | 0x80)); + storeByte(pos + 2, (byte) (value >>> 12 | 0x80)); + if (numPaddingBytes == 4) { + // bit 7 `set` indicates not have padding bytes. + // bit 8 `set` indicates have next data bytes. + storeByte(pos + 3, (byte) ((value >>> 18) | 0x40)); + writerIndex = writerIdx + 4; + return 4; + } else { + storeByte(pos + 3, (byte) (value >>> 18)); + // zero out 4 bytes, so that `bit 7` value can be trusted. + storeInt(pos + 4, 0); + storeByte(pos + numPaddingBytes + 3, (byte) (0x40)); + writerIndex = writerIdx + numPaddingBytes + 4; + return numPaddingBytes + 4; + } + } + + private int writeVarUInt32Aligned5(int value) { + final int writerIdx = writerIndex; + int numPaddingBytes = 4 - writerIdx % 4; + ensure(writerIdx + 9); // 5 byte + 4 bytes(zero out), padding range in (zero out) + int first = (value & 0x3F); + final long pos = address + writerIdx; + storeByte(pos, (byte) (first | 0x80)); + storeByte(pos + 1, (byte) (value >>> 6 | 0x80)); + storeByte(pos + 2, (byte) (value >>> 12 | 0x80)); + storeByte(pos + 3, (byte) (value >>> 18 | 0x80)); + if (numPaddingBytes == 1) { + // bit 7 `set` indicates not have padding bytes. + // bit 8 `set` indicates have next data bytes. + storeByte(pos + 4, (byte) ((value >>> 24) | 0x40)); + writerIndex = writerIdx + 5; + return 5; + } else { + storeByte(pos + 4, (byte) (value >>> 24)); + // zero out 4 bytes, so that `bit 7` value can be trusted. + storeInt(pos + 5, 0); + storeByte(pos + numPaddingBytes + 3, (byte) (0x40)); + writerIndex = writerIdx + numPaddingBytes + 4; + return numPaddingBytes + 4; + } + } + + private int writeVarUInt32Aligned6(int value) { + final int writerIdx = writerIndex; + int numPaddingBytes = 4 - writerIdx % 4; + ensure(writerIdx + 10); // 6 byte + 4 bytes(zero out), padding range in (zero out) + int first = (value & 0x3F); + final long pos = address + writerIdx; + storeByte(pos, (byte) (first | 0x80)); + storeByte(pos + 1, (byte) (value >>> 6 | 0x80)); + storeByte(pos + 2, (byte) (value >>> 12 | 0x80)); + storeByte(pos + 3, (byte) (value >>> 18 | 0x80)); + storeByte(pos + 4, (byte) (value >>> 24 | 0x80)); + if (numPaddingBytes == 2) { + // bit 7 `set` indicates not have padding bytes. + // bit 8 `set` indicates have next data bytes. + storeByte(pos + 5, (byte) ((value >>> 30) | 0x40)); + writerIndex = writerIdx + 6; + return 6; + } else { + storeByte(pos + 5, (byte) (value >>> 30)); + // zero out 4 bytes, so that `bit 7` value can be trusted. + storeInt(pos + 6, 0); + if (numPaddingBytes == 1) { + storeByte(pos + 8, (byte) (0x40)); + writerIndex = writerIdx + 9; + return 9; + } else { + storeByte(pos + numPaddingBytes + 3, (byte) (0x40)); + writerIndex = writerIdx + numPaddingBytes + 4; + return numPaddingBytes + 4; + } + } + } + + /** + * Write long using variable length encoding. If the value is positive, use {@link + * #writeVarUInt64} to save one bit. + */ + public int writeVarInt64(long value) { + ensure(writerIndex + 9); + return _unsafeWriteVarUInt64((value << 1) ^ (value >> 63)); + } + + @CodegenInvoke + // CHECKSTYLE.OFF:MethodName + public int _unsafeWriteVarInt64(long value) { + // CHECKSTYLE.ON:MethodName + return _unsafeWriteVarUInt64((value << 1) ^ (value >> 63)); + } + + public int writeVarUInt64(long value) { + // Var long encoding algorithm is based kryo UnsafeMemoryOutput.writeVarInt64. + // var long are written using little endian byte order. + ensure(writerIndex + 9); + return _unsafeWriteVarUInt64(value); + } + + // CHECKSTYLE.OFF:MethodName + @CodegenInvoke + public int _unsafeWriteVarUInt64(long value) { + // CHECKSTYLE.ON:MethodName + final int writerIndex = this.writerIndex; + int varInt; + varInt = (int) (value & 0x7F); + if (value >>> 7 == 0) { + storeByte(address + writerIndex, (byte) varInt); + this.writerIndex = writerIndex + 1; + return 1; + } + varInt |= (int) (((value & 0x3f80) << 1) | 0x80); + if (value >>> 14 == 0) { + _unsafePutInt32(writerIndex, varInt); + this.writerIndex = writerIndex + 2; + return 2; + } + varInt |= (int) (((value & 0x1fc000) << 2) | 0x8000); + if (value >>> 21 == 0) { + _unsafePutInt32(writerIndex, varInt); + this.writerIndex = writerIndex + 3; + return 3; + } + varInt |= (int) (((value & 0xfe00000) << 3) | 0x800000); + if (value >>> 28 == 0) { + _unsafePutInt32(writerIndex, varInt); + this.writerIndex = writerIndex + 4; + return 4; + } + long varLong = (varInt & 0xFFFFFFFFL); + varLong |= ((value & 0x7f0000000L) << 4) | 0x80000000L; + if (value >>> 35 == 0) { + _unsafePutInt64(writerIndex, varLong); + this.writerIndex = writerIndex + 5; + return 5; + } + varLong |= ((value & 0x3f800000000L) << 5) | 0x8000000000L; + if (value >>> 42 == 0) { + _unsafePutInt64(writerIndex, varLong); + this.writerIndex = writerIndex + 6; + return 6; + } + varLong |= ((value & 0x1fc0000000000L) << 6) | 0x800000000000L; + if (value >>> 49 == 0) { + _unsafePutInt64(writerIndex, varLong); + this.writerIndex = writerIndex + 7; + return 7; + } + varLong |= ((value & 0xfe000000000000L) << 7) | 0x80000000000000L; + value >>>= 56; + if (value == 0) { + _unsafePutInt64(writerIndex, varLong); + this.writerIndex = writerIndex + 8; + return 8; + } + _unsafePutInt64(writerIndex, varLong | 0x8000000000000000L); + storeByte(address + writerIndex + 8, (byte) (value & 0xFF)); + this.writerIndex = writerIndex + 9; + return 9; + } + + /** + * Write signed long using fory Tagged(Small long as int) encoding. If long is in [0xc0000000, + * 0x3fffffff], encode as 4 bytes int: {@code | little-endian: ((int) value) << 1 |}; Otherwise + * write as 9 bytes: {@code | 0b1 | little-endian 8bytes long |}. + */ + public int writeTaggedInt64(long value) { + ensure(writerIndex + 9); + return _unsafeWriteTaggedInt64(value); + } + + /** + * Write unsigned long using fory Tagged(Small long as int) encoding. If long is in [0, + * 0x7fffffff], encode as 4 bytes int: {@code | little-endian: ((int) value) << 1 |}; Otherwise + * write as 9 bytes: {@code | 0b1 | little-endian 8bytes long |}. + */ + public int writeTaggedUInt64(long value) { + ensure(writerIndex + 9); + return _unsafeWriteTaggedUInt64(value); + } + + /** Write unsigned long using fory Tagged(Small Long as Int) encoding. */ + // CHECKSTYLE.OFF:MethodName + public int _unsafeWriteTaggedUInt64(long value) { + // CHECKSTYLE.ON:MethodName + final int writerIndex = this.writerIndex; + final long pos = address + writerIndex; + final byte[] heapMemory = this.heapMemory; + if (value >= 0 && value <= Integer.MAX_VALUE) { + int v = ((int) value) << 1; // bit 0 unset, means int. + if (!LITTLE_ENDIAN) { + v = Integer.reverseBytes(v); + } + storeInt(pos, v); + this.writerIndex = writerIndex + 4; + return 4; + } else { + storeByte(pos, BIG_LONG_FLAG); + if (!LITTLE_ENDIAN) { + value = Long.reverseBytes(value); + } + storeLong(pos + 1, value); + this.writerIndex = writerIndex + 9; + return 9; + } + } + + private static final long HALF_MAX_INT_VALUE = Integer.MAX_VALUE / 2; + private static final long HALF_MIN_INT_VALUE = Integer.MIN_VALUE / 2; + private static final byte BIG_LONG_FLAG = 0b1; // bit 0 set, means big long. + + /** Write long using fory Tagged(Small Long as Int) encoding. */ + // CHECKSTYLE.OFF:MethodName + public int _unsafeWriteTaggedInt64(long value) { + // CHECKSTYLE.ON:MethodName + final int writerIndex = this.writerIndex; + final long pos = address + writerIndex; + final byte[] heapMemory = this.heapMemory; + if (value >= HALF_MIN_INT_VALUE && value <= HALF_MAX_INT_VALUE) { + // write: + // 00xxx -> 0xxx + // 11xxx -> 1xxx + // read: + // 0xxx -> 00xxx + // 1xxx -> 11xxx + int v = ((int) value) << 1; // bit 0 unset, means int. + if (!LITTLE_ENDIAN) { + v = Integer.reverseBytes(v); + } + storeInt(pos, v); + this.writerIndex = writerIndex + 4; + return 4; + } else { + storeByte(pos, BIG_LONG_FLAG); + if (!LITTLE_ENDIAN) { + value = Long.reverseBytes(value); + } + storeLong(pos + 1, value); + this.writerIndex = writerIndex + 9; + return 9; + } + } + + public void writeBytes(byte[] bytes) { + writeBytes(bytes, 0, bytes.length); + } + + public void writeBytes(byte[] bytes, int offset, int length) { + final int writerIdx = writerIndex; + final int newIdx = writerIdx + length; + ensure(newIdx); + put(writerIdx, bytes, offset, length); + writerIndex = newIdx; + } + + public void write(ByteBuffer source) { + write(source, source.remaining()); + } + + public void write(ByteBuffer source, int numBytes) { + final int writerIdx = writerIndex; + final int newIdx = writerIdx + numBytes; + ensure(newIdx); + put(writerIdx, source, numBytes); + writerIndex = newIdx; + } + + public void writeBytesWithSize(byte[] values) { + writeVarUInt32Small7(values.length); + writeBytes(values, 0, values.length); + } + + public void writeBooleansWithSize(boolean[] values) { + writeVarUInt32Small7(values.length); + writeBooleans(values, 0, values.length); + } + + public void writeBooleans(boolean[] values) { + writeBooleans(values, 0, values.length); + } + + public void writeBooleans(boolean[] values, int offset, int numElements) { + final int writerIdx = writerIndex; + final int newIdx = writerIdx + numElements; + ensure(newIdx); + writeBooleansFromArray(address + writerIdx, values, BOOLEAN_ARRAY_OFFSET + offset, numElements); + writerIndex = newIdx; + } + + public void writeCharsWithSize(char[] values) { + int numBytes = Math.multiplyExact(values.length, 2); + writeVarUInt32Small7(numBytes); + writeChars(values, 0, values.length); + } + + public void writeChars(char[] values) { + writeChars(values, 0, values.length); + } + + public void writeChars(char[] values, int offset, int numElements) { + int numBytes = Math.multiplyExact(numElements, 2); + final int writerIdx = writerIndex; + final int newIdx = writerIdx + numBytes; + ensure(newIdx); + writeCharsFromArray(address + writerIdx, values, CHAR_ARRAY_OFFSET + offset, numBytes); + writerIndex = newIdx; + } + + public void writeShortsWithSize(short[] values) { + int numBytes = Math.multiplyExact(values.length, 2); + writeVarUInt32Small7(numBytes); + writeShorts(values, 0, values.length); + } + + public void writeShorts(short[] values) { + writeShorts(values, 0, values.length); + } + + public void writeShorts(short[] values, int offset, int numElements) { + int numBytes = Math.multiplyExact(numElements, 2); + final int writerIdx = writerIndex; + final int newIdx = writerIdx + numBytes; + ensure(newIdx); + writeShortsFromArray(address + writerIdx, values, SHORT_ARRAY_OFFSET + offset, numBytes); + writerIndex = newIdx; + } + + public void writeIntsWithSize(int[] values) { + int numBytes = Math.multiplyExact(values.length, 4); + writeVarUInt32Small7(numBytes); + writeInts(values, 0, values.length); + } + + public void writeInts(int[] values) { + writeInts(values, 0, values.length); + } + + public void writeInts(int[] values, int offset, int numElements) { + int numBytes = Math.multiplyExact(numElements, 4); + final int writerIdx = writerIndex; + final int newIdx = writerIdx + numBytes; + ensure(newIdx); + writeIntsFromArray(address + writerIdx, values, INT_ARRAY_OFFSET + offset, numBytes); + writerIndex = newIdx; + } + + public void writeLongsWithSize(long[] values) { + int numBytes = Math.multiplyExact(values.length, 8); + writeVarUInt32Small7(numBytes); + writeLongs(values, 0, values.length); + } + + public void writeLongs(long[] values) { + writeLongs(values, 0, values.length); + } + + public void writeLongs(long[] values, int offset, int numElements) { + int numBytes = Math.multiplyExact(numElements, 8); + final int writerIdx = writerIndex; + final int newIdx = writerIdx + numBytes; + ensure(newIdx); + writeLongsFromArray(address + writerIdx, values, LONG_ARRAY_OFFSET + offset, numBytes); + writerIndex = newIdx; + } + + public void writeFloatsWithSize(float[] values) { + int numBytes = Math.multiplyExact(values.length, 4); + writeVarUInt32Small7(numBytes); + writeFloats(values, 0, values.length); + } + + public void writeFloats(float[] values) { + writeFloats(values, 0, values.length); + } + + public void writeFloats(float[] values, int offset, int numElements) { + int numBytes = Math.multiplyExact(numElements, 4); + final int writerIdx = writerIndex; + final int newIdx = writerIdx + numBytes; + ensure(newIdx); + writeFloatsFromArray(address + writerIdx, values, FLOAT_ARRAY_OFFSET + offset, numBytes); + writerIndex = newIdx; + } + + public void writeDoublesWithSize(double[] values) { + int numBytes = Math.multiplyExact(values.length, 8); + writeVarUInt32Small7(numBytes); + writeDoubles(values, 0, values.length); + } + + public void writeDoubles(double[] values) { + writeDoubles(values, 0, values.length); + } + + public void writeDoubles(double[] values, int offset, int numElements) { + int numBytes = Math.multiplyExact(numElements, 8); + final int writerIdx = writerIndex; + final int newIdx = writerIdx + numBytes; + ensure(newIdx); + writeDoublesFromArray(address + writerIdx, values, DOUBLE_ARRAY_OFFSET + offset, numBytes); + writerIndex = newIdx; + } + + /** For off-heap buffer, this will make a heap buffer internally. */ + public void grow(int neededSize) { + int length = writerIndex + neededSize; + if (length > size) { + globalAllocator.grow(this, length); + } + } + + /** For off-heap buffer, this will make a heap buffer internally. */ + public void ensure(int length) { + if (length > size) { + globalAllocator.grow(this, length); + } + } + + // ------------------------------------------------------------------------- + // Read Methods + // ------------------------------------------------------------------------- + + // Check should be done outside to avoid this method got into the critical path. + private void throwIndexOOBExceptionForRead() { + throw new IndexOutOfBoundsException( + String.format( + "readerIndex: %d (expected: 0 <= readerIndex <= size(%d))", readerIndex, size)); + } + + // Check should be done outside to avoid this method got into the critical path. + private void throwIndexOOBExceptionForRead(int length) { + throw new IndexOutOfBoundsException( + String.format( + "readerIndex: %d (expected: 0 <= readerIndex <= size(%d)), length %d", + readerIndex, size, length)); + } + + /** Returns the {@code readerIndex} of this buffer. */ + public int readerIndex() { + return readerIndex; + } + + /** + * Sets the {@code readerIndex} of this buffer. + * + * @throws IndexOutOfBoundsException if the specified {@code readerIndex} is less than {@code 0} + * or greater than {@code this.size} + */ + public void readerIndex(int readerIndex) { + if (readerIndex < 0) { + throwIndexOOBExceptionForRead(); + } else if (readerIndex > size) { + // in this case, diff must be greater than 0. + streamReader.fillBuffer(readerIndex - size); + } + this.readerIndex = readerIndex; + } + + /** Returns array index for reader index if buffer is a heap buffer. */ + // CHECKSTYLE.OFF:MethodName + public int _unsafeHeapReaderIndex() { + // CHECKSTYLE.ON:MethodName + return readerIndex + heapOffset; + } + + // CHECKSTYLE.OFF:MethodName + public void _increaseReaderIndexUnsafe(int diff) { + // CHECKSTYLE.ON:MethodName + readerIndex += diff; + } + + public void increaseReaderIndex(int diff) { + int readerIdx = readerIndex; + readerIndex = readerIdx += diff; + if (readerIdx < 0) { + throwIndexOOBExceptionForRead(); + } else if (readerIdx > size) { + // in this case, diff must be greater than 0. + streamReader.fillBuffer(readerIdx - size); + } + } + + public long getUnsafeReaderAddress() { + checkHeapAddressAccess(); + return address + readerIndex; + } + + private void checkHeapAddressAccess() { + if (heapMemory == null) { + throw new UnsupportedOperationException( + "JDK25 MemoryBuffer does not expose raw native addresses for direct buffers."); + } + } + + public int remaining() { + return size - readerIndex; + } + + public boolean readBoolean() { + int readerIdx = readerIndex; + // use subtract to avoid overflow + if (readerIdx > size - 1) { + streamReader.fillBuffer(1); + } + readerIndex = readerIdx + 1; + return loadByte(address + readerIdx) != 0; + } + + public int readUInt8() { + int readerIdx = readerIndex; + if (readerIdx > size - 1) { + streamReader.fillBuffer(1); + } + readerIndex = readerIdx + 1; + int v = loadByte(address + readerIdx); + v &= 0b11111111; + return v; + } + + public int readUnsignedByte() { + return readUInt8(); + } + + public byte readByte() { + int readerIdx = readerIndex; + if (readerIdx > size - 1) { + streamReader.fillBuffer(1); + } + readerIndex = readerIdx + 1; + return loadByte(address + readerIdx); + } + + public char readChar() { + int readerIdx = readerIndex; + // use subtract to avoid overflow + int remaining = size - readerIdx; + if (remaining < 2) { + streamReader.fillBuffer(2 - remaining); + } + readerIndex = readerIdx + 2; + char c = loadChar(address + readerIdx); + return LITTLE_ENDIAN ? c : Character.reverseBytes(c); + } + + public short readInt16() { + int readerIdx = readerIndex; + // use subtract to avoid overflow + int remaining = size - readerIdx; + if (remaining < 2) { + streamReader.fillBuffer(2 - remaining); + } + readerIndex = readerIdx + 2; + short v = loadShort(address + readerIdx); + return LITTLE_ENDIAN ? v : Short.reverseBytes(v); + } + + // Reduce method body for better inline in the caller. + @CodegenInvoke + // CHECKSTYLE.OFF:MethodName + public short _readInt16OnLE() { + // CHECKSTYLE.ON:MethodName + int readerIdx = readerIndex; + // use subtract to avoid overflow + int remaining = size - readerIdx; + if (remaining < 2) { + streamReader.fillBuffer(2 - remaining); + } + readerIndex = readerIdx + 2; + return loadShort(address + readerIdx); + } + + // Reduce method body for better inline in the caller. + @CodegenInvoke + // CHECKSTYLE.OFF:MethodName + public short _readInt16OnBE() { + // CHECKSTYLE.ON:MethodName + int readerIdx = readerIndex; + // use subtract to avoid overflow + int remaining = size - readerIdx; + if (remaining < 2) { + streamReader.fillBuffer(2 - remaining); + } + readerIndex = readerIdx + 2; + return Short.reverseBytes(loadShort(address + readerIdx)); + } + + public int readInt32() { + int readerIdx = readerIndex; + // use subtract to avoid overflow + int remaining = size - readerIdx; + if (remaining < 4) { + streamReader.fillBuffer(4 - remaining); + } + readerIndex = readerIdx + 4; + int v = loadInt(address + readerIdx); + return LITTLE_ENDIAN ? v : Integer.reverseBytes(v); + } + + // Reduce method body for better inline in the caller. + @CodegenInvoke + // CHECKSTYLE.OFF:MethodName + public int _readInt32OnLE() { + // CHECKSTYLE.ON:MethodName + int readerIdx = readerIndex; + // use subtract to avoid overflow + int remaining = size - readerIdx; + if (remaining < 4) { + streamReader.fillBuffer(4 - remaining); + } + readerIndex = readerIdx + 4; + return loadInt(address + readerIdx); + } + + // Reduce method body for better inline in the caller. + @CodegenInvoke + // CHECKSTYLE.OFF:MethodName + public int _readInt32OnBE() { + // CHECKSTYLE.ON:MethodName + int readerIdx = readerIndex; + // use subtract to avoid overflow + int remaining = size - readerIdx; + if (remaining < 4) { + streamReader.fillBuffer(4 - remaining); + } + readerIndex = readerIdx + 4; + return Integer.reverseBytes(loadInt(address + readerIdx)); + } + + public long readInt64() { + int readerIdx = readerIndex; + // use subtract to avoid overflow + int remaining = size - readerIdx; + if (remaining < 8) { + streamReader.fillBuffer(8 - remaining); + } + readerIndex = readerIdx + 8; + long v = loadLong(address + readerIdx); + return LITTLE_ENDIAN ? v : Long.reverseBytes(v); + } + + // Reduce method body for better inline in the caller. + @CodegenInvoke + // CHECKSTYLE.OFF:MethodName + public long _readInt64OnLE() { + // CHECKSTYLE.ON:MethodName + int readerIdx = readerIndex; + // use subtract to avoid overflow + int remaining = size - readerIdx; + if (remaining < 8) { + streamReader.fillBuffer(8 - remaining); + } + readerIndex = readerIdx + 8; + return loadLong(address + readerIdx); + } + + // Reduce method body for better inline in the caller. + @CodegenInvoke + // CHECKSTYLE.OFF:MethodName + public long _readInt64OnBE() { + // CHECKSTYLE.ON:MethodName + int readerIdx = readerIndex; + // use subtract to avoid overflow + int remaining = size - readerIdx; + if (remaining < 8) { + streamReader.fillBuffer(8 - remaining); + } + readerIndex = readerIdx + 8; + return Long.reverseBytes(loadLong(address + readerIdx)); + } + + /** Read signed fory Tagged(Small Long as Int) encoded long. */ + public long readTaggedInt64() { + if (LITTLE_ENDIAN) { + return _readTaggedInt64OnLE(); + } else { + return _readTaggedInt64OnBE(); + } + } + + /** Read unsigned fory Tagged(Small Long as Int) encoded long. */ + public long readTaggedUInt64() { + if (LITTLE_ENDIAN) { + return _readTaggedUInt64OnLE(); + } else { + return _readTaggedUInt64OnBE(); + } + } + + @CodegenInvoke + // CHECKSTYLE.OFF:MethodName + public long _readTaggedUInt64OnLE() { + // CHECKSTYLE.ON:MethodName + final int readIdx = readerIndex; + int diff = size - readIdx; + if (diff < 4) { + streamReader.fillBuffer(4 - diff); + } + int i = loadInt(address + readIdx); + if ((i & 0b1) != 0b1) { + readerIndex = readIdx + 4; + return i >>> 1; // unsigned right shift + } + diff = size - readIdx; + if (diff < 9) { + streamReader.fillBuffer(9 - diff); + } + readerIndex = readIdx + 9; + return loadLong(address + readIdx + 1); + } + + @CodegenInvoke + // CHECKSTYLE.OFF:MethodName + public long _readTaggedUInt64OnBE() { + // CHECKSTYLE.ON:MethodName + final int readIdx = readerIndex; + int diff = size - readIdx; + if (diff < 4) { + streamReader.fillBuffer(4 - diff); + } + int i = Integer.reverseBytes(loadInt(address + readIdx)); + if ((i & 0b1) != 0b1) { + readerIndex = readIdx + 4; + return i >>> 1; // unsigned right shift + } + diff = size - readIdx; + if (diff < 9) { + streamReader.fillBuffer(9 - diff); + } + readerIndex = readIdx + 9; + return Long.reverseBytes(loadLong(address + readIdx + 1)); + } + + @CodegenInvoke + // CHECKSTYLE.OFF:MethodName + public long _readTaggedInt64OnLE() { + // CHECKSTYLE.ON:MethodName + // Duplicate and manual inline for performance. + // noinspection Duplicates + final int readIdx = readerIndex; + int diff = size - readIdx; + if (diff < 4) { + streamReader.fillBuffer(4 - diff); + } + int i = loadInt(address + readIdx); + if ((i & 0b1) != 0b1) { + readerIndex = readIdx + 4; + return i >> 1; + } + diff = size - readIdx; + if (diff < 9) { + streamReader.fillBuffer(9 - diff); + } + readerIndex = readIdx + 9; + return loadLong(address + readIdx + 1); + } + + @CodegenInvoke + // CHECKSTYLE.OFF:MethodName + public long _readTaggedInt64OnBE() { + // CHECKSTYLE.ON:MethodName + // noinspection Duplicates + final int readIdx = readerIndex; + int diff = size - readIdx; + if (diff < 4) { + streamReader.fillBuffer(4 - diff); + } + int i = Integer.reverseBytes(loadInt(address + readIdx)); + if ((i & 0b1) != 0b1) { + readerIndex = readIdx + 4; + return i >> 1; + } + diff = size - readIdx; + if (diff < 9) { + streamReader.fillBuffer(9 - diff); + } + readerIndex = readIdx + 9; + return Long.reverseBytes(loadLong(address + readIdx + 1)); + } + + public float readFloat32() { + // noinspection Duplicates + int readerIdx = readerIndex; + // use subtract to avoid overflow + int remaining = size - readerIdx; + if (remaining < 4) { + streamReader.fillBuffer(4 - remaining); + } + readerIndex = readerIdx + 4; + int v = loadInt(address + readerIdx); + if (!LITTLE_ENDIAN) { + v = Integer.reverseBytes(v); + } + return Float.intBitsToFloat(v); + } + + // Reduce method body for better inline in the caller. + @CodegenInvoke + // CHECKSTYLE.OFF:MethodName + public float _readFloat32OnLE() { + // CHECKSTYLE.ON:MethodName + int readerIdx = readerIndex; + // use subtract to avoid overflow + int remaining = size - readerIdx; + if (remaining < 4) { + streamReader.fillBuffer(4 - remaining); + } + readerIndex = readerIdx + 4; + return Float.intBitsToFloat(loadInt(address + readerIdx)); + } + + // Reduce method body for better inline in the caller. + @CodegenInvoke + // CHECKSTYLE.OFF:MethodName + public float _readFloat32OnBE() { + // CHECKSTYLE.ON:MethodName + int readerIdx = readerIndex; + // use subtract to avoid overflow + int remaining = size - readerIdx; + if (remaining < 4) { + streamReader.fillBuffer(4 - remaining); + } + readerIndex = readerIdx + 4; + return Float.intBitsToFloat(Integer.reverseBytes(loadInt(address + readerIdx))); + } + + public double readFloat64() { + // noinspection Duplicates + int readerIdx = readerIndex; + // use subtract to avoid overflow + int remaining = size - readerIdx; + if (remaining < 8) { + streamReader.fillBuffer(8 - remaining); + } + readerIndex = readerIdx + 8; + long v = loadLong(address + readerIdx); + if (!LITTLE_ENDIAN) { + v = Long.reverseBytes(v); + } + return Double.longBitsToDouble(v); + } + + // Reduce method body for better inline in the caller. + @CodegenInvoke + // CHECKSTYLE.OFF:MethodName + public double _readFloat64OnLE() { + // CHECKSTYLE.ON:MethodName + int readerIdx = readerIndex; + // use subtract to avoid overflow + int remaining = size - readerIdx; + if (remaining < 8) { + streamReader.fillBuffer(8 - remaining); + } + readerIndex = readerIdx + 8; + return Double.longBitsToDouble(loadLong(address + readerIdx)); + } + + // Reduce method body for better inline in the caller. + @CodegenInvoke + // CHECKSTYLE.OFF:MethodName + public double _readFloat64OnBE() { + // CHECKSTYLE.ON:MethodName + int readerIdx = readerIndex; + // use subtract to avoid overflow + int remaining = size - readerIdx; + if (remaining < 8) { + streamReader.fillBuffer(8 - remaining); + } + readerIndex = readerIdx + 8; + return Double.longBitsToDouble(Long.reverseBytes(loadLong(address + readerIdx))); + } + + /** Reads the 1-5 byte int part of a varint. */ + @CodegenInvoke + public int readVarInt32() { + if (LITTLE_ENDIAN) { + return _readVarInt32OnLE(); + } else { + return _readVarInt32OnBE(); + } + } + + /** Reads the 1-5 byte as a varint on a little endian machine. */ + @CodegenInvoke + // CHECKSTYLE.OFF:MethodName + public int _readVarInt32OnLE() { + // CHECKSTYLE.ON:MethodName + // noinspection Duplicates + int readIdx = readerIndex; + int result; + if (size - readIdx < 5) { + result = readVarUInt32Slow(); + } else { + long address = this.address; + // | 1bit + 7bits | 1bit + 7bits | 1bit + 7bits | 1bit + 7bits | + int fourByteValue = loadInt(address + readIdx); + // Duplicate and manual inline for performance. + // noinspection Duplicates + readIdx++; + result = fourByteValue & 0x7F; + if ((fourByteValue & 0x80) != 0) { + readIdx++; + // 0x3f80: 0b1111111 << 7 + result |= (fourByteValue >>> 1) & 0x3f80; + // 0x8000: 0b1 << 15 + if ((fourByteValue & 0x8000) != 0) { + readIdx++; + // 0x1fc000: 0b1111111 << 14 + result |= (fourByteValue >>> 2) & 0x1fc000; + // 0x800000: 0b1 << 23 + if ((fourByteValue & 0x800000) != 0) { + readIdx++; + // 0xfe00000: 0b1111111 << 21 + result |= (fourByteValue >>> 3) & 0xfe00000; + if ((fourByteValue & 0x80000000) != 0) { + int fifthByte = loadByte(address + readIdx++) & 0xFF; + if ((fifthByte & 0xF0) != 0) { + throwMalformedVarUInt32(fifthByte); + } + result |= fifthByte << 28; + } + } + } + } + readerIndex = readIdx; + } + return (result >>> 1) ^ -(result & 1); + } + + /** Reads the 1-5 byte as a varint on a big endian machine. */ + @CodegenInvoke + // CHECKSTYLE.OFF:MethodName + public int _readVarInt32OnBE() { + // CHECKSTYLE.ON:MethodName + // noinspection Duplicates + int readIdx = readerIndex; + int result; + if (size - readIdx < 5) { + result = readVarUInt32Slow(); + } else { + long address = this.address; + int fourByteValue = Integer.reverseBytes(loadInt(address + readIdx)); + // Duplicate and manual inline for performance. + // noinspection Duplicates + readIdx++; + result = fourByteValue & 0x7F; + if ((fourByteValue & 0x80) != 0) { + readIdx++; + // 0x3f80: 0b1111111 << 7 + result |= (fourByteValue >>> 1) & 0x3f80; + // 0x8000: 0b1 << 15 + if ((fourByteValue & 0x8000) != 0) { + readIdx++; + // 0x1fc000: 0b1111111 << 14 + result |= (fourByteValue >>> 2) & 0x1fc000; + // 0x800000: 0b1 << 23 + if ((fourByteValue & 0x800000) != 0) { + readIdx++; + // 0xfe00000: 0b1111111 << 21 + result |= (fourByteValue >>> 3) & 0xfe00000; + if ((fourByteValue & 0x80000000) != 0) { + int fifthByte = loadByte(address + readIdx++) & 0xFF; + if ((fifthByte & 0xF0) != 0) { + throwMalformedVarUInt32(fifthByte); + } + result |= fifthByte << 28; + } + } + } + } + readerIndex = readIdx; + } + return (result >>> 1) ^ -(result & 1); + } + + public long readVarUint36Small() { + // Android exits above. Keep JVM small-varint bulk reads as direct bulk loads instead of calling + // `_unsafeGet*` helpers; those helpers carry Android/endian branches and can break inlining. + // Duplicate and manual inline for performance. + // noinspection Duplicates + int readIdx = readerIndex; + if (size - readIdx >= 9) { + long bulkValue = loadLong(address + readIdx++); + if (!LITTLE_ENDIAN) { + bulkValue = Long.reverseBytes(bulkValue); + } + // noinspection Duplicates + long result = bulkValue & 0x7F; + if ((bulkValue & 0x80) != 0) { + readIdx++; + // 0x3f80: 0b1111111 << 7 + result |= (bulkValue >>> 1) & 0x3f80; + // 0x8000: 0b1 << 15 + if ((bulkValue & 0x8000) != 0) { + return continueReadVarInt36(readIdx, bulkValue, result); + } + } + readerIndex = readIdx; + return result; + } else { + return readVarUint36Slow(); + } + } + + private long continueReadVarInt36(int readIdx, long bulkValue, long result) { + readIdx++; + // 0x1fc000: 0b1111111 << 14 + result |= (bulkValue >>> 2) & 0x1fc000; + // 0x800000: 0b1 << 23 + if ((bulkValue & 0x800000) != 0) { + readIdx++; + // 0xfe00000: 0b1111111 << 21 + result |= (bulkValue >>> 3) & 0xfe00000; + if ((bulkValue & 0x80000000L) != 0) { + readIdx++; + // 0xff0000000: 0b11111111 << 28 + result |= (bulkValue >>> 4) & 0xff0000000L; + } + } + readerIndex = readIdx; + return result; + } + + private long readVarUint36Slow() { + long b = readByte(); + long result = b & 0x7F; + // Note: + // Loop are not used here to improve performance. + // We manually unroll the loop for better performance. + // noinspection Duplicates + if ((b & 0x80) != 0) { + b = readByte(); + result |= (b & 0x7F) << 7; + if ((b & 0x80) != 0) { + b = readByte(); + result |= (b & 0x7F) << 14; + if ((b & 0x80) != 0) { + b = readByte(); + result |= (b & 0x7F) << 21; + if ((b & 0x80) != 0) { + b = readByte(); + result |= (b & 0xFF) << 28; + } + } + } + } + return result; + } + + private int readVarUInt32Slow() { + int b = readByte() & 0xFF; + int result = b & 0x7F; + // Note: + // Loop are not used here to improve performance. + // We manually unroll the loop for better performance. + // noinspection Duplicates + if ((b & 0x80) != 0) { + b = readByte() & 0xFF; + result |= (b & 0x7F) << 7; + if ((b & 0x80) != 0) { + b = readByte() & 0xFF; + result |= (b & 0x7F) << 14; + if ((b & 0x80) != 0) { + b = readByte() & 0xFF; + result |= (b & 0x7F) << 21; + if ((b & 0x80) != 0) { + b = readByte() & 0xFF; + if ((b & 0xF0) != 0) { + throwMalformedVarUInt32(b); + } + result |= b << 28; + } + } + } + } + return result; + } + + private static void throwMalformedVarUInt32(int fifthByte) { + throw new IllegalArgumentException( + "Malformed varuint32 fifth byte " + fifthByte + " exceeds 32 bits"); + } + + /** Reads the 1-5 byte int part of a non-negative varint. */ + public int readVarUInt32() { + int readIdx = readerIndex; + if (size - readIdx < 5) { + return readVarUInt32Slow(); + } + // | 1bit + 7bits | 1bit + 7bits | 1bit + 7bits | 1bit + 7bits | + int fourByteValue = loadInt(address + readIdx); + if (!LITTLE_ENDIAN) { + fourByteValue = Integer.reverseBytes(fourByteValue); + } + readIdx++; + int result = fourByteValue & 0x7F; + // Duplicate and manual inline for performance. + // noinspection Duplicates + if ((fourByteValue & 0x80) != 0) { + readIdx++; + // 0x3f80: 0b1111111 << 7 + result |= (fourByteValue >>> 1) & 0x3f80; + // 0x8000: 0b1 << 15 + if ((fourByteValue & 0x8000) != 0) { + readIdx++; + // 0x1fc000: 0b1111111 << 14 + result |= (fourByteValue >>> 2) & 0x1fc000; + // 0x800000: 0b1 << 23 + if ((fourByteValue & 0x800000) != 0) { + readIdx++; + // 0xfe00000: 0b1111111 << 21 + result |= (fourByteValue >>> 3) & 0xfe00000; + if ((fourByteValue & 0x80000000) != 0) { + int fifthByte = loadByte(address + readIdx++) & 0xFF; + if ((fifthByte & 0xF0) != 0) { + throwMalformedVarUInt32(fifthByte); + } + result |= fifthByte << 28; + } + } + } + } + readerIndex = readIdx; + return result; + } + + /** + * Fast method for read an unsigned varint which is mostly a small value in 7 bits value in [0, + * 127). When the value is equal or greater than 127, the read will be a little slower. + */ + public int readVarUInt32Small7() { + int readIdx = readerIndex; + if (size - readIdx > 0) { + byte v = loadByte(address + readIdx++); + if ((v & 0x80) == 0) { + readerIndex = readIdx; + return v; + } + } + return readVarUInt32Small14(); + } + + /** + * Fast path for read an unsigned varint which is mostly a small value in 14 bits value in [0, + * 16384). When the value is equal or greater than 16384, the read will be a little slower. + */ + public int readVarUInt32Small14() { + int readIdx = readerIndex; + if (size - readIdx >= 5) { + int fourByteValue = loadInt(address + readIdx++); + if (!LITTLE_ENDIAN) { + fourByteValue = Integer.reverseBytes(fourByteValue); + } + int value = fourByteValue & 0x7F; + // Duplicate and manual inline for performance. + // noinspection Duplicates + if ((fourByteValue & 0x80) != 0) { + readIdx++; + value |= (fourByteValue >>> 1) & 0x3f80; + if ((fourByteValue & 0x8000) != 0) { + // merely executed path, make it as a separate method to reduce + // code size of current method for better jvm inline + return continueReadVarUInt32(readIdx, fourByteValue, value); + } + } + readerIndex = readIdx; + return value; + } else { + return readVarUInt32Slow(); + } + } + + private int continueReadVarUInt32(int readIdx, int bulkRead, int value) { + // Duplicate and manual inline for performance. + // noinspection Duplicates + readIdx++; + value |= (bulkRead >>> 2) & 0x1fc000; + if ((bulkRead & 0x800000) != 0) { + readIdx++; + value |= (bulkRead >>> 3) & 0xfe00000; + if ((bulkRead & 0x80000000) != 0) { + int fifthByte = loadByte(address + readIdx++) & 0xFF; + if ((fifthByte & 0xF0) != 0) { + throwMalformedVarUInt32(fifthByte); + } + value |= fifthByte << 28; + } + } + readerIndex = readIdx; + return value; + } + + /** Reads the 1-9 byte int part of a var long. */ + public long readVarInt64() { + return LITTLE_ENDIAN ? _readVarInt64OnLE() : _readVarInt64OnBE(); + } + + @CodegenInvoke + // CHECKSTYLE.OFF:MethodName + public long _readVarInt64OnLE() { + // CHECKSTYLE.ON:MethodName + // Duplicate and manual inline for performance. + // noinspection Duplicates + int readIdx = readerIndex; + long result; + if (size - readIdx < 9) { + result = readVarUInt64Slow(); + } else { + long address = this.address; + long bulkValue = loadLong(address + readIdx); + // Duplicate and manual inline for performance. + // noinspection Duplicates + readIdx++; + result = bulkValue & 0x7F; + if ((bulkValue & 0x80) != 0) { + readIdx++; + // 0x3f80: 0b1111111 << 7 + result |= (bulkValue >>> 1) & 0x3f80; + // 0x8000: 0b1 << 15 + if ((bulkValue & 0x8000) != 0) { + result = continueReadVarInt64(readIdx, bulkValue, result); + return ((result >>> 1) ^ -(result & 1)); + } + } + readerIndex = readIdx; + } + return ((result >>> 1) ^ -(result & 1)); + } + + @CodegenInvoke + // CHECKSTYLE.OFF:MethodName + public long _readVarInt64OnBE() { + // CHECKSTYLE.ON:MethodName + int readIdx = readerIndex; + long result; + if (size - readIdx < 9) { + result = readVarUInt64Slow(); + } else { + long address = this.address; + long bulkValue = Long.reverseBytes(loadLong(address + readIdx)); + // Duplicate and manual inline for performance. + // noinspection Duplicates + readIdx++; + result = bulkValue & 0x7F; + if ((bulkValue & 0x80) != 0) { + readIdx++; + // 0x3f80: 0b1111111 << 7 + result |= (bulkValue >>> 1) & 0x3f80; + // 0x8000: 0b1 << 15 + if ((bulkValue & 0x8000) != 0) { + result = continueReadVarInt64(readIdx, bulkValue, result); + return ((result >>> 1) ^ -(result & 1)); + } + } + readerIndex = readIdx; + } + return ((result >>> 1) ^ -(result & 1)); + } + + /** Reads the 1-9 byte int part of a non-negative var long. */ + public long readVarUInt64() { + int readIdx = readerIndex; + if (size - readIdx < 9) { + return readVarUInt64Slow(); + } + // varint are written using little endian byte order, so read by little endian byte order. + long bulkValue = loadLong(address + readIdx); + if (!LITTLE_ENDIAN) { + bulkValue = Long.reverseBytes(bulkValue); + } + // Duplicate and manual inline for performance. + // noinspection Duplicates + readIdx++; + long result = bulkValue & 0x7F; + if ((bulkValue & 0x80) != 0) { + readIdx++; + // 0x3f80: 0b1111111 << 7 + result |= (bulkValue >>> 1) & 0x3f80; + // 0x8000: 0b1 << 15 + if ((bulkValue & 0x8000) != 0) { + return continueReadVarInt64(readIdx, bulkValue, result); + } + } + readerIndex = readIdx; + return result; + } + + private long continueReadVarInt64(int readIdx, long bulkValue, long result) { + readIdx++; + // 0x1fc000: 0b1111111 << 14 + result |= (bulkValue >>> 2) & 0x1fc000; + // 0x800000: 0b1 << 23 + if ((bulkValue & 0x800000) != 0) { + readIdx++; + // 0xfe00000: 0b1111111 << 21 + result |= (bulkValue >>> 3) & 0xfe00000; + if ((bulkValue & 0x80000000L) != 0) { + readIdx++; + result |= (bulkValue >>> 4) & 0x7f0000000L; + if ((bulkValue & 0x8000000000L) != 0) { + readIdx++; + result |= (bulkValue >>> 5) & 0x3f800000000L; + if ((bulkValue & 0x800000000000L) != 0) { + readIdx++; + result |= (bulkValue >>> 6) & 0x1fc0000000000L; + if ((bulkValue & 0x80000000000000L) != 0) { + readIdx++; + result |= (bulkValue >>> 7) & 0xfe000000000000L; + if ((bulkValue & 0x8000000000000000L) != 0) { + long b = loadByte(address + readIdx++); + result |= b << 56; + } + } + } + } + } + } + readerIndex = readIdx; + return result; + } + + private long readVarUInt64Slow() { + long b = readByte(); + long result = b & 0x7F; + // Note: + // Loop are not used here to improve performance. + // We manually unroll the loop for better performance. + if ((b & 0x80) != 0) { + b = readByte(); + result |= (b & 0x7F) << 7; + if ((b & 0x80) != 0) { + b = readByte(); + result |= (b & 0x7F) << 14; + if ((b & 0x80) != 0) { + b = readByte(); + result |= (b & 0x7F) << 21; + if ((b & 0x80) != 0) { + b = readByte(); + result |= (b & 0x7F) << 28; + if ((b & 0x80) != 0) { + b = readByte(); + result |= (b & 0x7F) << 35; + if ((b & 0x80) != 0) { + b = readByte(); + result |= (b & 0x7F) << 42; + if ((b & 0x80) != 0) { + b = readByte(); + result |= (b & 0x7F) << 49; + if ((b & 0x80) != 0) { + b = readByte(); + // highest bit in last byte is symbols bit. + result |= b << 56; + } + } + } + } + } + } + } + } + return result; + } + + /** Reads the 1-9 byte int part of an aligned varint. */ + public int readAlignedVarUInt32() { + int readerIdx = readerIndex; + // use subtract to avoid overflow + if (readerIdx < size - 10) { + return slowReadAlignedVarUInt32(); + } + long pos = address + readerIdx; + long startPos = pos; + int b = loadByte(pos++); + // Mask first 6 bits, + // bit 8 `set` indicates have next data bytes. + int result = b & 0x3F; + // Note: + // Loop are not used here to improve performance. + // We manually unroll the loop for better performance. + if ((b & 0x80) != 0) { // has 2nd byte + b = loadByte(pos++); + result |= (b & 0x3F) << 6; + if ((b & 0x80) != 0) { // has 3rd byte + b = loadByte(pos++); + result |= (b & 0x3F) << 12; + if ((b & 0x80) != 0) { // has 4th byte + b = loadByte(pos++); + result |= (b & 0x3F) << 18; + if ((b & 0x80) != 0) { // has 5th byte + b = loadByte(pos++); + result |= (b & 0x3F) << 24; + if ((b & 0x80) != 0) { // has 6th byte + b = loadByte(pos++); + result |= (b & 0x3F) << 30; + } + } + } + } + } + pos = skipPadding(pos, b); // split method for `readVarUint` inlined + readerIndex = (int) (pos - startPos + readerIdx); + return result; + } + + public int slowReadAlignedVarUInt32() { + int b = readByte(); + // Mask first 6 bits, + // bit 8 `set` indicates have next data bytes. + int result = b & 0x3F; + if ((b & 0x80) != 0) { // has 2nd byte + b = readByte(); + result |= (b & 0x3F) << 6; + if ((b & 0x80) != 0) { // has 3rd byte + b = readByte(); + result |= (b & 0x3F) << 12; + if ((b & 0x80) != 0) { // has 4th byte + b = readByte(); + result |= (b & 0x3F) << 18; + if ((b & 0x80) != 0) { // has 5th byte + b = readByte(); + result |= (b & 0x3F) << 24; + if ((b & 0x80) != 0) { // has 6th byte + b = readByte(); + result |= (b & 0x3F) << 30; + } + } + } + } + } + // bit 7 `unset` indicates have next padding bytes, + if ((b & 0x40) == 0) { // has first padding bytes + b = readByte(); + if ((b & 0x40) == 0) { // has 2nd padding bytes + b = readByte(); + if ((b & 0x40) == 0) { // has 3rd padding bytes + b = readByte(); + checkArgument((b & 0x40) != 0, "At most 3 padding bytes."); + } + } + } + return result; + } + + private long skipPadding(long pos, int b) { + // bit 7 `unset` indicates have next padding bytes, + if ((b & 0x40) == 0) { // has first padding bytes + b = loadByte(pos++); + if ((b & 0x40) == 0) { // has 2nd padding bytes + b = loadByte(pos++); + if ((b & 0x40) == 0) { // has 3rd padding bytes + b = loadByte(pos++); + checkArgument((b & 0x40) != 0, "At most 3 padding bytes."); + } + } + } + return pos; + } + + public byte[] readBytes(int length) { + int readerIdx = readerIndex; + byte[] bytes = new byte[length]; + // use subtract to avoid overflow + if (length > size - readerIdx) { + streamReader.readTo(bytes, 0, length); + return bytes; + } + readBytesToArray(address + readerIdx, bytes, BYTE_ARRAY_OFFSET, length); + readerIndex = readerIdx + length; + return bytes; + } + + public void readBytes(byte[] dst, int dstIndex, int length) { + int readerIdx = readerIndex; + // use subtract to avoid overflow + if (readerIdx > size - length) { + streamReader.readTo(dst, dstIndex, length); + return; + } + if (dstIndex < 0 || dstIndex > dst.length - length) { + throwIndexOOBExceptionForRead(); + } + get(readerIdx, dst, dstIndex, length); + readerIndex = readerIdx + length; + } + + public void readBytes(byte[] dst) { + readBytes(dst, 0, dst.length); + } + + /** Read {@code len} bytes into a long using little-endian order. */ + public long readBytesAsInt64(int len) { + int readerIdx = readerIndex; + // use subtract to avoid overflow + int remaining = size - readerIdx; + if (remaining >= 8) { + readerIndex = readerIdx + len; + long v = loadLong(address + readerIdx); + v = (LITTLE_ENDIAN ? v : Long.reverseBytes(v)) & (0xffffffffffffffffL >>> ((8 - len) * 8)); + return v; + } + return slowReadBytesAsInt64(remaining, len); + } + + private long slowReadBytesAsInt64(int remaining, int len) { + if (remaining < len) { + streamReader.fillBuffer(len - remaining); + } + int readerIdx = readerIndex; + readerIndex = readerIdx + len; + long result = 0; + byte[] heapMemory = this.heapMemory; + if (heapMemory != null) { + for (int i = 0, start = heapOffset + readerIdx; i < len; i++) { + result |= (((long) heapMemory[start + i]) & 0xff) << (i * 8); + } + } else { + long start = address + readerIdx; + for (int i = 0; i < len; i++) { + result |= ((long) loadByte(start + i) & 0xff) << (i * 8); + } + } + return result; + } + + public int read(ByteBuffer dst) { + int readerIdx = readerIndex; + int len = dst.remaining(); + // use subtract to avoid overflow + if (readerIdx > size - len) { + return streamReader.readToByteBuffer(dst); + } + if (heapMemory != null) { + dst.put(heapMemory, readerIndex + heapOffset, len); + } else { + dst.put(sliceAsByteBuffer(readerIdx, len)); + } + readerIndex = readerIdx + len; + return len; + } + + public void read(ByteBuffer dst, int len) { + int readerIdx = readerIndex; + // use subtract to avoid overflow + if (readerIdx > size - len) { + streamReader.readToByteBuffer(dst, len); + } else { + if (heapMemory != null) { + dst.put(heapMemory, readerIndex + heapOffset, len); + } else { + dst.put(sliceAsByteBuffer(readerIdx, len)); + } + readerIndex = readerIdx + len; + } + } + + /** + * Read size for following binary, this method will check and fill readable bytes too. This method + * is optimized for small size, it's faster than {@link #readVarUInt32}. + */ + public int readBinarySize() { + int binarySize; + int readIdx = readerIndex; + if (size - readIdx >= 5) { + // Android exits above. Keep this small-size fast path as a raw JVM load; `_unsafeGetInt32` + // carries Android/endian branches and can grow the method enough to disturb inlining. + int fourByteValue = loadInt(address + readIdx++); + if (!LITTLE_ENDIAN) { + fourByteValue = Integer.reverseBytes(fourByteValue); + } + binarySize = fourByteValue & 0x7F; + // Duplicate and manual inline for performance. + // noinspection Duplicates + if ((fourByteValue & 0x80) != 0) { + readIdx++; + binarySize |= (fourByteValue >>> 1) & 0x3f80; + if ((fourByteValue & 0x8000) != 0) { + // merely executed path, make it as a separate method to reduce + // code size of current method for better jvm inline + return continueReadBinarySize(readIdx, fourByteValue, binarySize); + } + } + readerIndex = readIdx; + } else { + binarySize = readVarUInt32Slow(); + readIdx = readerIndex; + } + int diff = size - readIdx; + if (diff < binarySize) { + streamReader.fillBuffer(diff); + } + return binarySize; + } + + private int continueReadBinarySize(int readIdx, int bulkRead, int binarySize) { + // Duplicate and manual inline for performance. + // noinspection Duplicates + readIdx++; + binarySize |= (bulkRead >>> 2) & 0x1fc000; + if ((bulkRead & 0x800000) != 0) { + readIdx++; + binarySize |= (bulkRead >>> 3) & 0xfe00000; + if ((bulkRead & 0x80000000) != 0) { + int fifthByte = loadByte(address + readIdx++) & 0xFF; + if ((fifthByte & 0xF0) != 0) { + throwMalformedVarUInt32(fifthByte); + } + binarySize |= fifthByte << 28; + } + } + int diff = size - readIdx; + if (diff < binarySize) { + streamReader.fillBuffer(diff); + } + return binarySize; + } + + public byte[] readBytesAndSize() { + final int numBytes = readBinarySize(); + int readerIdx = readerIndex; + final byte[] arr = new byte[numBytes]; + // use subtract to avoid overflow + if (readerIdx > size - numBytes) { + streamReader.readTo(arr, 0, numBytes); + return arr; + } + readBytesToArray(address + readerIdx, arr, BYTE_ARRAY_OFFSET, numBytes); + readerIndex = readerIdx + numBytes; + return arr; + } + + /** + * Reads a size-validated primitive byte-array payload into {@code values}. The caller owns size + * validation and destination allocation; this method reads payload bytes only, not the size + * prefix. + */ + public void readByteArrayPayload(byte[] values, int numBytes) { + int readerIdx = readerIndex; + if (readerIdx > size - numBytes) { + streamReader.readTo(values, 0, numBytes); + return; + } + readBytesToArray(address + readerIdx, values, BYTE_ARRAY_OFFSET, numBytes); + readerIndex = readerIdx + numBytes; + } + + /** + * Reads a size-validated primitive boolean-array payload into {@code values}. The caller owns + * size validation and destination allocation; this method reads payload bytes only, not the size + * prefix. + */ + public void readBooleanArrayPayload(boolean[] values, int numBytes) { + int readerIdx = readerIndex; + if (readerIdx > size - numBytes) { + streamReader.readBooleans(values, 0, numBytes); + return; + } + readBooleansToArray(address + readerIdx, values, BOOLEAN_ARRAY_OFFSET, numBytes); + readerIndex = readerIdx + numBytes; + } + + /** + * Reads a size-validated primitive char-array payload into {@code values}. The caller owns size + * validation and destination allocation; this method reads payload bytes only, not the size + * prefix. + */ + public void readCharArrayPayload(char[] values, int numBytes) { + int readerIdx = readerIndex; + if (readerIdx > size - numBytes) { + streamReader.readChars(values, 0, numBytes >>> 1); + return; + } + readCharsToArray(address + readerIdx, values, CHAR_ARRAY_OFFSET, numBytes); + readerIndex = readerIdx + numBytes; + } + + /** + * Reads a size-validated primitive int16-array payload into {@code values}. The caller owns size + * validation and destination allocation; this method reads payload bytes only, not the size + * prefix. + */ + public void readInt16ArrayPayload(short[] values, int numBytes) { + int readerIdx = readerIndex; + if (readerIdx > size - numBytes) { + streamReader.readShorts(values, 0, numBytes >>> 1); + return; + } + readShortsToArray(address + readerIdx, values, SHORT_ARRAY_OFFSET, numBytes); + readerIndex = readerIdx + numBytes; + } + + /** + * Reads a size-validated primitive int32-array payload into {@code values}. The caller owns size + * validation and destination allocation; this method reads payload bytes only, not the size + * prefix. + */ + public void readInt32ArrayPayload(int[] values, int numBytes) { + int readerIdx = readerIndex; + if (readerIdx > size - numBytes) { + streamReader.readInts(values, 0, numBytes >>> 2); + return; + } + readIntsToArray(address + readerIdx, values, INT_ARRAY_OFFSET, numBytes); + readerIndex = readerIdx + numBytes; + } + + /** + * Reads a size-validated primitive int64-array payload into {@code values}. The caller owns size + * validation and destination allocation; this method reads payload bytes only, not the size + * prefix. + */ + public void readInt64ArrayPayload(long[] values, int numBytes) { + int readerIdx = readerIndex; + if (readerIdx > size - numBytes) { + streamReader.readLongs(values, 0, numBytes >>> 3); + return; + } + readLongsToArray(address + readerIdx, values, LONG_ARRAY_OFFSET, numBytes); + readerIndex = readerIdx + numBytes; + } + + /** + * Reads a size-validated primitive float32-array payload into {@code values}. The caller owns + * size validation and destination allocation; this method reads payload bytes only, not the size + * prefix. + */ + public void readFloat32ArrayPayload(float[] values, int numBytes) { + int readerIdx = readerIndex; + if (readerIdx > size - numBytes) { + streamReader.readFloats(values, 0, numBytes >>> 2); + return; + } + readFloatsToArray(address + readerIdx, values, FLOAT_ARRAY_OFFSET, numBytes); + readerIndex = readerIdx + numBytes; + } + + /** + * Reads a size-validated primitive float64-array payload into {@code values}. The caller owns + * size validation and destination allocation; this method reads payload bytes only, not the size + * prefix. + */ + public void readFloat64ArrayPayload(double[] values, int numBytes) { + int readerIdx = readerIndex; + if (readerIdx > size - numBytes) { + streamReader.readDoubles(values, 0, numBytes >>> 3); + return; + } + readDoublesToArray(address + readerIdx, values, DOUBLE_ARRAY_OFFSET, numBytes); + readerIndex = readerIdx + numBytes; + } + + public void readBooleans(boolean[] values, int offset, int numElements) { + if ((offset | numElements | (offset + numElements) | (values.length - offset - numElements)) + < 0) { + throwOOBException(); + } + if (readerIndex > size - numElements) { + streamReader.readBooleans(values, offset, numElements); + return; + } + int readerIdx = readerIndex; + readBooleansToArray(address + readerIdx, values, BOOLEAN_ARRAY_OFFSET + offset, numElements); + readerIndex = readerIdx + numElements; + } + + public void readChars(char[] chars, int numElements) { + readChars(chars, 0, numElements); + } + + public void readChars(char[] chars, int offset, int numElements) { + if ((offset | numElements | (offset + numElements) | (chars.length - offset - numElements)) + < 0) { + throwOOBException(); + } + int numBytes = Math.multiplyExact(numElements, 2); + if (readerIndex > size - numBytes) { + streamReader.readChars(chars, offset, numElements); + return; + } + int readerIdx = readerIndex; + readCharsToArray(address + readerIdx, chars, CHAR_ARRAY_OFFSET + offset, numBytes); + readerIndex = readerIdx + numBytes; + } + + @CodegenInvoke + public char[] readCharsAndSize() { + final int numBytes = readBinarySize(); + int numElements = numBytes >> 1; + char[] values = new char[numElements]; + readChars(values, 0, numElements); + return values; + } + + public void readShorts(short[] values, int offset, int numElements) { + if ((offset | numElements | (offset + numElements) | (values.length - offset - numElements)) + < 0) { + throwOOBException(); + } + int numBytes = Math.multiplyExact(numElements, 2); + if (readerIndex > size - numBytes) { + streamReader.readShorts(values, offset, numElements); + return; + } + int readerIdx = readerIndex; + readShortsToArray(address + readerIdx, values, SHORT_ARRAY_OFFSET + offset, numBytes); + readerIndex = readerIdx + numBytes; + } + + public void readInts(int[] values, int offset, int numElements) { + if ((offset | numElements | (offset + numElements) | (values.length - offset - numElements)) + < 0) { + throwOOBException(); + } + int numBytes = Math.multiplyExact(numElements, 4); + if (readerIndex > size - numBytes) { + streamReader.readInts(values, offset, numElements); + return; + } + int readerIdx = readerIndex; + readIntsToArray(address + readerIdx, values, INT_ARRAY_OFFSET + offset, numBytes); + readerIndex = readerIdx + numBytes; + } + + public void readLongs(long[] values, int offset, int numElements) { + if ((offset | numElements | (offset + numElements) | (values.length - offset - numElements)) + < 0) { + throwOOBException(); + } + int numBytes = Math.multiplyExact(numElements, 8); + if (readerIndex > size - numBytes) { + streamReader.readLongs(values, offset, numElements); + return; + } + int readerIdx = readerIndex; + readLongsToArray(address + readerIdx, values, LONG_ARRAY_OFFSET + offset, numBytes); + readerIndex = readerIdx + numBytes; + } + + public void readFloats(float[] values, int offset, int numElements) { + if ((offset | numElements | (offset + numElements) | (values.length - offset - numElements)) + < 0) { + throwOOBException(); + } + int numBytes = Math.multiplyExact(numElements, 4); + if (readerIndex > size - numBytes) { + streamReader.readFloats(values, offset, numElements); + return; + } + int readerIdx = readerIndex; + readFloatsToArray(address + readerIdx, values, FLOAT_ARRAY_OFFSET + offset, numBytes); + readerIndex = readerIdx + numBytes; + } + + public void readDoubles(double[] values, int offset, int numElements) { + if ((offset | numElements | (offset + numElements) | (values.length - offset - numElements)) + < 0) { + throwOOBException(); + } + int numBytes = Math.multiplyExact(numElements, 8); + if (readerIndex > size - numBytes) { + streamReader.readDoubles(values, offset, numElements); + return; + } + int readerIdx = readerIndex; + readDoublesToArray(address + readerIdx, values, DOUBLE_ARRAY_OFFSET + offset, numBytes); + readerIndex = readerIdx + numBytes; + } + + public void checkReadableBytes(int minimumReadableBytes) { + // use subtract to avoid overflow + int remaining = size - readerIndex; + if (minimumReadableBytes > remaining) { + streamReader.fillBuffer(minimumReadableBytes - remaining); + } + } + + /** + * Returns internal byte array if data is on heap and remaining buffer size is equal to internal + * byte array size, or create a new byte array which copy remaining data from off-heap. + */ + public byte[] getRemainingBytes() { + int length = size - readerIndex; + if (heapMemory != null && size == length && heapOffset == 0) { + return heapMemory; + } else { + return getBytes(readerIndex, length); + } + } + + // ------------------------- Read Methods Finished ------------------------------------- + + public void copyTo(int offset, MemoryBuffer target, int targetOffset, int numBytes) { + final byte[] thisHeapRef = this.heapMemory; + final byte[] otherHeapRef = target.heapMemory; + final long thisPointer = this.address + offset; + final long otherPointer = target.address + targetOffset; + if ((numBytes | offset | targetOffset) >= 0 + && thisPointer <= this.addressLimit - numBytes + && otherPointer <= target.addressLimit - numBytes) { + if (thisHeapRef != null && otherHeapRef != null) { + System.arraycopy( + thisHeapRef, toIntIndex(thisPointer), otherHeapRef, toIntIndex(otherPointer), numBytes); + } else if (sameBufferOverlap(target, offset, targetOffset, numBytes)) { + byte[] tmp = new byte[numBytes]; + sliceAsByteBuffer(offset, numBytes).get(tmp); + target.sliceAsByteBuffer(targetOffset, numBytes).put(tmp); + } else { + target.sliceAsByteBuffer(targetOffset, numBytes).put(sliceAsByteBuffer(offset, numBytes)); + } + } else { + throw new IndexOutOfBoundsException( + String.format( + "offset=%d, targetOffset=%d, numBytes=%d, address=%d, targetAddress=%d", + offset, targetOffset, numBytes, this.address, target.address)); + } + } + + private boolean sameBufferOverlap( + MemoryBuffer target, int offset, int targetOffset, int numBytes) { + if (numBytes <= 0) { + return false; + } + if (heapMemory != null && heapMemory == target.heapMemory) { + int sourceStart = heapOffset + offset; + int targetStart = target.heapOffset + targetOffset; + return sourceStart < targetStart + numBytes && targetStart < sourceStart + numBytes; + } + if (offHeapBuffer != null && offHeapBuffer == target.offHeapBuffer) { + long sourceStart = address + offset; + long targetStart = target.address + targetOffset; + return sourceStart < targetStart + numBytes && targetStart < sourceStart + numBytes; + } + return false; + } + + public void copyFrom(int offset, MemoryBuffer source, int sourcePointer, int numBytes) { + source.copyTo(sourcePointer, this, offset, numBytes); + } + + public void copyToByteArray(int offset, byte[] target, int targetOffset, int numBytes) { + checkArrayCopy(offset, targetOffset, target.length, numBytes, 0); + readBytesToArray(address + offset, target, BYTE_ARRAY_OFFSET + targetOffset, numBytes); + } + + public void copyToBooleanArray(int offset, boolean[] target, int targetOffset, int numBytes) { + checkArrayCopy(offset, targetOffset, target.length, numBytes, 0); + readBooleansToArray(address + offset, target, BOOLEAN_ARRAY_OFFSET + targetOffset, numBytes); + } + + public void copyToCharArray(int offset, char[] target, int targetOffset, int numBytes) { + checkArrayCopy(offset, targetOffset, target.length, numBytes, 1); + readCharsToArray(address + offset, target, CHAR_ARRAY_OFFSET + targetOffset, numBytes); + } + + public void copyToShortArray(int offset, short[] target, int targetOffset, int numBytes) { + checkArrayCopy(offset, targetOffset, target.length, numBytes, 1); + readShortsToArray(address + offset, target, SHORT_ARRAY_OFFSET + targetOffset, numBytes); + } + + public void copyToIntArray(int offset, int[] target, int targetOffset, int numBytes) { + checkArrayCopy(offset, targetOffset, target.length, numBytes, 2); + readIntsToArray(address + offset, target, INT_ARRAY_OFFSET + targetOffset, numBytes); + } + + public void copyToLongArray(int offset, long[] target, int targetOffset, int numBytes) { + checkArrayCopy(offset, targetOffset, target.length, numBytes, 3); + readLongsToArray(address + offset, target, LONG_ARRAY_OFFSET + targetOffset, numBytes); + } + + public void copyToFloatArray(int offset, float[] target, int targetOffset, int numBytes) { + checkArrayCopy(offset, targetOffset, target.length, numBytes, 2); + readFloatsToArray(address + offset, target, FLOAT_ARRAY_OFFSET + targetOffset, numBytes); + } + + public void copyToDoubleArray(int offset, double[] target, int targetOffset, int numBytes) { + checkArrayCopy(offset, targetOffset, target.length, numBytes, 3); + readDoublesToArray(address + offset, target, DOUBLE_ARRAY_OFFSET + targetOffset, 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(); + } + } + + public void copyFromByteArray(int offset, byte[] source, int sourceOffset, int numBytes) { + checkArrayCopy(offset, sourceOffset, source.length, numBytes, 0); + writeBytesFromArray(address + offset, source, BYTE_ARRAY_OFFSET + sourceOffset, numBytes); + } + + public void copyFromBooleanArray(int offset, boolean[] source, int sourceOffset, int numBytes) { + checkArrayCopy(offset, sourceOffset, source.length, numBytes, 0); + writeBooleansFromArray(address + offset, source, BOOLEAN_ARRAY_OFFSET + sourceOffset, numBytes); + } + + public void copyFromCharArray(int offset, char[] source, int sourceOffset, int numBytes) { + checkArrayCopy(offset, sourceOffset, source.length, numBytes, 1); + writeCharsFromArray(address + offset, source, CHAR_ARRAY_OFFSET + sourceOffset, numBytes); + } + + public void copyFromShortArray(int offset, short[] source, int sourceOffset, int numBytes) { + checkArrayCopy(offset, sourceOffset, source.length, numBytes, 1); + writeShortsFromArray(address + offset, source, SHORT_ARRAY_OFFSET + sourceOffset, numBytes); + } + + public void copyFromIntArray(int offset, int[] source, int sourceOffset, int numBytes) { + checkArrayCopy(offset, sourceOffset, source.length, numBytes, 2); + writeIntsFromArray(address + offset, source, INT_ARRAY_OFFSET + sourceOffset, numBytes); + } + + public void copyFromLongArray(int offset, long[] source, int sourceOffset, int numBytes) { + checkArrayCopy(offset, sourceOffset, source.length, numBytes, 3); + writeLongsFromArray(address + offset, source, LONG_ARRAY_OFFSET + sourceOffset, numBytes); + } + + public void copyFromFloatArray(int offset, float[] source, int sourceOffset, int numBytes) { + checkArrayCopy(offset, sourceOffset, source.length, numBytes, 2); + writeFloatsFromArray(address + offset, source, FLOAT_ARRAY_OFFSET + sourceOffset, numBytes); + } + + public void copyFromDoubleArray(int offset, double[] source, int sourceOffset, int numBytes) { + checkArrayCopy(offset, sourceOffset, source.length, numBytes, 3); + writeDoublesFromArray(address + offset, source, DOUBLE_ARRAY_OFFSET + sourceOffset, numBytes); + } + + public byte[] getBytes(int index, int length) { + if (index == 0 && heapMemory != null && heapOffset == 0) { + // Arrays.copyOf is an intrinsics, which is faster + return Arrays.copyOf(heapMemory, length); + } + if (index + length > size) { + throwIndexOOBExceptionForRead(length); + } + byte[] data = new byte[length]; + get(index, data, 0, length); + return data; + } + + public void getBytes(int index, byte[] dst, int dstIndex, int length) { + if (dstIndex > dst.length - length) { + throwOOBException(); + } + if (index > size - length) { + throwOOBException(); + } + get(index, dst, dstIndex, length); + } + + public MemoryBuffer slice(int offset) { + return slice(offset, size - offset); + } + + public MemoryBuffer slice(int offset, int length) { + if (offset + length > size) { + throwOOBExceptionForRange(offset, length); + } + if (heapMemory != null) { + return new MemoryBuffer(heapMemory, heapOffset + offset, length); + } else { + return new MemoryBuffer(address + offset, length, offHeapBuffer); + } + } + + public ByteBuffer sliceAsByteBuffer() { + return sliceAsByteBuffer(readerIndex, size - readerIndex); + } + + public ByteBuffer sliceAsByteBuffer(int offset, int length) { + if (offset + length > size) { + throwOOBExceptionForRange(offset, length); + } + if (heapMemory != null) { + return ByteBuffer.wrap(heapMemory, heapOffset + offset, length).slice(); + } else { + ByteBuffer offHeapBuffer = this.offHeapBuffer; + if (offHeapBuffer != null) { + ByteBuffer duplicate = offHeapBuffer.duplicate(); + int start = (int) address; + duplicate.clear(); + ByteBufferUtil.position(duplicate, start + offset); + duplicate.limit(start + offset + length); + return duplicate.slice(); + } else { + throw new IllegalStateException("Memory buffer does not own a ByteBuffer"); + } + } + } + + private void throwOOBExceptionForRange(int offset, int length) { + throw new IndexOutOfBoundsException( + String.format("offset(%d) + length(%d) exceeds size(%d): %s", offset, length, size, this)); + } + + public ForyStreamReader getStreamReader() { + return streamReader; + } + + /** + * Equals two memory buffer regions. + * + * @param buf2 Buffer to equal this buffer with + * @param offset1 Offset of this buffer to start equaling + * @param offset2 Offset of buf2 to start equaling + * @param len Length of the equaled memory region + * @return true if buffers equal or len zero, false otherwise + */ + public boolean equalTo(MemoryBuffer buf2, int offset1, int offset2, int len) { + if (len == 0) { + return buf2 != null; + } + + final long pos1 = address + offset1; + final long pos2 = buf2.address + offset2; + checkArgument(pos1 < addressLimit); + checkArgument(pos2 < buf2.addressLimit); + return unsafeEqualTo(this, heapMemory, pos1, buf2, buf2.heapMemory, pos2, len); + } + + /** + * Equals a memory buffer region with a byte array region. + * + * @param bytes Array to compare with + * @param bytesOffset Offset of bytes to start comparing + * @param offset Offset of this buffer to start comparing + * @param len Length of the compared memory region + * @return true if regions are equal or len zero, false otherwise + */ + public boolean equalTo(byte[] bytes, int bytesOffset, int offset, int len) { + checkArgument(bytes != null); + checkArgument(len >= 0); + checkArgument(bytesOffset >= 0 && bytesOffset <= bytes.length - len); + checkArgument(offset >= 0 && offset <= size - len); + if (len == 0) { + return true; + } + + final long pos = address + offset; + return unsafeEqualTo(this, heapMemory, pos, this, bytes, BYTE_ARRAY_OFFSET + bytesOffset, len); + } + + private static boolean unsafeEqualTo( + MemoryBuffer left, + Object leftBase, + long leftOffset, + MemoryBuffer right, + Object rightBase, + long rightOffset, + int length) { + int i = 0; + if ((leftOffset % 8) == (rightOffset % 8)) { + while ((leftOffset + i) % 8 != 0 && i < length) { + if (left.rawByte(leftBase, leftOffset + i) != right.rawByte(rightBase, rightOffset + i)) { + return false; + } + i += 1; + } + } + if (UNALIGNED || (((leftOffset + i) % 8 == 0) && ((rightOffset + i) % 8 == 0))) { + while (i <= length - 8) { + if (left.rawLong(leftBase, leftOffset + i) != right.rawLong(rightBase, rightOffset + i)) { + return false; + } + i += 8; + } + } + while (i < length) { + if (left.rawByte(leftBase, leftOffset + i) != right.rawByte(rightBase, rightOffset + i)) { + return false; + } + i += 1; + } + return true; + } + + private byte rawByte(Object base, long offset) { + if (base == null) { + return loadByte(offset); + } + return ((byte[]) base)[toIntIndex(offset)]; + } + + private long rawLong(Object base, long offset) { + if (base == null) { + return loadLong(offset); + } + return (long) BYTE_ARRAY_LONG.get((byte[]) base, toIntIndex(offset)); + } + + @Override + public String toString() { + return "MemoryBuffer{" + + "size=" + + size + + ", readerIndex=" + + readerIndex + + ", writerIndex=" + + writerIndex + + ", heapMemory=" + + (heapMemory == null ? null : "len(" + heapMemory.length + ")") + + ", heapOffset=" + + heapOffset + + ", offHeapBuffer=" + + offHeapBuffer + + ", address=" + + address + + ", addressLimit=" + + addressLimit + + '}'; + } + + // ------------------------------------------------------------------------ + // Memory Allocator Support + // ------------------------------------------------------------------------ + + /** Default memory allocator that uses the original heap-based allocation strategy. */ + private static final class DefaultMemoryAllocator implements MemoryAllocator { + @Override + public MemoryBuffer allocate(int initialSize) { + return fromByteArray(new byte[initialSize]); + } + + @Override + public void grow(MemoryBuffer buffer, int newCapacity) { + if (newCapacity <= buffer.size()) { + return; + } + + int newSize = + newCapacity < BUFFER_GROW_STEP_THRESHOLD + ? newCapacity << 1 + : (int) Math.min(newCapacity * 1.5d, Integer.MAX_VALUE - 8); + + byte[] data = new byte[newSize]; + buffer.get(0, data, 0, buffer.size()); + buffer.initHeapBuffer(data, 0, data.length); + } + } + + /** + * Sets the global memory allocator. This affects all new MemoryBuffer allocations and growth + * operations. + * + * @param allocator the new global allocator to use + * @throws NullPointerException if allocator is null + */ + public static void setGlobalAllocator(MemoryAllocator allocator) { + if (allocator == null) { + throw new NullPointerException("Memory allocator cannot be null"); + } + globalAllocator = allocator; + } + + /** + * Gets the current global memory allocator. + * + * @return the current global allocator + */ + public static MemoryAllocator getGlobalAllocator() { + return globalAllocator; + } + + /** Point this buffer to a new byte array. */ + public void pointTo(byte[] buffer, int offset, int length) { + initHeapBuffer(buffer, offset, length); + } + + /** Creates a new memory buffer that targets to the given heap memory region. */ + public static MemoryBuffer fromByteArray(byte[] buffer, int offset, int length) { + return new MemoryBuffer(buffer, offset, length, null); + } + + public static MemoryBuffer fromByteArray( + byte[] buffer, int offset, int length, ForyStreamReader streamReader) { + return new MemoryBuffer(buffer, offset, length, streamReader); + } + + /** Creates a new memory buffer that targets to the given heap memory region. */ + public static MemoryBuffer fromByteArray(byte[] buffer) { + return new MemoryBuffer(buffer, 0, buffer.length); + } + + /** + * Creates a new memory buffer that represents the memory backing the given byte buffer section of + * {@code [buffer.position(), buffer.limit())}. The buffer will change into a heap buffer + * automatically if not enough. + * + * @param buffer a direct buffer or heap buffer + */ + public static MemoryBuffer fromByteBuffer(ByteBuffer buffer) { + if (buffer.isDirect()) { + return new MemoryBuffer(buffer.position(), buffer.remaining(), buffer); + } else if (buffer.hasArray()) { + int offset = buffer.arrayOffset() + buffer.position(); + return new MemoryBuffer(buffer.array(), offset, buffer.remaining()); + } else { + ByteBuffer duplicate = buffer.duplicate(); + byte[] bytes = new byte[duplicate.remaining()]; + duplicate.get(bytes); + return fromByteArray(bytes); + } + } + + public static MemoryBuffer fromDirectByteBuffer( + ByteBuffer buffer, int size, ForyStreamReader streamReader) { + long offHeapAddress = buffer.position(); + return new MemoryBuffer(offHeapAddress, size, buffer, streamReader); + } + + /** + * Create a heap buffer of specified initial size. The buffer will grow automatically if not + * enough. + */ + public static MemoryBuffer newHeapBuffer(int initialSize) { + return globalAllocator.allocate(initialSize); + } +} diff --git a/java/fory-core/src/main/java25/org/apache/fory/platform/internal/_Lookup.java b/java/fory-core/src/main/java25/org/apache/fory/platform/internal/_Lookup.java new file mode 100644 index 0000000000..e1c5878e9b --- /dev/null +++ b/java/fory-core/src/main/java25/org/apache/fory/platform/internal/_Lookup.java @@ -0,0 +1,113 @@ +/* + * 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.MethodType; +import java.lang.invoke.MethodHandles.Lookup; +import java.lang.reflect.Field; + +// CHECKSTYLE.OFF:TypeName +class _Lookup { + // CHECKSTYLE.ON:TypeName + private static volatile Lookup implLookup; + private static volatile MethodHandle constructorLookup; + + // CHECKSTYLE.OFF:MethodName + public static Lookup _trustedLookup(Class objectClass) { + // CHECKSTYLE.ON:MethodName + try { + // IMPL_LOOKUP.in(type) drops trusted modes. JDK26 final-field writes under + // --illegal-final-field-mutation=deny require a target-class /trusted lookup. + return (Lookup) constructorLookup().invoke(objectClass, null, -1); + } catch (Throwable e) { + throw new IllegalStateException(trustedLookupMessage(), e); + } + } + + public static Lookup privateLookupIn(Class targetClass, Lookup caller) { + return _trustedLookup(targetClass); + } + + /** + * Creates and links a class or interface from {@code bytes} with the same class loader and in the + * same runtime package and protection domain as this lookup's lookup class. Classes in bytecode + * must be in the same package as the lookup class. + */ + public static Class defineClass(Lookup lookup, byte[] bytes) { + try { + return lookup.defineClass(bytes); + } catch (IllegalAccessException e) { + throw new IllegalStateException(trustedLookupMessage(), e); + } + } + + private static Lookup loadImplLookup() { + try { + Field field = Lookup.class.getDeclaredField("IMPL_LOOKUP"); + field.setAccessible(true); + return (Lookup) field.get(null); + } catch (ReflectiveOperationException | RuntimeException e) { + throw new IllegalStateException(trustedLookupMessage(), e); + } + } + + private static MethodHandle constructorLookup() { + MethodHandle constructor = constructorLookup; + if (constructor == null) { + synchronized (_Lookup.class) { + constructor = constructorLookup; + if (constructor == null) { + constructor = loadConstructorLookup(); + constructorLookup = constructor; + } + } + } + return constructor; + } + + private static MethodHandle loadConstructorLookup() { + try { + return implLookup() + .findConstructor( + Lookup.class, MethodType.methodType(void.class, Class.class, Class.class, int.class)); + } catch (ReflectiveOperationException | RuntimeException e) { + throw new IllegalStateException(trustedLookupMessage(), e); + } + } + + private static Lookup implLookup() { + Lookup lookup = implLookup; + if (lookup == null) { + synchronized (_Lookup.class) { + lookup = implLookup; + if (lookup == null) { + lookup = loadImplLookup(); + implLookup = lookup; + } + } + } + return lookup; + } + + private static String trustedLookupMessage() { + return _JDKAccess.jdk25AccessMessage(); + } +} diff --git a/java/fory-core/src/main/java25/org/apache/fory/platform/internal/_UnsafeUtils.java b/java/fory-core/src/main/java25/org/apache/fory/platform/internal/_UnsafeUtils.java new file mode 100644 index 0000000000..63ecfd36ce --- /dev/null +++ b/java/fory-core/src/main/java25/org/apache/fory/platform/internal/_UnsafeUtils.java @@ -0,0 +1,27 @@ +/* + * 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; + +/** JDK25 replacement that removes the root runtime's Unsafe descriptor from the class graph. */ +// CHECKSTYLE.OFF:TypeName +public final class _UnsafeUtils { + // CHECKSTYLE.ON:TypeName + private _UnsafeUtils() {} +} diff --git a/java/fory-core/src/main/java25/org/apache/fory/reflect/InstanceFieldAccessors.java b/java/fory-core/src/main/java25/org/apache/fory/reflect/InstanceFieldAccessors.java new file mode 100644 index 0000000000..022ed59f46 --- /dev/null +++ b/java/fory-core/src/main/java25/org/apache/fory/reflect/InstanceFieldAccessors.java @@ -0,0 +1,291 @@ +/* + * 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.VarHandle; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import org.apache.fory.annotation.Internal; +import org.apache.fory.platform.internal._JDKAccess; +import org.apache.fory.util.Preconditions; + +/** + * 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); + 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 VarHandle fieldHandle(Field field) { + try { + return _JDKAccess + ._trustedLookup(field.getDeclaringClass()) + .findVarHandle(field.getDeclaringClass(), field.getName(), field.getType()); + } catch (IllegalAccessException e) { + throw accessFailure(field, e); + } catch (NoSuchFieldException e) { + throw new IllegalStateException("Failed to create VarHandle for field " + field, e); + } + } + + private static IllegalStateException accessFailure(Field field, Throwable cause) { + return new IllegalStateException( + "Cannot access field " + field + ". " + _JDKAccess.jdk25AccessMessage(), + cause); + } + + /** Public only for generated serializers; use {@link FieldAccessor#createAccessor(Field)}. */ + public static final class InstanceAccessor extends FieldAccessor { + private final VarHandle handle; + private final int accessKind; + + InstanceAccessor(Field field) { + super(field); + handle = fieldHandle(field); + accessKind = accessKind(field); + } + + @Override + public Object get(Object obj) { + switch (accessKind) { + case BOOLEAN_ACCESS: + return (boolean) handle.get(obj); + case BYTE_ACCESS: + return (byte) handle.get(obj); + case CHAR_ACCESS: + return (char) handle.get(obj); + case SHORT_ACCESS: + return (short) handle.get(obj); + case INT_ACCESS: + return (int) handle.get(obj); + case LONG_ACCESS: + return (long) handle.get(obj); + case FLOAT_ACCESS: + return (float) handle.get(obj); + case DOUBLE_ACCESS: + return (double) handle.get(obj); + case OBJECT_ACCESS: + return handle.get(obj); + default: + throw new IllegalStateException("Unsupported access kind " + accessKind); + } + } + + @Override + public void set(Object obj, Object value) { + switch (accessKind) { + case BOOLEAN_ACCESS: + handle.set(obj, (Boolean) value); + return; + case BYTE_ACCESS: + handle.set(obj, (Byte) value); + return; + case CHAR_ACCESS: + handle.set(obj, (Character) value); + return; + case SHORT_ACCESS: + handle.set(obj, (Short) value); + return; + case INT_ACCESS: + handle.set(obj, (Integer) value); + return; + case LONG_ACCESS: + handle.set(obj, (Long) value); + return; + case FLOAT_ACCESS: + handle.set(obj, (Float) value); + return; + case DOUBLE_ACCESS: + handle.set(obj, (Double) value); + return; + case OBJECT_ACCESS: + handle.set(obj, value); + return; + default: + throw new IllegalStateException("Unsupported access kind " + accessKind); + } + } + + @Override + public void copy(Object sourceObject, Object targetObject) { + switch (accessKind) { + case BOOLEAN_ACCESS: + handle.set(targetObject, (boolean) handle.get(sourceObject)); + return; + case BYTE_ACCESS: + handle.set(targetObject, (byte) handle.get(sourceObject)); + return; + case CHAR_ACCESS: + handle.set(targetObject, (char) handle.get(sourceObject)); + return; + case SHORT_ACCESS: + handle.set(targetObject, (short) handle.get(sourceObject)); + return; + case INT_ACCESS: + handle.set(targetObject, (int) handle.get(sourceObject)); + return; + case LONG_ACCESS: + handle.set(targetObject, (long) handle.get(sourceObject)); + return; + case FLOAT_ACCESS: + handle.set(targetObject, (float) handle.get(sourceObject)); + return; + case DOUBLE_ACCESS: + handle.set(targetObject, (double) handle.get(sourceObject)); + return; + case OBJECT_ACCESS: + handle.set(targetObject, handle.get(sourceObject)); + return; + default: + throw new IllegalStateException("Unsupported access kind " + accessKind); + } + } + + @Override + public void copyObject(Object sourceObject, Object targetObject) { + if (accessKind == OBJECT_ACCESS) { + handle.set(targetObject, handle.get(sourceObject)); + } else { + super.copyObject(sourceObject, targetObject); + } + } + + @Override + public boolean getBoolean(Object obj) { + return (boolean) handle.get(obj); + } + + @Override + public void putBoolean(Object obj, boolean value) { + handle.set(obj, value); + } + + @Override + public byte getByte(Object obj) { + return (byte) handle.get(obj); + } + + @Override + public void putByte(Object obj, byte value) { + handle.set(obj, value); + } + + @Override + public char getChar(Object obj) { + return (char) handle.get(obj); + } + + @Override + public void putChar(Object obj, char value) { + handle.set(obj, value); + } + + @Override + public short getShort(Object obj) { + return (short) handle.get(obj); + } + + @Override + public void putShort(Object obj, short value) { + handle.set(obj, value); + } + + @Override + public int getInt(Object obj) { + return (int) handle.get(obj); + } + + @Override + public void putInt(Object obj, int value) { + handle.set(obj, value); + } + + @Override + public long getLong(Object obj) { + return (long) handle.get(obj); + } + + @Override + public void putLong(Object obj, long value) { + handle.set(obj, value); + } + + @Override + public float getFloat(Object obj) { + return (float) handle.get(obj); + } + + @Override + public void putFloat(Object obj, float value) { + handle.set(obj, value); + } + + @Override + public double getDouble(Object obj) { + return (double) handle.get(obj); + } + + @Override + public void putDouble(Object obj, double value) { + handle.set(obj, value); + } + } +} diff --git a/java/fory-core/src/main/java/org/apache/fory/reflect/ReflectionFieldAccessor.java b/java/fory-core/src/main/java25/org/apache/fory/reflect/UnsafeObjectInstantiator.java similarity index 53% rename from java/fory-core/src/main/java/org/apache/fory/reflect/ReflectionFieldAccessor.java rename to java/fory-core/src/main/java25/org/apache/fory/reflect/UnsafeObjectInstantiator.java index 3ac32c4625..8f4c1ef374 100644 --- a/java/fory-core/src/main/java/org/apache/fory/reflect/ReflectionFieldAccessor.java +++ b/java/fory-core/src/main/java25/org/apache/fory/reflect/UnsafeObjectInstantiator.java @@ -19,36 +19,31 @@ package org.apache.fory.reflect; -import java.lang.reflect.Field; +import org.apache.fory.annotation.Internal; import org.apache.fory.exception.ForyException; -final class ReflectionFieldAccessor extends FieldAccessor { - ReflectionFieldAccessor(Field field) { - super(field, -1); - try { - field.setAccessible(true); - } catch (RuntimeException e) { - throw new ForyException("Failed to make field accessible: " + field, e); - } +/** JDK25 replacement for the JDK8-24 Unsafe-backed instantiator. */ +@Internal +final class UnsafeObjectInstantiator extends ObjectInstantiator { + UnsafeObjectInstantiator(Class type) { + super(type); } @Override - public Object get(Object obj) { - checkObj(obj); - try { - return field.get(obj); - } catch (IllegalAccessException | IllegalArgumentException e) { - throw new ForyException("Failed to read field reflectively: " + field, e); - } + public T newInstance() { + throw unsupported(type); } @Override - public void set(Object obj, Object value) { - checkObj(obj); - try { - field.set(obj, value); - } catch (IllegalAccessException | IllegalArgumentException e) { - throw new ForyException("Failed to write field reflectively: " + field, e); - } + public T newInstanceWithArguments(Object... arguments) { + throw unsupported(type); + } + + private static ForyException unsupported(Class type) { + return new ForyException( + "Unsafe allocation is unsupported for " + + type + + " in JDK25+ zero-Unsafe mode. Provide an accessible no-arg constructor, " + + "use a record canonical constructor, or register a custom serializer."); } } diff --git a/java/fory-core/src/main/java25/org/apache/fory/serializer/PlatformStringUtils.java b/java/fory-core/src/main/java25/org/apache/fory/serializer/PlatformStringUtils.java new file mode 100644 index 0000000000..b399206bd1 --- /dev/null +++ b/java/fory-core/src/main/java25/org/apache/fory/serializer/PlatformStringUtils.java @@ -0,0 +1,201 @@ +/* + * 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.serializer; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodHandles.Lookup; +import java.lang.invoke.VarHandle; +import java.lang.reflect.Field; +import java.nio.ByteOrder; +import org.apache.fory.memory.MemoryBuffer; +import org.apache.fory.memory.NativeByteOrder; +import org.apache.fory.platform.GraalvmSupport; +import org.apache.fory.platform.internal._JDKAccess; +import org.apache.fory.util.Preconditions; + +/** JDK25 string internals used by {@link StringSerializer}. */ +final class PlatformStringUtils { + private static final StringHandles STRING_HANDLES = stringHandles(); + + static final boolean JDK_STRING_FIELD_ACCESS = STRING_HANDLES.fieldAccess; + static final boolean STRING_VALUE_FIELD_IS_CHARS = + JDK_STRING_FIELD_ACCESS && STRING_HANDLES.valueFieldIsChars; + static final boolean STRING_VALUE_FIELD_IS_BYTES = + JDK_STRING_FIELD_ACCESS && STRING_HANDLES.valueFieldIsBytes; + static final boolean STRING_HAS_COUNT_OFFSET = JDK_STRING_FIELD_ACCESS && STRING_HANDLES.counted; + + private static final VarHandle STRING_VALUE_HANDLE = STRING_HANDLES.value; + private static final VarHandle STRING_CODER_HANDLE = STRING_HANDLES.coder; + private static final VarHandle STRING_COUNT_HANDLE = STRING_HANDLES.count; + private static final VarHandle STRING_OFFSET_HANDLE = STRING_HANDLES.offset; + + private static final VarHandle BYTE_ARRAY_LONG = + MethodHandles.byteArrayViewVarHandle(long[].class, ByteOrder.nativeOrder()); + private static final VarHandle BYTE_ARRAY_CHAR = + MethodHandles.byteArrayViewVarHandle(char[].class, ByteOrder.nativeOrder()); + + private PlatformStringUtils() {} + + private static StringHandles stringHandles() { + if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE || !_JDKAccess.JDK_INTERNAL_FIELD_ACCESS) { + return StringHandles.noAccess(); + } + try { + Field valueField = String.class.getDeclaredField("value"); + boolean valueFieldIsChars = valueField.getType() == char[].class; + boolean valueFieldIsBytes = valueField.getType() == byte[].class; + Field countField = getStringFieldNullable("count"); + Field offsetField = getStringFieldNullable("offset"); + boolean counted = false; + if (countField != null || offsetField != null) { + Preconditions.checkArgument( + countField != null && offsetField != null, "Current jdk not supported"); + Preconditions.checkArgument( + countField.getType() == int.class && offsetField.getType() == int.class, + "Current jdk not supported"); + counted = true; + } + try { + Lookup stringLookup = _JDKAccess._trustedLookup(String.class); + return new StringHandles( + true, + valueFieldIsChars, + valueFieldIsBytes, + counted, + stringLookup.findVarHandle(String.class, "value", valueField.getType()), + valueFieldIsBytes + ? stringLookup.findVarHandle(String.class, "coder", byte.class) + : null, + countField == null + ? null + : stringLookup.findVarHandle(String.class, "count", int.class), + offsetField == null + ? null + : stringLookup.findVarHandle(String.class, "offset", int.class)); + } catch (Throwable e) { + throw new IllegalStateException( + "JDK25+ string internals are inaccessible. " + _JDKAccess.jdk25AccessMessage(), + e); + } + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } + } + + private static Field getStringFieldNullable(String fieldName) { + try { + return String.class.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + return null; + } + } + + private static final class StringHandles { + private final boolean fieldAccess; + private final boolean valueFieldIsChars; + private final boolean valueFieldIsBytes; + private final boolean counted; + private final VarHandle value; + private final VarHandle coder; + private final VarHandle count; + private final VarHandle offset; + + private StringHandles( + boolean fieldAccess, + boolean valueFieldIsChars, + boolean valueFieldIsBytes, + boolean counted, + VarHandle value, + VarHandle coder, + VarHandle count, + VarHandle offset) { + this.fieldAccess = fieldAccess; + this.valueFieldIsChars = valueFieldIsChars; + this.valueFieldIsBytes = valueFieldIsBytes; + this.counted = counted; + this.value = value; + this.coder = coder; + this.count = count; + this.offset = offset; + } + + private static StringHandles noAccess() { + return new StringHandles(false, false, false, false, null, null, null, null); + } + } + + static Object getStringValue(String value) { + return STRING_VALUE_HANDLE.get(value); + } + + static byte getStringCoder(String value) { + return (byte) STRING_CODER_HANDLE.get(value); + } + + static int getStringOffset(String value) { + return (int) STRING_OFFSET_HANDLE.get(value); + } + + static int getStringCount(String value) { + return (int) STRING_COUNT_HANDLE.get(value); + } + + static long getCharsLong(char[] chars, int charIndex) { + long c0 = chars[charIndex]; + long c1 = chars[charIndex + 1]; + long c2 = chars[charIndex + 2]; + long c3 = chars[charIndex + 3]; + if (NativeByteOrder.IS_LITTLE_ENDIAN) { + return c0 | (c1 << 16) | (c2 << 32) | (c3 << 48); + } else { + return (c0 << 48) | (c1 << 32) | (c2 << 16) | c3; + } + } + + static long getBytesLong(byte[] bytes, int byteIndex) { + return (long) BYTE_ARRAY_LONG.get(bytes, byteIndex); + } + + static char getBytesChar(byte[] bytes, int byteIndex) { + return (char) BYTE_ARRAY_CHAR.get(bytes, byteIndex); + } + + static void copyCharsToBytes( + char[] chars, int charOffset, byte[] target, int byteOffset, int numBytes) { + int charIndex = charOffset; + if (NativeByteOrder.IS_LITTLE_ENDIAN) { + for (int i = byteOffset, end = byteOffset + numBytes; i < end; i += 2) { + char c = chars[charIndex++]; + target[i] = (byte) c; + target[i + 1] = (byte) (c >>> 8); + } + } else { + for (int i = byteOffset, end = byteOffset + numBytes; i < end; i += 2) { + char c = chars[charIndex++]; + target[i] = (byte) (c >>> 8); + target[i + 1] = (byte) c; + } + } + } + + static void putBytes(MemoryBuffer buffer, int writerIndex, byte[] bytes, int numBytes) { + buffer.put(writerIndex, bytes, 0, numBytes); + } +} diff --git a/java/fory-core/src/main/java9/module-info.java b/java/fory-core/src/main/java9/module-info.java index e381802cae..49b67a908f 100644 --- a/java/fory-core/src/main/java9/module-info.java +++ b/java/fory-core/src/main/java9/module-info.java @@ -53,6 +53,4 @@ exports org.apache.fory.util; exports org.apache.fory.util.function; exports org.apache.fory.util.record; - exports org.apache.fory.util.unsafe to - org.apache.fory.format; } diff --git a/java/fory-core/src/main/resources/META-INF/LICENSE b/java/fory-core/src/main/resources/META-INF/LICENSE index 6de997e32b..149d74bdcf 100644 --- a/java/fory-core/src/main/resources/META-INF/LICENSE +++ b/java/fory-core/src/main/resources/META-INF/LICENSE @@ -224,7 +224,6 @@ The text of each license is the standard Apache 2.0 license. * spark (https://github.com/apache/spark) Files: java/fory-core/src/main/java/org/apache/fory/codegen/Code.java - java/fory-core/src/main/java/org/apache/fory/platform/UnsafeOps.java * commons-io (https://github.com/apache/commons-io) Files: diff --git a/java/fory-core/src/main/resources/META-INF/native-image/org.apache.fory/fory-core/native-image.properties b/java/fory-core/src/main/resources/META-INF/native-image/org.apache.fory/fory-core/native-image.properties index 3df07eed99..a4019d07d3 100644 --- a/java/fory-core/src/main/resources/META-INF/native-image/org.apache.fory/fory-core/native-image.properties +++ b/java/fory-core/src/main/resources/META-INF/native-image/org.apache.fory/fory-core/native-image.properties @@ -26,15 +26,21 @@ Args=--initialize-at-build-time=org.apache.fory.annotation.ForyField$Dynamic,\ org.apache.fory.collection.ReferenceConcurrentMap$SoftValueReference,\ org.apache.fory.collection.ReferenceConcurrentMap$WeakKeyReference,\ org.apache.fory.memory.ByteBufferUtil,\ + org.apache.fory.memory.LittleEndian,\ org.apache.fory.memory.MemoryBuffer,\ org.apache.fory.memory.NativeByteOrder,\ org.apache.fory.platform.AndroidSupport,\ + org.apache.fory.platform.internal._UnsafeUtils,\ + org.apache.fory.reflect.UnsafeObjectInstantiator,\ org.apache.fory.reflect.JvmTypeUseMetadata,\ org.apache.fory.reflect.TypeUseMetadata,\ org.apache.fory.resolver.StaticGeneratedSerializerRegistry,\ + org.apache.fory.serializer.collection.CollectionSerializers,\ org.apache.fory.serializer.collection.GuavaCollectionSerializers,\ + org.apache.fory.serializer.collection.MapSerializers,\ org.apache.fory.serializer.struct.Fingerprint,\ org.apache.fory.serializer.Float16Serializer,\ + org.apache.fory.serializer.PlatformStringUtils,\ org.apache.fory.serializer.PrimitiveSerializers$Float16Serializer,\ java.io.IOException,\ java.lang.ArrayIndexOutOfBoundsException,\ @@ -158,6 +164,7 @@ Args=--initialize-at-build-time=org.apache.fory.annotation.ForyField$Dynamic,\ org.apache.fory.Fory,\ org.apache.fory.ThreadLocalFory,\ org.apache.fory.builder.AccessorHelper,\ + org.apache.fory.builder.UnsafeCodegenSupport,\ org.apache.fory.builder.JITContext,\ org.apache.fory.builder.ObjectCodecBuilder,\ org.apache.fory.builder.CompatibleCodecBuilder,\ @@ -212,7 +219,6 @@ Args=--initialize-at-build-time=org.apache.fory.annotation.ForyField$Dynamic,\ org.apache.fory.memory.MemoryBuffer$DefaultMemoryAllocator,\ org.apache.fory.memory.MemoryUtils,\ org.apache.fory.platform.JdkVersion,\ - org.apache.fory.platform.UnsafeOps,\ org.apache.fory.collection.ConcurrentIdentityMap,\ org.apache.fory.collection.ConcurrentIdentityMap$IdentityKey,\ org.apache.fory.meta.FieldTypes,\ @@ -241,7 +247,9 @@ Args=--initialize-at-build-time=org.apache.fory.annotation.ForyField$Dynamic,\ org.apache.fory.meta.MetaStringEncoder,\ org.apache.fory.meta.TypeEqualMetaCompressor,\ org.apache.fory.pool.ThreadPoolFory,\ - org.apache.fory.reflect.FieldAccessor$GeneratedAccessor,\ + org.apache.fory.reflect.InstanceFieldAccessors,\ + org.apache.fory.reflect.InstanceFieldAccessors$InstanceAccessor,\ + org.apache.fory.reflect.InstanceFieldAccessors$GeneratedAccessor,\ org.apache.fory.reflect.ReflectionUtils$1,\ org.apache.fory.reflect.ReflectionUtils$2,\ org.apache.fory.reflect.ReflectionUtils,\ @@ -250,11 +258,15 @@ Args=--initialize-at-build-time=org.apache.fory.annotation.ForyField$Dynamic,\ org.apache.fory.reflect.TypeRef$ClassOwnership,\ org.apache.fory.reflect.TypeRef$TypeVariableKey,\ org.apache.fory.reflect.TypeRef,\ - org.apache.fory.reflect.ObjectCreators,\ - org.apache.fory.reflect.ObjectCreators$UnsafeObjectCreator,\ - org.apache.fory.reflect.ObjectCreators$DeclaredNoArgCtrObjectCreator,\ - org.apache.fory.reflect.ObjectCreators$ParentNoArgCtrObjectCreator,\ - org.apache.fory.reflect.ObjectCreators$RecordObjectCreator,\ + org.apache.fory.reflect.ObjectInstantiators,\ + org.apache.fory.reflect.ObjectInstantiators$DeclaredNoArgCtrInstantiator,\ + org.apache.fory.reflect.ObjectInstantiators$GraalvmObjectInstantiator,\ + org.apache.fory.reflect.ObjectInstantiators$ObjectStreamClassInstantiator,\ + org.apache.fory.reflect.ObjectInstantiators$ObjectStreamInstantiator,\ + org.apache.fory.reflect.ObjectInstantiators$ObjectStreamAccess,\ + org.apache.fory.reflect.ObjectInstantiators$ParentNoArgCtrInstantiator,\ + org.apache.fory.reflect.ObjectInstantiators$RecordObjectInstantiator,\ + org.apache.fory.reflect.ObjectInstantiators$UnsupportedObjectInstantiator,\ org.apache.fory.resolver.TypeChecker,\ org.apache.fory.resolver.TypeInfo,\ org.apache.fory.resolver.TypeInfoHolder,\ @@ -335,7 +347,6 @@ Args=--initialize-at-build-time=org.apache.fory.annotation.ForyField$Dynamic,\ org.apache.fory.serializer.ExceptionSerializers,\ org.apache.fory.serializer.ExceptionSerializers$ExceptionSerializer,\ org.apache.fory.serializer.ExceptionSerializers$StackTraceElementSerializer,\ - org.apache.fory.serializer.ExceptionSerializers$ThrowableOffsets,\ org.apache.fory.serializer.collection.SubListSerializers,\ org.apache.fory.serializer.collection.SubListSerializers$SubListSerializer,\ org.apache.fory.serializer.collection.SubListSerializers$ViewFields,\ @@ -396,7 +407,6 @@ Args=--initialize-at-build-time=org.apache.fory.annotation.ForyField$Dynamic,\ org.apache.fory.serializer.Serializers$URISerializer,\ org.apache.fory.serializer.Serializers$UUIDSerializer,\ org.apache.fory.serializer.Serializers,\ - org.apache.fory.serializer.StringSerializer$Offset,\ org.apache.fory.serializer.StringSerializer,\ org.apache.fory.serializer.TimeSerializers$DateSerializer,\ org.apache.fory.serializer.TimeSerializers$DurationSerializer,\ @@ -458,11 +468,11 @@ Args=--initialize-at-build-time=org.apache.fory.annotation.ForyField$Dynamic,\ org.apache.fory.serializer.collection.MapSerializers$SingletonMapSerializer,\ org.apache.fory.serializer.collection.MapSerializers$SortedMapSerializer,\ org.apache.fory.serializer.collection.MapSerializers$XlangMapSerializer,\ - org.apache.fory.serializer.collection.SynchronizedSerializers$Offset,\ + org.apache.fory.serializer.collection.SynchronizedSerializers$SourceAccessors,\ org.apache.fory.serializer.collection.SynchronizedSerializers$SynchronizedCollectionSerializer,\ org.apache.fory.serializer.collection.SynchronizedSerializers$SynchronizedMapSerializer,\ org.apache.fory.serializer.collection.SynchronizedSerializers,\ - org.apache.fory.serializer.collection.UnmodifiableSerializers$Offset,\ + org.apache.fory.serializer.collection.UnmodifiableSerializers$SourceAccessors,\ org.apache.fory.serializer.collection.UnmodifiableSerializers$UnmodifiableCollectionSerializer,\ org.apache.fory.serializer.collection.UnmodifiableSerializers$UnmodifiableMapSerializer,\ org.apache.fory.serializer.collection.UnmodifiableSerializers,\ @@ -492,7 +502,7 @@ Args=--initialize-at-build-time=org.apache.fory.annotation.ForyField$Dynamic,\ org.apache.fory.serializer.ObjectStreamSerializer$1,\ org.apache.fory.serializer.ObjectStreamSerializer$ForyObjectInputStream,\ org.apache.fory.serializer.ObjectStreamSerializer$ForyObjectOutputStream,\ - org.apache.fory.serializer.ObjectStreamSerializer$StreamTypeInfo,\ + org.apache.fory.serializer.ObjectStreamSerializer$StreamTypeInfo,\ org.apache.fory.serializer.ObjectStreamSerializer$SlotsInfo,\ org.apache.fory.serializer.collection.ChildContainerSerializers$ChildCollectionSerializer,\ org.apache.fory.serializer.collection.ChildContainerSerializers$ChildMapSerializer,\ @@ -534,9 +544,9 @@ Args=--initialize-at-build-time=org.apache.fory.annotation.ForyField$Dynamic,\ org.apache.fory.util.record.RecordUtils$2,\ org.apache.fory.util.record.RecordUtils$3,\ org.apache.fory.util.record.RecordUtils,\ - org.apache.fory.util.unsafe._JDKAccess$1,\ - org.apache.fory.util.unsafe._JDKAccess,\ - org.apache.fory.util.unsafe._Lookup,\ + org.apache.fory.serializer.PlatformStringUtils$StringCoderField,\ + org.apache.fory.platform.internal._JDKAccess,\ + org.apache.fory.platform.internal._Lookup,\ org.apache.fory.codegen.JaninoUtils$CodeStats,\ org.apache.fory.shaded.org.codehaus.commons.compiler.Location,\ org.apache.fory.shaded.org.codehaus.commons.compiler.util.iterator.Iterables,\ @@ -882,5 +892,4 @@ Args=--initialize-at-build-time=org.apache.fory.annotation.ForyField$Dynamic,\ org.apache.fory.shaded.org.codehaus.janino.util.ClassFile$StackMapTableAttribute$StackMapFrame,\ org.apache.fory.shaded.org.codehaus.janino.util.ClassFile$StackMapTableAttribute$UninitializedVariableInfo,\ org.apache.fory.shaded.org.codehaus.janino.util.ClassFile$StackMapTableAttribute$VerificationTypeInfo,\ - org.apache.fory.shaded.org.codehaus.janino.util.Traverser,\ - sun.misc.Unsafe + org.apache.fory.shaded.org.codehaus.janino.util.Traverser diff --git a/java/fory-core/src/test/java/org/apache/fory/ForyTest.java b/java/fory-core/src/test/java/org/apache/fory/ForyTest.java index 5a3041677f..b070b64777 100644 --- a/java/fory-core/src/test/java/org/apache/fory/ForyTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/ForyTest.java @@ -50,7 +50,6 @@ import java.util.TreeSet; import java.util.UUID; import java.util.WeakHashMap; -import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; import org.apache.fory.annotation.Expose; @@ -72,6 +71,7 @@ import org.apache.fory.serializer.EnumSerializerTest; import org.apache.fory.serializer.ExceptionSerializers; import org.apache.fory.serializer.ObjectSerializer; +import org.apache.fory.serializer.ReplaceResolveSerializer; import org.apache.fory.serializer.Serializer; import org.apache.fory.test.bean.BeanA; import org.apache.fory.test.bean.Struct; @@ -96,6 +96,49 @@ public void typedDeserializeRejectsOutOfBandRootHeaderWithoutBuffers() { assertThrows(IllegalArgumentException.class, () -> fory.deserialize(bytes, Integer.class)); } + @Test + public void testReverseComparatorSerializer() { + Fory fory = Fory.builder().withXlang(false).requireClassRegistration(false).build(); + Comparator comparator = Comparator.reverseOrder(); + Serializer serializer = + fory.getTypeResolver().getTypeInfo(comparator.getClass()).getSerializer(); + assertTrue(serializer instanceof ReplaceResolveSerializer); + Comparator roundTrip = + (Comparator) fory.deserialize(fory.serialize(comparator)); + Assert.assertEquals(roundTrip.getClass(), comparator.getClass()); + Assert.assertEquals(roundTrip.compare(1, 2), comparator.compare(1, 2)); + Comparator copy = fory.copy(comparator); + Assert.assertEquals(copy.getClass(), comparator.getClass()); + Assert.assertEquals(copy.compare(2, 1), comparator.compare(2, 1)); + } + + @Test + public void testRegistrationFreezesOnUse() { + byte[] bytes = newNativeFory().serialize(1); + + Fory writer = newNativeFory(); + writer.serialize(1); + assertRegistrationFrozen(writer); + + Fory reader = newNativeFory(); + reader.deserialize(bytes); + assertRegistrationFrozen(reader); + + Fory copier = newNativeFory(); + copier.copy(1); + assertRegistrationFrozen(copier); + } + + private static Fory newNativeFory() { + return Fory.builder().withXlang(false).requireClassRegistration(false).build(); + } + + private static void assertRegistrationFrozen(Fory fory) { + assertThrows(ForyException.class, () -> fory.register(BeanA.class)); + assertThrows( + ForyException.class, () -> fory.registerSerializer(BeanA.class, ObjectSerializer.class)); + } + @Test(dataProvider = "crossLanguageReferenceTrackingConfig") public void primitivesTest(boolean referenceTracking, boolean xlang) { Fory fory1 = @@ -434,11 +477,20 @@ class A {} } @Data - @AllArgsConstructor private static class IgnoreFields { @Ignore int f1; @Ignore long f2; long f3; + + IgnoreFields(int f1, long f2, long f3) { + this.f1 = f1; + this.f2 = f2; + this.f3 = f3; + } + + IgnoreFields(long f3) { + this.f3 = f3; + } } @Test @@ -451,13 +503,31 @@ public void testIgnoreFields() { } @Data - @AllArgsConstructor private static class ExposeFields { @Expose int f1; @Expose long f2; long f3; @Expose ImmutableMap map1; ImmutableMap map2; + + ExposeFields( + int f1, + long f2, + long f3, + ImmutableMap map1, + ImmutableMap map2) { + this.f1 = f1; + this.f2 = f2; + this.f3 = f3; + this.map1 = map1; + this.map2 = map2; + } + + ExposeFields(int f1, long f2, ImmutableMap map1) { + this.f1 = f1; + this.f2 = f2; + this.map1 = map1; + } } @Test @@ -474,11 +544,16 @@ public void testExposeFields() { } @Data - @AllArgsConstructor private static class ExposeFields2 { @Expose int f1; @Ignore long f2; long f3; + + ExposeFields2(int f1, long f2, long f3) { + this.f1 = f1; + this.f2 = f2; + this.f3 = f3; + } } @Test @@ -596,7 +671,7 @@ public void testPkgAccessLevelParentClass() { .build(); HashBasedTable table = HashBasedTable.create(2, 4); table.put("r", "c", 100); - serDeCheckSerializer(fory, table, "Codec"); + serDeCheckSerializer(fory, table, "HashBasedTableSerializer"); } @Data @@ -732,10 +807,14 @@ public void testMaxDepth() { assertThrows(InsecureException.class, () -> fory.deserialize(bytes)); } - @AllArgsConstructor static class MaxDepth { int f1; Object f2; + + MaxDepth(int f1, Object f2) { + this.f1 = f1; + this.f2 = f2; + } } @Test diff --git a/java/fory-core/src/test/java/org/apache/fory/GuavaOptionalDependencyTest.java b/java/fory-core/src/test/java/org/apache/fory/GuavaOptionalDependencyTest.java index a10e9270db..1e2f624f6e 100644 --- a/java/fory-core/src/test/java/org/apache/fory/GuavaOptionalDependencyTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/GuavaOptionalDependencyTest.java @@ -26,10 +26,16 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.Set; import java.util.stream.Collectors; +import org.apache.fory.platform.GraalvmSupport; import org.apache.fory.resolver.ClassResolver; +import org.apache.fory.serializer.Serializer; import org.apache.fory.serializer.collection.GuavaCollectionSerializers; import org.testng.annotations.Test; @@ -39,6 +45,8 @@ public class GuavaOptionalDependencyTest { @Test public void testBuildWithoutGuavaAndReserveIds() throws Exception { assertTrue(GuavaCollectionSerializers.isGuavaAvailable()); + assertEquals(GuavaCollectionSerializers.getNumReservedTypeIds(), 13); + assertGraalvmGuavaSerializers(); RegistrationIds inProcessIds = currentProcessIds(); assertEquals( inProcessIds.enabledId - inProcessIds.disabledId, @@ -48,10 +56,26 @@ public void testBuildWithoutGuavaAndReserveIds() throws Exception { assertEquals(childIds.disabledId, inProcessIds.disabledId); } + @Test + public void testPartialGuavaReservesIds() throws Exception { + RegistrationIds inProcessIds = currentProcessIds(); + RegistrationIds childIds = runWithPartialGuava(); + assertEquals(childIds.enabledId, inProcessIds.enabledId); + assertEquals(childIds.disabledId, inProcessIds.disabledId); + } + private static RegistrationIds currentProcessIds() { return new RegistrationIds(registeredInternalId(true), registeredInternalId(false)); } + private static void assertGraalvmGuavaSerializers() { + Set> serializers = GraalvmSupport.getRegisteredSerializerClasses(); + assertTrue(serializers.contains(GuavaCollectionSerializers.ImmutableIntArraySerializer.class)); + assertTrue(serializers.contains(GuavaCollectionSerializers.ImmutableMapFormSerializer.class)); + assertTrue(serializers.contains(GuavaCollectionSerializers.ImmutableBiMapFormSerializer.class)); + assertTrue(serializers.contains(GuavaCollectionSerializers.HashBasedTableSerializer.class)); + } + private static int registeredInternalId(boolean registerGuavaTypes) { Fory fory = Fory.builder() @@ -66,11 +90,9 @@ private static int registeredInternalId(boolean registerGuavaTypes) { } private static RegistrationIds runWithoutGuava() throws Exception { - String javaBin = - System.getProperty("java.home") + File.separator + "bin" + File.separator + "java"; String filteredClassPath = removeGuavaFromClasspath(System.getProperty("java.class.path")); Process process = - new ProcessBuilder(javaBin, "-cp", filteredClassPath, NoGuavaMain.class.getName()) + new ProcessBuilder(TestUtils.javaCommand(filteredClassPath, NoGuavaMain.class)) .redirectErrorStream(true) .start(); String output = readFully(process.getInputStream()); @@ -78,6 +100,14 @@ private static RegistrationIds runWithoutGuava() throws Exception { return parseResult(output); } + private static RegistrationIds runWithPartialGuava() throws Exception { + try (PartialGuavaClassLoader loader = new PartialGuavaClassLoader(classPathUrls())) { + Class main = Class.forName(PartialGuavaMain.class.getName(), true, loader); + String output = (String) main.getMethod("run").invoke(null); + return parseResult(output); + } + } + private static RegistrationIds parseResult(String output) { for (String line : output.split("\\R")) { if (line.startsWith(RESULT_PREFIX)) { @@ -94,6 +124,20 @@ private static String removeGuavaFromClasspath(String classPath) { .collect(Collectors.joining(File.pathSeparator)); } + private static URL[] classPathUrls() { + return Arrays.stream(System.getProperty("java.class.path").split(File.pathSeparator)) + .map(GuavaOptionalDependencyTest::toUrl) + .toArray(URL[]::new); + } + + private static URL toUrl(String path) { + try { + return new File(path).toURI().toURL(); + } catch (MalformedURLException e) { + throw new IllegalArgumentException(e); + } + } + private static String readFully(InputStream inputStream) throws IOException { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; @@ -116,6 +160,7 @@ private RegistrationIds(int enabledId, int disabledId) { public static final class NoGuavaMain { public static void main(String[] args) { + assertSerializerMetadataLinked(); RegistrationIds ids = currentProcessIds(); Fory fory = Fory.builder() @@ -133,6 +178,51 @@ public static void main(String[] args) { } } + public static final class PartialGuavaMain { + public static String run() { + assertSerializerMetadataLinked(); + RegistrationIds ids = currentProcessIds(); + Fory fory = + Fory.builder() + .withXlang(false) + .registerGuavaTypes(true) + .requireClassRegistration(false) + .suppressClassRegistrationWarnings(true) + .build(); + byte[] bytes = fory.serialize(com.google.common.collect.ImmutableList.of("fory")); + Object value = fory.deserialize(bytes); + if (!com.google.common.collect.ImmutableList.of("fory").equals(value)) { + throw new AssertionError("Unexpected round-trip value " + value); + } + return RESULT_PREFIX + ids.enabledId + "," + ids.disabledId; + } + } + + private static void assertSerializerMetadataLinked() { + for (Class serializerClass : + GraalvmSupport.getRegisteredSerializerClasses()) { + serializerClass.getDeclaredConstructors(); + serializerClass.getDeclaredMethods(); + serializerClass.getDeclaredFields(); + } + } + + private static final class PartialGuavaClassLoader extends URLClassLoader { + private static final String FILTERED_CLASS = "com.google.common.primitives.ImmutableIntArray"; + + private PartialGuavaClassLoader(URL[] urls) { + super(urls, null); + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (name.equals(FILTERED_CLASS)) { + throw new ClassNotFoundException(name); + } + return super.loadClass(name, resolve); + } + } + public static final class InternalSample {} public static final class SampleValue { diff --git a/java/fory-core/src/test/java/org/apache/fory/JpmsOptionalClassLoadingTest.java b/java/fory-core/src/test/java/org/apache/fory/JpmsOptionalClassLoadingTest.java index 3a14548cd0..0d952e7072 100644 --- a/java/fory-core/src/test/java/org/apache/fory/JpmsOptionalClassLoadingTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/JpmsOptionalClassLoadingTest.java @@ -22,7 +22,6 @@ import static org.testng.Assert.assertEquals; import java.io.ByteArrayOutputStream; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.Serializable; @@ -37,16 +36,17 @@ public void testBuildWithoutJavaSqlModule() throws Exception { if (JdkVersion.MAJOR_VERSION < 9) { throw new SkipException("Skip on jdk" + JdkVersion.MAJOR_VERSION); } - String javaBin = - System.getProperty("java.home") + File.separator + "bin" + File.separator + "java"; + if (JdkVersion.MAJOR_VERSION >= 25) { + throw new SkipException( + "JDK25+ optional-module coverage runs in integration_tests/jpms_tests with packaged MR-JAR"); + } Process process = new ProcessBuilder( - javaBin, - "--limit-modules", - "java.base,java.logging,jdk.unsupported", - "-cp", - System.getProperty("java.class.path"), - NoJavaSqlMain.class.getName()) + TestUtils.javaCommand( + System.getProperty("java.class.path"), + NoJavaSqlMain.class, + "--limit-modules", + "java.base,java.logging,jdk.unsupported")) .redirectErrorStream(true) .start(); String output = readFully(process.getInputStream()); diff --git a/java/fory-core/src/test/java/org/apache/fory/StreamTest.java b/java/fory-core/src/test/java/org/apache/fory/StreamTest.java index e25b486493..41e63fa474 100644 --- a/java/fory-core/src/test/java/org/apache/fory/StreamTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/StreamTest.java @@ -457,6 +457,15 @@ public void testPrimitiveArrayStreamReaderUsesTypedReads() throws IOException { Assert.assertEquals((long[]) fory.deserialize(channel), longs); assertTrue(channel.readLongsCalled); } + + ByteBuffer limitedDirectBuffer = ByteBuffer.allocateDirect(serialized.length + 8); + limitedDirectBuffer.limit(5); + try (TrackingForyReadableChannel channel = + new TrackingForyReadableChannel( + new ChunkedReadableByteChannel(serialized, 1), limitedDirectBuffer)) { + Assert.assertEquals((long[]) fory.deserialize(channel), longs); + assertTrue(channel.readLongsCalled); + } } @Test diff --git a/java/fory-core/src/test/java/org/apache/fory/TestUtils.java b/java/fory-core/src/test/java/org/apache/fory/TestUtils.java index 4ea85a98ad..81cfec51a8 100644 --- a/java/fory-core/src/test/java/org/apache/fory/TestUtils.java +++ b/java/fory-core/src/test/java/org/apache/fory/TestUtils.java @@ -22,24 +22,100 @@ import com.google.common.collect.ImmutableBiMap; import com.google.common.collect.ImmutableMap; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.IOException; import java.io.ObjectOutputStream; +import java.lang.management.ManagementFactory; import java.lang.reflect.Field; import java.lang.reflect.Modifier; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; import java.util.function.Supplier; import java.util.stream.Collectors; import org.apache.fory.collection.Tuple3; import org.apache.fory.meta.TypeDef; -import org.apache.fory.platform.UnsafeOps; +import org.apache.fory.platform.JdkVersion; +import org.apache.fory.platform.internal._JDKAccess; import org.apache.fory.reflect.FieldAccessor; +import org.apache.fory.reflect.ObjectInstantiators; import org.apache.fory.reflect.ReflectionUtils; import org.apache.fory.type.Descriptor; -import org.apache.fory.util.unsafe._JDKAccess; import org.testng.SkipException; /** Test utils. */ public class TestUtils { + public static List javaCommand(Class mainClass) { + return javaCommand(forkClassPath(), mainClass); + } + + public static List javaCommand( + String classPath, Class mainClass, String... extraJvmArgs) { + List command = new ArrayList<>(); + command.add(System.getProperty("java.home") + File.separator + "bin" + File.separator + "java"); + command.addAll(forkJvmArgs()); + Collections.addAll(command, extraJvmArgs); + if (JdkVersion.MAJOR_VERSION >= 25) { + String modulePath = System.getProperty("jdk.module.path"); + if (modulePath != null && !modulePath.isEmpty()) { + command.add("--module-path"); + command.add(modulePath); + command.add("--add-modules"); + command.add("org.apache.fory.core"); + } + } + command.add("-cp"); + command.add(classPath); + command.add(mainClass.getName()); + return command; + } + + private static List forkJvmArgs() { + List args = new ArrayList<>(); + if (JdkVersion.MAJOR_VERSION >= 25) { + // TestUtils.javaCommand launches probes with -cp, so Fory code is in the unnamed module. + // Named-module coverage lives in integration_tests/jpms_tests. + args.add("--add-opens=java.base/java.lang.invoke=ALL-UNNAMED"); + if (hasInputArg("--sun-misc-unsafe-memory-access=deny")) { + args.add("--sun-misc-unsafe-memory-access=deny"); + } + finalFieldPolicyArg().ifPresent(args::add); + } + return args; + } + + public static String forkClassPath() { + LinkedHashSet elements = new LinkedHashSet<>(); + addPathElements(elements, System.getProperty("surefire.real.class.path")); + addPathElements(elements, System.getProperty("java.class.path")); + return String.join(File.pathSeparator, elements); + } + + private static void addPathElements(Set elements, String path) { + if (path == null || path.isEmpty()) { + return; + } + Collections.addAll(elements, path.split(java.util.regex.Pattern.quote(File.pathSeparator))); + } + + private static boolean hasInputArg(String arg) { + return ManagementFactory.getRuntimeMXBean().getInputArguments().contains(arg); + } + + private static Optional finalFieldPolicyArg() { + return ManagementFactory.getRuntimeMXBean().getInputArguments().stream() + .filter(arg -> arg.startsWith("--illegal-final-field-mutation=")) + .findFirst(); + } + @SuppressWarnings("unchecked") public static T getFieldValue(Object obj, String fieldName) { return (T) @@ -106,7 +182,7 @@ public static void jdkSerialize(ByteArrayOutputStream bas, Object data) { public static T unsafeCopy(T obj) { @SuppressWarnings("unchecked") - T newInstance = (T) UnsafeOps.newInstance(obj.getClass()); + T newInstance = (T) ObjectInstantiators.getObjectInstantiator(obj.getClass()).newInstance(); for (Field field : ReflectionUtils.getFields(obj.getClass(), true)) { if (!Modifier.isStatic(field.getModifiers())) { // Don't cache accessors by `obj.getClass()` using WeakHashMap, the `field` will reference diff --git a/java/fory-core/src/test/java/org/apache/fory/ThreadSafeForyTest.java b/java/fory-core/src/test/java/org/apache/fory/ThreadSafeForyTest.java index 47fe199d7c..19bcf03008 100644 --- a/java/fory-core/src/test/java/org/apache/fory/ThreadSafeForyTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/ThreadSafeForyTest.java @@ -51,14 +51,14 @@ public class ThreadSafeForyTest extends ForyTestBase { @Test - public void testBuildThreadSafeForyUsesThreadPoolFory() { + public void testBuildThreadSafeForyPool() { ThreadSafeFory fory = Fory.builder().withXlang(false).requireClassRegistration(false).buildThreadSafeFory(); assertTrue(fory instanceof ThreadPoolFory); } @Test - public void testThreadSafeBuildersAssignGeneratedNames() { + public void testThreadSafeBuilderNames() { ThreadSafeFory threadSafe = Fory.builder().withXlang(false).requireClassRegistration(false).buildThreadSafeFory(); ThreadSafeFory threadLocal = @@ -86,7 +86,7 @@ public void testThreadSafeBuildersAssignGeneratedNames() { } @Test - public void testFunctionFactoryConstructorsUseBuilderProvidedClassLoader() { + public void testFactoryConstructorsClassLoader() { ClassLoader custom = new ClassLoader(ClassLoader.getSystemClassLoader()) {}; ThreadLocalFory threadLocal = new ThreadLocalFory( @@ -99,7 +99,7 @@ public void testFunctionFactoryConstructorsUseBuilderProvidedClassLoader() { } @Test - public void testThreadSafeRuntimesShareRegistryAcrossRawForyInstances() throws Exception { + public void testThreadSafeRuntimesShareRegistry() throws Exception { ThreadLocalFory threadLocal = Fory.builder().withXlang(false).requireClassRegistration(false).buildThreadLocalFory(); AtomicReference threadLocalRegistry1 = new AtomicReference<>(); @@ -316,7 +316,7 @@ public void testSerializeWithMetaShare() throws InterruptedException { } @Test - public void testThreadLocalMetaShareWithPerThreadMetaContexts() throws InterruptedException { + public void testThreadLocalMetaShare() throws InterruptedException { ThreadSafeFory fory = Fory.builder() .withXlang(false) @@ -380,7 +380,7 @@ public void testSerializeDeserializeWithType() { } @Test - public void testDeserializeByteBufferPreservesPositionAndLimit() { + public void testByteBufferPositionLimit() { Fory writer = Fory.builder().withXlang(false).requireClassRegistration(false).build(); String value = "thread-safe-byte-buffer"; byte[] payload = writer.serialize(value); @@ -505,7 +505,7 @@ public void testSerializerRegister() { } @Test - public void testRegisterAfterSerializeThrowsException() { + public void testRegisterAfterSerializeThrows() { ThreadSafeFory fory = Fory.builder().withXlang(false).requireClassRegistration(true).buildThreadLocalFory(); fory.register(BeanA.class); @@ -514,7 +514,7 @@ public void testRegisterAfterSerializeThrowsException() { } @Test - public void testRegisterAfterSerializeThrowsExceptionWithFory() { + public void testForyRegisterAfterSerializeThrows() { Fory fory = Fory.builder().withXlang(false).requireClassRegistration(true).build(); fory.register(BeanA.class); fory.serialize("ok"); @@ -522,7 +522,7 @@ public void testRegisterAfterSerializeThrowsExceptionWithFory() { } @Test - public void testRegisterAfterSerializeThrowsExceptionWithForyPool() { + public void testPoolRegisterAfterSerializeThrows() { ThreadSafeFory fory = Fory.builder().withXlang(false).requireClassRegistration(true).buildThreadSafeForyPool(2); fory.register(BeanA.class); diff --git a/java/fory-core/src/test/java/org/apache/fory/builder/CodecUtilsTest.java b/java/fory-core/src/test/java/org/apache/fory/builder/CodecUtilsTest.java index f9af95de99..b6b943f884 100644 --- a/java/fory-core/src/test/java/org/apache/fory/builder/CodecUtilsTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/builder/CodecUtilsTest.java @@ -20,11 +20,14 @@ package org.apache.fory.builder; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertSame; +import static org.testng.Assert.assertTrue; import org.apache.fory.Fory; import org.apache.fory.ForyTestBase; import org.apache.fory.memory.MemoryBuffer; import org.apache.fory.memory.MemoryUtils; +import org.apache.fory.platform.JdkVersion; import org.apache.fory.resolver.TypeResolver; import org.apache.fory.test.bean.BeanA; import org.testng.annotations.Test; @@ -41,6 +44,7 @@ public void loadOrGenObjectCodecClass() throws Exception { .requireClassRegistration(false) .build(); Class seqCodecClass = fory.getTypeResolver().getSerializerClass(BeanA.class); + assertGeneratedClassShape(seqCodecClass, BeanA.class); Generated.GeneratedSerializer serializer = seqCodecClass .asSubclass(Generated.GeneratedSerializer.class) @@ -53,4 +57,15 @@ public void loadOrGenObjectCodecClass() throws Exception { Object obj = ForyTestBase.readSerializer(fory, serializer, MemoryUtils.wrap(bytes)); assertEquals(obj, beanA); } + + private static void assertGeneratedClassShape(Class serializerClass, Class beanClass) + throws Exception { + if (JdkVersion.MAJOR_VERSION >= 25) { + assertTrue((Boolean) Class.class.getMethod("isHidden").invoke(serializerClass)); + assertSame(Class.class.getMethod("getNestHost").invoke(serializerClass), beanClass); + } else { + assertSame( + serializerClass.getClassLoader().loadClass(serializerClass.getName()), serializerClass); + } + } } diff --git a/java/fory-core/src/test/java/org/apache/fory/builder/Jdk25MultiReleaseJarVerifier.java b/java/fory-core/src/test/java/org/apache/fory/builder/Jdk25MultiReleaseJarVerifier.java new file mode 100644 index 0000000000..bf0f9faf7c --- /dev/null +++ b/java/fory-core/src/test/java/org/apache/fory/builder/Jdk25MultiReleaseJarVerifier.java @@ -0,0 +1,173 @@ +/* + * 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.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** Verifies the packaged JDK25 multi-release class graph. */ +public final class Jdk25MultiReleaseJarVerifier { + private static final String VERSION_25_PREFIX = "META-INF/versions/25/"; + private static final String FORY_CLASS_PREFIX = "org/apache/fory/"; + private static final String SHADED_CLASS_PREFIX = "org/apache/fory/shaded/"; + private static final String[] FORBIDDEN_CONSTANTS = {"sun/misc/Unsafe", "sun.misc.Unsafe"}; + private static final String[] REQUIRED_VERSION_25_CLASSES = { + "module-info.class", + "org/apache/fory/memory/LittleEndian.class", + "org/apache/fory/memory/MemoryBuffer.class", + "org/apache/fory/platform/internal/_Lookup.class", + "org/apache/fory/platform/internal/_UnsafeUtils.class", + "org/apache/fory/builder/UnsafeCodegenSupport.class", + "org/apache/fory/reflect/InstanceFieldAccessors.class", + "org/apache/fory/reflect/UnsafeObjectInstantiator.class", + "org/apache/fory/serializer/PlatformStringUtils.class" + }; + + private Jdk25MultiReleaseJarVerifier() {} + + public static void main(String[] args) throws IOException { + if (args.length != 1) { + throw new IllegalArgumentException("Usage: Jdk25MultiReleaseJarVerifier "); + } + verify(Paths.get(args[0])); + } + + static void verify(Path jarPath) throws IOException { + Map rootClasses = new HashMap<>(); + Map version25Classes = new HashMap<>(); + List violations = new ArrayList<>(); + try (JarFile jarFile = new JarFile(jarPath.toFile())) { + Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + if (entry.isDirectory() || !entry.getName().endsWith(".class")) { + continue; + } + String name = entry.getName(); + if (isForyRootClass(name)) { + rootClasses.put(name, readFully(jarFile, entry)); + } else if (name.startsWith(VERSION_25_PREFIX)) { + String className = name.substring(VERSION_25_PREFIX.length()); + if ("module-info.class".equals(className) || isForyRootClass(className)) { + version25Classes.put(className, readFully(jarFile, entry)); + } + } + } + } + + verifyRequiredClasses(rootClasses, version25Classes, violations); + verifyForbiddenConstants(rootClasses, version25Classes, violations); + if (!violations.isEmpty()) { + throw new AssertionError( + "Invalid JDK25 multi-release fory-core jar " + jarPath + ": " + violations); + } + } + + private static boolean isForyRootClass(String name) { + return name.startsWith(FORY_CLASS_PREFIX) && !name.startsWith(SHADED_CLASS_PREFIX); + } + + private static void verifyRequiredClasses( + Map rootClasses, Map version25Classes, List out) { + if (rootClasses.containsKey("org/apache/fory/platform/UnsafeOps.class")) { + out.add("Root UnsafeOps class must not be packaged"); + } + if (version25Classes.containsKey("org/apache/fory/platform/UnsafeOps.class")) { + out.add("JDK25 UnsafeOps class must not be packaged"); + } + for (String requiredClass : REQUIRED_VERSION_25_CLASSES) { + if (!version25Classes.containsKey(requiredClass)) { + out.add("Missing JDK25 multi-release class " + requiredClass); + } + } + } + + private static void verifyForbiddenConstants( + Map rootClasses, Map version25Classes, List out) { + for (Map.Entry entry : rootClasses.entrySet()) { + if (containsForbiddenConstant(entry.getValue()) + && !version25Classes.containsKey(entry.getKey())) { + out.add( + "Root class " + entry.getKey() + " has forbidden constants without JDK25 replacement"); + } + } + Set resolvedClasses = new HashSet<>(rootClasses.keySet()); + resolvedClasses.addAll(version25Classes.keySet()); + resolvedClasses.remove("module-info.class"); + for (String className : resolvedClasses) { + byte[] bytes = + version25Classes.containsKey(className) + ? version25Classes.get(className) + : rootClasses.get(className); + if (containsForbiddenConstant(bytes)) { + out.add("JDK25 resolved class " + className + " has forbidden constants"); + } + } + } + + private static boolean containsForbiddenConstant(byte[] bytes) { + for (String forbiddenConstant : FORBIDDEN_CONSTANTS) { + if (containsAscii(bytes, forbiddenConstant)) { + return true; + } + } + return false; + } + + private static boolean containsAscii(byte[] bytes, String value) { + byte[] target = value.getBytes(StandardCharsets.US_ASCII); + int maxStart = bytes.length - target.length; + for (int i = 0; i <= maxStart; i++) { + int j = 0; + while (j < target.length && bytes[i + j] == target[j]) { + j++; + } + if (j == target.length) { + return true; + } + } + return false; + } + + private static byte[] readFully(JarFile jarFile, JarEntry entry) throws IOException { + try (InputStream inputStream = jarFile.getInputStream(entry)) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + byte[] buffer = new byte[8192]; + int read; + while ((read = inputStream.read(buffer)) >= 0) { + outputStream.write(buffer, 0, read); + } + return outputStream.toByteArray(); + } + } +} diff --git a/java/fory-core/src/test/java/org/apache/fory/builder/Jdk25UnsafeClassGraphTest.java b/java/fory-core/src/test/java/org/apache/fory/builder/Jdk25UnsafeClassGraphTest.java new file mode 100644 index 0000000000..9a3879057d --- /dev/null +++ b/java/fory-core/src/test/java/org/apache/fory/builder/Jdk25UnsafeClassGraphTest.java @@ -0,0 +1,106 @@ +/* + * 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 static org.testng.Assert.assertTrue; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Stream; +import org.testng.annotations.Test; + +public class Jdk25UnsafeClassGraphTest { + private static final Path ROOT_SOURCES = Paths.get("src/main/java/org/apache/fory"); + private static final Path JAVA25_SOURCES = Paths.get("src/main/java25/org/apache/fory"); + private static final Pattern ROOT_UNSAFE_REFERENCE = + Pattern.compile( + "import\\s+sun\\.misc\\.Unsafe|" + + "sun\\.misc\\.Unsafe|" + + "\\bUnsafe\\.class\\b|" + + "_UnsafeUtils\\.UNSAFE|" + + "_JDKAccess\\.UNSAFE|" + + "Class\\.forName\\(\"sun\\.misc\\.Unsafe\"\\)|" + + "TypeRef\\.of\\(Unsafe"); + private static final Pattern JAVA25_UNSAFE_REFERENCE = + Pattern.compile( + "import\\s+sun\\.misc\\.Unsafe|" + + "sun\\.misc\\.Unsafe|" + + "\\bUnsafe\\.class\\b|" + + "_JDKAccess\\.UNSAFE|" + + "TypeRef\\.of\\(Unsafe|" + + "Class\\.forName\\(\"sun\\.misc\\.Unsafe\"\\)"); + + @Test + public void testUnsafeOwner() throws IOException { + List violations = new ArrayList<>(); + try (Stream paths = Files.walk(ROOT_SOURCES)) { + paths + .filter(path -> path.toString().endsWith(".java")) + .forEach( + path -> { + try { + String source = new String(Files.readAllBytes(path), StandardCharsets.UTF_8); + if (!ROOT_UNSAFE_REFERENCE.matcher(source).find()) { + return; + } + Path relative = ROOT_SOURCES.relativize(path); + Path replacement = JAVA25_SOURCES.resolve(relative); + if (!Files.exists(replacement)) { + violations.add(relative.toString().replace('\\', '/')); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + assertTrue( + violations.isEmpty(), + "Root classes that mention Unsafe must have Java 25 replacements: " + violations); + } + + @Test + public void testJava25OwnerIsClean() throws IOException { + List violations = new ArrayList<>(); + try (Stream paths = Files.walk(JAVA25_SOURCES)) { + paths + .filter(path -> path.toString().endsWith(".java")) + .forEach( + path -> { + try { + String source = new String(Files.readAllBytes(path), StandardCharsets.UTF_8); + if (JAVA25_UNSAFE_REFERENCE.matcher(source).find()) { + violations.add(JAVA25_SOURCES.relativize(path).toString().replace('\\', '/')); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + assertTrue( + violations.isEmpty(), + "Java 25 replacements must not reference sun.misc.Unsafe: " + violations); + } +} diff --git a/java/fory-core/src/test/java/org/apache/fory/codegen/JaninoUtilsTest.java b/java/fory-core/src/test/java/org/apache/fory/codegen/JaninoUtilsTest.java index 901a22e0ca..1189ce7631 100644 --- a/java/fory-core/src/test/java/org/apache/fory/codegen/JaninoUtilsTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/codegen/JaninoUtilsTest.java @@ -25,7 +25,6 @@ import java.util.List; import java.util.Map; import java.util.function.Function; -import org.apache.fory.platform.UnsafeOps; import org.apache.fory.reflect.ReflectionUtils; import org.apache.fory.test.bean.Struct; import org.apache.fory.util.ClassLoaderUtils; @@ -254,12 +253,8 @@ public WeakReference> janinoCompileDependentClass(Class de // Compile all sources compiler.compile(sourceFinder.resources().toArray(new Resource[0])); // See https://github.com/janino-compiler/janino/issues/173 - UnsafeOps.putObject( - classLoader, ReflectionUtils.getFieldOffset(classLoader.getClass(), "classLoader"), null); - UnsafeOps.putObject( - classLoader, - ReflectionUtils.getFieldOffset(classLoader.getClass(), "loadedIClasses"), - null); + ReflectionUtils.setObjectFieldValue(classLoader, "classLoader", null); + ReflectionUtils.setObjectFieldValue(classLoader, "loadedIClasses", null); byte[] byteCodes = classes.entrySet().iterator().next().getValue(); ClassLoaderUtils.tryDefineClassesInClassLoader("B", dep, dep.getClassLoader(), byteCodes); Class cls = dep.getClassLoader().loadClass("B"); diff --git a/java/fory-core/src/test/java/org/apache/fory/codegen/pkgprivate/PackagePrivateMapKeyTest.java b/java/fory-core/src/test/java/org/apache/fory/codegen/pkgprivate/PackagePrivateMapKeyTest.java index 62dd2bc6ea..4124695b6b 100644 --- a/java/fory-core/src/test/java/org/apache/fory/codegen/pkgprivate/PackagePrivateMapKeyTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/codegen/pkgprivate/PackagePrivateMapKeyTest.java @@ -25,7 +25,6 @@ import java.util.EnumMap; import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import org.apache.fory.Fory; @@ -48,7 +47,6 @@ public void testCodegenForMapWithPackagePrivateEnumKey() { ReproNode parent = new ReproNode(ReproType.TYPE_A, "p1"); ReproNode child = new ReproNode(ReproType.TYPE_B, "c1"); parent.children.add(child); - child.parents.computeIfAbsent(parent.type, k -> new LinkedHashSet<>()).add(parent); container.nodes.computeIfAbsent(ReproType.TYPE_A, k -> new HashMap<>()).put("p1", parent); container.nodes.computeIfAbsent(ReproType.TYPE_B, k -> new HashMap<>()).put("c1", child); @@ -68,20 +66,32 @@ enum ReproType implements Serializable { class ReproNode implements Serializable { final ReproType type; final String id; - final Set children = new HashSet<>(); - final Map> parents = new EnumMap<>(ReproType.class); + Set children; + Map> parents; ReproNode(ReproType type, String id) { + this(type, id, new HashSet<>(), new EnumMap<>(ReproType.class)); + } + + ReproNode( + ReproType type, String id, Set children, Map> parents) { this.type = type; this.id = id; + this.children = children; + this.parents = parents; } } class ReproContainer implements Serializable { - final Map> nodes = new EnumMap<>(ReproType.class); + final Map> nodes; final String version; ReproContainer(String version) { + this(new EnumMap<>(ReproType.class), version); + } + + ReproContainer(Map> nodes, String version) { + this.nodes = nodes; this.version = version; } } diff --git a/java/fory-core/src/test/java/org/apache/fory/collection/MultiKeyWeakMapTest.java b/java/fory-core/src/test/java/org/apache/fory/collection/MultiKeyWeakMapTest.java index 5df82373cf..8dc2cba0da 100644 --- a/java/fory-core/src/test/java/org/apache/fory/collection/MultiKeyWeakMapTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/collection/MultiKeyWeakMapTest.java @@ -24,7 +24,7 @@ import java.util.Set; import org.apache.fory.logging.Logger; import org.apache.fory.logging.LoggerFactory; -import org.apache.fory.util.unsafe._JDKAccess; +import org.apache.fory.platform.internal._JDKAccess; import org.testng.SkipException; import org.testng.annotations.Test; diff --git a/java/fory-core/src/test/java/org/apache/fory/config/ForyBuilderTest.java b/java/fory-core/src/test/java/org/apache/fory/config/ForyBuilderTest.java index c72cfd02a6..6b552966c5 100644 --- a/java/fory-core/src/test/java/org/apache/fory/config/ForyBuilderTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/config/ForyBuilderTest.java @@ -24,11 +24,11 @@ import static org.testng.Assert.assertTrue; import java.io.ByteArrayOutputStream; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import org.apache.fory.Fory; +import org.apache.fory.TestUtils; import org.apache.fory.ThreadSafeFory; import org.apache.fory.meta.MetaCompressor; import org.testng.Assert; @@ -155,15 +155,12 @@ public void testCodegenDefaultsOnOrdinaryJvm() { @Test public void testGraalvmRuntimeForcesCodegenOff() throws Exception { - String javaBin = - System.getProperty("java.home") + File.separator + "bin" + File.separator + "java"; Process process = new ProcessBuilder( - javaBin, - "-Dorg.graalvm.nativeimage.imagecode=runtime", - "-cp", - System.getProperty("java.class.path"), - GraalvmCodegenConfigMain.class.getName()) + TestUtils.javaCommand( + System.getProperty("java.class.path"), + GraalvmCodegenConfigMain.class, + "-Dorg.graalvm.nativeimage.imagecode=runtime")) .redirectErrorStream(true) .start(); String output = readFully(process.getInputStream()); diff --git a/java/fory-core/src/test/java/org/apache/fory/memory/MemoryBufferTest.java b/java/fory-core/src/test/java/org/apache/fory/memory/MemoryBufferTest.java index 0b9e41b79e..a7f2f062f0 100644 --- a/java/fory-core/src/test/java/org/apache/fory/memory/MemoryBufferTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/memory/MemoryBufferTest.java @@ -30,8 +30,11 @@ import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.Random; +import org.apache.fory.TestUtils; import org.apache.fory.platform.AndroidSupport; +import org.apache.fory.platform.JdkVersion; import org.testng.Assert; +import org.testng.SkipException; import org.testng.annotations.Test; public class MemoryBufferTest { @@ -81,6 +84,29 @@ public void testBufferWrite() { assertEquals(buffer.readerIndex(), buffer.writerIndex()); } + @Test + public void testDirectBufferRejectsHeap() { + assertThrows( + IllegalArgumentException.class, + () -> MemoryBuffer.fromDirectByteBuffer(ByteBuffer.allocate(8), 8, null)); + } + + @Test + public void testDirectByteBufferNoNioOpen() throws Exception { + ProcessBuilder processBuilder = + new ProcessBuilder(TestUtils.javaCommand(DirectByteBufferNoNioOpenProbe.class)) + .redirectErrorStream(true); + for (String commandPart : processBuilder.command()) { + assertTrue(!commandPart.contains("java.base/java.nio"), processBuilder.command().toString()); + } + processBuilder.environment().remove("JDK_JAVA_OPTIONS"); + processBuilder.environment().remove("JAVA_TOOL_OPTIONS"); + processBuilder.environment().remove("_JAVA_OPTIONS"); + Process process = processBuilder.start(); + String output = readFully(process.getInputStream()); + assertEquals(process.waitFor(), 0, output); + } + @Test public void testAndroidHeapMemoryBufferPaths() throws Exception { String javaBin = @@ -182,10 +208,11 @@ public static void main(String[] args) { check(source.equalTo(target, 0, 0, 4), true); check(source.getBytes(1, 2), new byte[] {2, 3}); - assertThrows( - UnsupportedOperationException.class, () -> source.copyToUnsafe(0, new byte[4], 0, 4)); - assertThrows( - UnsupportedOperationException.class, () -> target.copyFromUnsafe(0, new byte[4], 0, 4)); + byte[] bytes = new byte[4]; + source.copyToByteArray(0, bytes, 0, 4); + check(bytes, new byte[] {1, 2, 3, 4}); + target.copyFromByteArray(0, new byte[] {4, 3, 2, 1}, 0, 4); + check(target.getBytes(0, 4), new byte[] {4, 3, 2, 1}); } private static void check(boolean actual, boolean expected) { @@ -247,6 +274,67 @@ private static void check(byte[] actual, byte[] expected) { } } + public static final class DirectByteBufferNoNioOpenProbe { + public static void main(String[] args) { + if (JdkVersion.MAJOR_VERSION >= 25) { + for (String inputArg : + java.lang.management.ManagementFactory.getRuntimeMXBean().getInputArguments()) { + if (inputArg.contains("java.base/java.nio")) { + throw new AssertionError("Unexpected java.nio open: " + inputArg); + } + } + if (isNioOpenToProbe()) { + throw new AssertionError("java.base/java.nio must not be open to this test probe"); + } + } + MemoryBuffer buffer = MemoryUtils.wrap(ByteBuffer.allocateDirect(128)); + buffer.writeInt32(17); + buffer.writeInt64(19); + checkEqual(buffer.readInt32(), 17); + checkEqual(buffer.readInt64(), 19L); + + int[] ints = new int[] {1, 2, 3, 4}; + buffer.writerIndex(0); + buffer.copyFromIntArray(0, ints, 0, ints.length * Integer.BYTES); + int[] readInts = new int[ints.length]; + buffer.copyToIntArray(0, readInts, 0, readInts.length * Integer.BYTES); + if (!java.util.Arrays.equals(readInts, ints)) { + throw new AssertionError( + "Expected " + + java.util.Arrays.toString(ints) + + " but got " + + java.util.Arrays.toString(readInts)); + } + } + + private static void checkEqual(int actual, int expected) { + if (actual != expected) { + throw new AssertionError("Expected " + expected + " but got " + actual); + } + } + + private static void checkEqual(long actual, long expected) { + if (actual != expected) { + throw new AssertionError("Expected " + expected + " but got " + actual); + } + } + + private static boolean isNioOpenToProbe() { + try { + Class moduleType = Class.forName("java.lang.Module"); + java.lang.reflect.Method getModule = Class.class.getMethod("getModule"); + Object byteBufferModule = getModule.invoke(ByteBuffer.class); + Object probeModule = getModule.invoke(DirectByteBufferNoNioOpenProbe.class); + java.lang.reflect.Method isOpen = moduleType.getMethod("isOpen", String.class, moduleType); + return (Boolean) isOpen.invoke(byteBufferModule, "java.nio", probeModule); + } catch (ClassNotFoundException | NoSuchMethodException e) { + return false; + } catch (ReflectiveOperationException e) { + throw new AssertionError("Failed to inspect java.nio module opens", e); + } + } + } + @Test public void testBufferUnsafeWrite() { { @@ -302,6 +390,17 @@ public void testWrapBuffer() { } } + @Test + public void testJdk25DirectBufferNoRawAddress() { + if (JdkVersion.MAJOR_VERSION < 25) { + throw new SkipException("Skip on jdk" + JdkVersion.MAJOR_VERSION); + } + MemoryBuffer buffer = MemoryUtils.wrap(ByteBuffer.allocateDirect(8)); + buffer.writeByte((byte) 1); + assertThrows(UnsupportedOperationException.class, () -> buffer.getUnsafeReaderAddress()); + assertThrows(UnsupportedOperationException.class, () -> buffer._unsafeWriterAddress()); + } + @Test public void testSliceAsByteBuffer() { byte[] data = new byte[10]; @@ -340,8 +439,11 @@ public void testEqualTo() { buf1.putByte(9, (byte) 1); buf2.putByte(9, (byte) 1); Assert.assertTrue(buf1.equalTo(buf2, 0, 0, buf1.size())); + Assert.assertTrue(buf1.equalTo(buf2, 1, 1, 9)); + Assert.assertTrue(buf1.equalTo(new byte[] {0, 0, 0, 0, 0, 0, 0, 0, 1}, 0, 1, 9)); buf1.putByte(9, (byte) 2); Assert.assertFalse(buf1.equalTo(buf2, 0, 0, buf1.size())); + Assert.assertFalse(buf1.equalTo(buf2, 1, 1, 9)); } @Test @@ -351,6 +453,112 @@ public void testEqualToZeroSize() { Assert.assertTrue(buf1.equalTo(buf2, 0, 0, buf1.size())); } + @Test + public void testDirectCopyTo() { + byte[] values = new byte[16]; + for (int i = 0; i < values.length; i++) { + values[i] = (byte) i; + } + MemoryBuffer source = MemoryUtils.wrap(ByteBuffer.allocateDirect(values.length)); + source.writeBytes(values); + MemoryBuffer directTarget = MemoryUtils.wrap(ByteBuffer.allocateDirect(values.length)); + source.copyTo(0, directTarget, 0, values.length); + assertEquals(directTarget.getBytes(0, values.length), values); + + MemoryBuffer heapTarget = MemoryUtils.buffer(values.length); + source.copyTo(0, heapTarget, 0, values.length); + assertEquals(heapTarget.getBytes(0, values.length), values); + + MemoryBuffer heapSource = MemoryUtils.wrap(values); + MemoryBuffer directFromHeap = MemoryUtils.wrap(ByteBuffer.allocateDirect(values.length)); + heapSource.copyTo(0, directFromHeap, 0, values.length); + assertEquals(directFromHeap.getBytes(0, values.length), values); + + source.copyTo(0, source, 4, 8); + assertEquals(source.getBytes(4, 8), new byte[] {0, 1, 2, 3, 4, 5, 6, 7}); + } + + @Test + public void testDirectPrimitiveArrays() { + MemoryBuffer direct = MemoryUtils.wrap(ByteBuffer.allocateDirect(64)); + int[] ints = {1, -2, 3, Integer.MIN_VALUE}; + long[] longs = {4L, -5L, Long.MAX_VALUE}; + direct.writeInts(ints); + direct.writeLongs(longs); + + int[] readInts = new int[ints.length]; + long[] readLongs = new long[longs.length]; + direct.readerIndex(0); + direct.readInts(readInts, 0, readInts.length); + direct.readLongs(readLongs, 0, readLongs.length); + assertEquals(readInts, ints); + assertEquals(readLongs, longs); + } + + @Test + public void testTypedArrayCopies() { + assertTypedArrayCopies(MemoryUtils.buffer(256)); + assertTypedArrayCopies(MemoryUtils.wrap(ByteBuffer.allocateDirect(256))); + } + + private void assertTypedArrayCopies(MemoryBuffer buffer) { + byte[] bytes = {1, 2, 3, 4}; + int byteOffset = buffer.writerIndex(); + buffer.writeBytes(bytes); + byte[] byteCopy = new byte[bytes.length]; + buffer.copyToByteArray(byteOffset, byteCopy, 0, bytes.length); + assertEquals(byteCopy, bytes); + + boolean[] booleans = {true, false, true}; + int booleanOffset = buffer.writerIndex(); + buffer.writeBooleans(booleans); + boolean[] booleanCopy = new boolean[booleans.length]; + buffer.copyToBooleanArray(booleanOffset, booleanCopy, 0, booleans.length); + assertEquals(booleanCopy, booleans); + + char[] chars = {'a', 0x1234, Character.MAX_VALUE}; + int charOffset = buffer.writerIndex(); + buffer.writeChars(chars); + char[] charCopy = new char[chars.length]; + buffer.copyToCharArray(charOffset, charCopy, 0, chars.length * Character.BYTES); + assertEquals(charCopy, chars); + + short[] shorts = {1, -2, Short.MAX_VALUE}; + int shortOffset = buffer.writerIndex(); + buffer.writeShorts(shorts); + short[] shortCopy = new short[shorts.length]; + buffer.copyToShortArray(shortOffset, shortCopy, 0, shorts.length * Short.BYTES); + assertEquals(shortCopy, shorts); + + int[] ints = {1, -2, Integer.MIN_VALUE}; + int intOffset = buffer.writerIndex(); + buffer.writeInts(ints); + int[] intCopy = new int[ints.length]; + buffer.copyToIntArray(intOffset, intCopy, 0, ints.length * Integer.BYTES); + assertEquals(intCopy, ints); + + long[] longs = {1L, -2L, Long.MAX_VALUE}; + int longOffset = buffer.writerIndex(); + buffer.writeLongs(longs); + long[] longCopy = new long[longs.length]; + buffer.copyToLongArray(longOffset, longCopy, 0, longs.length * Long.BYTES); + assertEquals(longCopy, longs); + + float[] floats = {1.5f, -2.5f, Float.MAX_VALUE}; + int floatOffset = buffer.writerIndex(); + buffer.writeFloats(floats); + float[] floatCopy = new float[floats.length]; + buffer.copyToFloatArray(floatOffset, floatCopy, 0, floats.length * Float.BYTES); + assertEquals(floatCopy, floats); + + double[] doubles = {1.5d, -2.5d, Double.MAX_VALUE}; + int doubleOffset = buffer.writerIndex(); + buffer.writeDoubles(doubles); + double[] doubleCopy = new double[doubles.length]; + buffer.copyToDoubleArray(doubleOffset, doubleCopy, 0, doubles.length * Double.BYTES); + assertEquals(doubleCopy, doubles); + } + @Test public void testWritePrimitiveArrayWithSizeEmbedded() { MemoryBuffer buf = MemoryUtils.buffer(16); @@ -404,7 +612,7 @@ public void testWriteVarUInt32() { } @Test - public void testReadVarUInt32RejectsMalformedFifthByte() { + public void testReadVarUInt32RejectsFifthByte() { byte[] malformed = new byte[] {(byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, 0x10}; assertThrows(IllegalArgumentException.class, () -> MemoryUtils.wrap(malformed).readVarUInt32()); assertThrows( diff --git a/java/fory-core/src/test/java/org/apache/fory/meta/TypeDefTest.java b/java/fory-core/src/test/java/org/apache/fory/meta/TypeDefTest.java index 19e5378c3f..0b79050727 100644 --- a/java/fory-core/src/test/java/org/apache/fory/meta/TypeDefTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/meta/TypeDefTest.java @@ -25,13 +25,10 @@ import com.google.common.collect.ImmutableList; import java.lang.reflect.Field; -import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.TreeSet; import org.apache.fory.Fory; import org.apache.fory.ForyTestBase; import org.apache.fory.annotation.ForyField; @@ -85,17 +82,6 @@ static class ContainerClass extends TestFieldsOrderClass1 { private Map map3; } - @Test - public void testFieldsOrder() { - List fieldList = new ArrayList<>(); - Collections.addAll(fieldList, TestFieldsOrderClass1.class.getDeclaredFields()); - Collections.addAll(fieldList, TestFieldsOrderClass2.class.getDeclaredFields()); - TreeSet sorted = new TreeSet<>(TypeDef.FIELD_COMPARATOR); - sorted.addAll(fieldList); - assertEquals(fieldList.size(), sorted.size()); - fieldList.sort(TypeDef.FIELD_COMPARATOR); - } - @Test public void testTypeDefSerialization() throws NoSuchFieldException { Fory fory = Fory.builder().withXlang(false).withMetaShare(true).build(); diff --git a/java/fory-core/src/test/java/org/apache/fory/util/unsafe/DefineClassTest.java b/java/fory-core/src/test/java/org/apache/fory/platform/internal/DefineClassTest.java similarity index 73% rename from java/fory-core/src/test/java/org/apache/fory/util/unsafe/DefineClassTest.java rename to java/fory-core/src/test/java/org/apache/fory/platform/internal/DefineClassTest.java index c974bdd2b9..ec4c7da0f5 100644 --- a/java/fory-core/src/test/java/org/apache/fory/util/unsafe/DefineClassTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/platform/internal/DefineClassTest.java @@ -17,7 +17,7 @@ * under the License. */ -package org.apache.fory.util.unsafe; +package org.apache.fory.platform.internal; import java.util.Collections; import org.apache.fory.codegen.CompileUnit; @@ -69,4 +69,32 @@ public void testDefineClass() throws ClassNotFoundException { className, null, DefineClassTest.class.getClassLoader(), null, bytecodes); } } + + @Test + public void testDefineHiddenNestmate() throws Exception { + if (JdkVersion.MAJOR_VERSION < 15) { + return; + } + String pkg = DefineClassTest.class.getPackage().getName(); + CompileUnit unit = + new CompileUnit( + pkg, + "HiddenNestmateA", + ("package " + + pkg + + ";\n" + + "public class HiddenNestmateA {\n" + + " public static String hello() { return \"HIDDEN\"; }\n" + + "}")); + byte[] bytecodes = + JaninoUtils.toBytecode(Thread.currentThread().getContextClassLoader(), unit) + .values() + .iterator() + .next(); + + Class clz = DefineClass.defineHiddenNestmate(DefineClassTest.class, bytecodes); + + Assert.assertEquals(clz.getMethod("hello").invoke(null), "HIDDEN"); + Assert.assertEquals(Class.class.getMethod("getNestHost").invoke(clz), DefineClassTest.class); + } } diff --git a/java/fory-core/src/test/java/org/apache/fory/util/unsafe/JDKAccessTest.java b/java/fory-core/src/test/java/org/apache/fory/platform/internal/JDKAccessTest.java similarity index 99% rename from java/fory-core/src/test/java/org/apache/fory/util/unsafe/JDKAccessTest.java rename to java/fory-core/src/test/java/org/apache/fory/platform/internal/JDKAccessTest.java index a6565c0972..5b4a902b68 100644 --- a/java/fory-core/src/test/java/org/apache/fory/util/unsafe/JDKAccessTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/platform/internal/JDKAccessTest.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; diff --git a/java/fory-core/src/test/java/org/apache/fory/reflect/FieldAccessorTest.java b/java/fory-core/src/test/java/org/apache/fory/reflect/FieldAccessorTest.java index 32d1d207ad..14778e4d94 100644 --- a/java/fory-core/src/test/java/org/apache/fory/reflect/FieldAccessorTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/reflect/FieldAccessorTest.java @@ -20,14 +20,14 @@ package org.apache.fory.reflect; import java.io.ByteArrayOutputStream; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Field; import java.nio.charset.StandardCharsets; import lombok.AllArgsConstructor; +import org.apache.fory.TestUtils; import org.apache.fory.platform.AndroidSupport; -import org.apache.fory.reflect.FieldAccessor.GeneratedAccessor; +import org.apache.fory.reflect.InstanceFieldAccessors.GeneratedAccessor; import org.testng.Assert; import org.testng.annotations.Test; @@ -46,26 +46,63 @@ public void testGeneratedAccessor() throws Exception { Assert.assertEquals(f1.get(struct), 10); f1.set(struct, 20); Assert.assertEquals(f1.get(struct), 20); + Assert.assertEquals(f1.getInt(struct), 20); + f1.putInt(struct, 30); + Assert.assertEquals(f1.getInt(struct), 30); GeneratedAccessor f2 = new GeneratedAccessor(TestStruct.class.getDeclaredField("f2")); Assert.assertEquals(f2.get(struct), true); f2.set(struct, false); Assert.assertEquals(f2.get(struct), false); + Assert.assertFalse(f2.getBoolean(struct)); + f2.putBoolean(struct, true); + Assert.assertTrue(f2.getBoolean(struct)); GeneratedAccessor f3 = new GeneratedAccessor(TestStruct.class.getDeclaredField("f3")); Assert.assertEquals(f3.get(struct), "str"); f3.set(struct, "a"); Assert.assertEquals(f3.get(struct), "a"); + Assert.assertEquals(f3.getObject(struct), "a"); + f3.putObject(struct, "b"); + Assert.assertEquals(f3.getObject(struct), "b"); + } + + @Test + public void testHiddenAccessor() throws Exception { + HiddenFields fields = new HiddenFields(); + FieldAccessor intAccessor = + FieldAccessor.createAccessor(HiddenFields.class.getDeclaredField("i")); + Assert.assertEquals(intAccessor.getInt(fields), 1); + intAccessor.putInt(fields, 2); + Assert.assertEquals(intAccessor.getInt(fields), 2); + + FieldAccessor objectAccessor = + FieldAccessor.createAccessor(HiddenFields.class.getDeclaredField("text")); + Assert.assertEquals(objectAccessor.getObject(fields), "a"); + objectAccessor.putObject(fields, "b"); + Assert.assertEquals(objectAccessor.getObject(fields), "b"); + + FieldAccessor finalAccessor = + FieldAccessor.createAccessor(HiddenFields.class.getDeclaredField("finalValue")); + Assert.assertEquals(finalAccessor.getLong(fields), 3L); + } + + @Test + public void testFinalFieldWrites() throws Exception { + FinalFields fields = new FinalFields(1, "a"); + FieldAccessor intAccessor = + FieldAccessor.createAccessor(FinalFields.class.getDeclaredField("intValue")); + intAccessor.putInt(fields, 2); + Assert.assertEquals(intAccessor.getInt(fields), 2); + + FieldAccessor objectAccessor = + FieldAccessor.createAccessor(FinalFields.class.getDeclaredField("objectValue")); + objectAccessor.putObject(fields, "b"); + Assert.assertEquals(objectAccessor.getObject(fields), "b"); } @Test public void testAndroidReflectionFieldAccessorPaths() throws Exception { - String javaBin = - System.getProperty("java.home") + File.separator + "bin" + File.separator + "java"; Process process = - new ProcessBuilder( - javaBin, - "-cp", - System.getProperty("java.class.path"), - AndroidReflectionFieldAccessorProbe.class.getName()) + new ProcessBuilder(TestUtils.javaCommand(AndroidReflectionFieldAccessorProbe.class)) .redirectErrorStream(true) .start(); String output = readFully(process.getInputStream()); @@ -106,8 +143,8 @@ private static void assertAccessor( Field field = AndroidFields.class.getDeclaredField(fieldName); FieldAccessor accessor = FieldAccessor.createAccessor(field); check( - accessor instanceof ReflectionFieldAccessor, "Expected reflection accessor for " + field); - check(accessor.getFieldOffset() == -1, "Android field accessor should not expose offsets"); + accessor.getClass().getEnclosingClass() == InstanceFieldAccessors.class, + "Expected instance accessor owner for " + field); checkEquals(accessor.get(fields), expected, "initial " + fieldName); accessor.set(fields, replacement); checkEquals(accessor.get(fields), replacement, "updated " + fieldName); @@ -137,4 +174,20 @@ private static final class AndroidFields { private double doubleValue = 3.5d; private Object objectValue = "before"; } + + private static final class HiddenFields { + private int i = 1; + private String text = "a"; + private final long finalValue = 3; + } + + private static final class FinalFields { + private final int intValue; + private final Object objectValue; + + private FinalFields(int value, Object object) { + intValue = value; + objectValue = object; + } + } } diff --git a/java/fory-core/src/test/java/org/apache/fory/reflect/ObjectCreatorsTest.java b/java/fory-core/src/test/java/org/apache/fory/reflect/ObjectInstantiatorsTest.java similarity index 57% rename from java/fory-core/src/test/java/org/apache/fory/reflect/ObjectCreatorsTest.java rename to java/fory-core/src/test/java/org/apache/fory/reflect/ObjectInstantiatorsTest.java index 8862cf421c..3153913503 100644 --- a/java/fory-core/src/test/java/org/apache/fory/reflect/ObjectCreatorsTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/reflect/ObjectInstantiatorsTest.java @@ -20,19 +20,19 @@ package org.apache.fory.reflect; import java.io.ByteArrayOutputStream; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.concurrent.ArrayBlockingQueue; +import org.apache.fory.TestUtils; import org.apache.fory.exception.ForyException; import org.apache.fory.platform.AndroidSupport; -import org.apache.fory.reflect.ObjectCreators.ParentNoArgCtrObjectCreator; +import org.apache.fory.reflect.ObjectInstantiators.ReflectionFactoryInstantiator; import org.testng.Assert; import org.testng.annotations.Test; @SuppressWarnings("rawtypes") -public class ObjectCreatorsTest { +public class ObjectInstantiatorsTest { static class NoCtrTestClass { int f1; @@ -42,26 +42,51 @@ public NoCtrTestClass(int f1) { } } + static class NonSerializableParentWithoutNoArg { + static int constructorCalls; + int parentValue; + + NonSerializableParentWithoutNoArg(int parentValue) { + constructorCalls++; + this.parentValue = parentValue; + } + } + + static class NonSerializableChildWithoutNoArg extends NonSerializableParentWithoutNoArg { + int childValue; + + NonSerializableChildWithoutNoArg(int parentValue, int childValue) { + super(parentValue); + this.childValue = childValue; + } + } + @Test - public void testObjectCreator() { - ParentNoArgCtrObjectCreator creator = - new ParentNoArgCtrObjectCreator<>(ArrayBlockingQueue.class); - Assert.assertEquals(creator.newInstance().getClass(), ArrayBlockingQueue.class); + public void testObjectInstantiator() { + ReflectionFactoryInstantiator instantiator = + new ReflectionFactoryInstantiator<>(ArrayBlockingQueue.class); + Assert.assertEquals(instantiator.newInstance().getClass(), ArrayBlockingQueue.class); Assert.assertEquals( - new ParentNoArgCtrObjectCreator<>(NoCtrTestClass.class).newInstance().getClass(), + new ReflectionFactoryInstantiator<>(NoCtrTestClass.class).newInstance().getClass(), NoCtrTestClass.class); } @Test - public void testAndroidObjectCreators() throws Exception { - String javaBin = - System.getProperty("java.home") + File.separator + "bin" + File.separator + "java"; + public void testNonSerializableInstantiator() { + NonSerializableParentWithoutNoArg.constructorCalls = 0; + ReflectionFactoryInstantiator instantiator = + new ReflectionFactoryInstantiator<>(NonSerializableChildWithoutNoArg.class); + NonSerializableChildWithoutNoArg instance = instantiator.newInstance(); + Assert.assertEquals(instance.getClass(), NonSerializableChildWithoutNoArg.class); + Assert.assertEquals(NonSerializableParentWithoutNoArg.constructorCalls, 0); + Assert.assertEquals(instance.parentValue, 0); + Assert.assertEquals(instance.childValue, 0); + } + + @Test + public void testAndroidObjectInstantiators() throws Exception { Process process = - new ProcessBuilder( - javaBin, - "-cp", - System.getProperty("java.class.path"), - AndroidObjectCreatorProbe.class.getName()) + new ProcessBuilder(TestUtils.javaCommand(AndroidObjectInstantiatorProbe.class)) .redirectErrorStream(true) .start(); String output = readFully(process.getInputStream()); @@ -78,22 +103,22 @@ private static String readFully(InputStream inputStream) throws IOException { return new String(outputStream.toByteArray(), StandardCharsets.UTF_8); } - public static final class AndroidObjectCreatorProbe { + public static final class AndroidObjectInstantiatorProbe { public static void main(String[] args) { System.setProperty("java.vm.name", "Dalvik"); System.setProperty("java.runtime.name", "Android Runtime"); check(AndroidSupport.IS_ANDROID, "AndroidSupport should detect Dalvik runtime"); - ObjectCreator creator = - ObjectCreators.getObjectCreator(AndroidPrivateNoArg.class); - AndroidPrivateNoArg instance = creator.newInstance(); + ObjectInstantiator instantiator = + ObjectInstantiators.getObjectInstantiator(AndroidPrivateNoArg.class); + AndroidPrivateNoArg instance = instantiator.newInstance(); check(instance.value == 7, "Android reflective constructor should initialize fields"); - ObjectCreator unsupported = - ObjectCreators.getObjectCreator(AndroidNoNoArg.class); + ObjectInstantiator unsupported = + ObjectInstantiators.getObjectInstantiator(AndroidNoNoArg.class); try { unsupported.newInstance(); - throw new AssertionError("Android creator without no-arg constructor should fail"); + throw new AssertionError("Android instantiator without no-arg constructor should fail"); } catch (ForyException expected) { check( expected.getMessage().contains("without an accessible no-arg constructor"), diff --git a/java/fory-core/src/test/java/org/apache/fory/reflect/ReflectionUtilsTest.java b/java/fory-core/src/test/java/org/apache/fory/reflect/ReflectionUtilsTest.java index 64170f72f7..641659ce3d 100644 --- a/java/fory-core/src/test/java/org/apache/fory/reflect/ReflectionUtilsTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/reflect/ReflectionUtilsTest.java @@ -113,19 +113,8 @@ public NoArgConstructor1(int f1) { } @Test - public void testGetNoArgConstructor() throws Throwable { + public void testGetNoArgConstructor() { Constructor ctr = ReflectionUtils.getNoArgConstructor(NoArgConstructor1.class); Assert.assertNull(ctr); - - // ctr is generated by jdk.internal.reflect.ReflectionFactory.generateConstructor, - // `unreflectConstructor` doesn't work, see ParentNoArgCtrObjectCreator to get parent no-arg - // ctr. - // MethodHandle handle = lookup.unreflectConstructor(ctr); - // System.out.println(ctr); - // System.out.println(handle); - // org.apache.fory.reflect.ReflectionUtilsTest$NoArgConstructor1@4d339552 - // System.out.println(ctr.newInstance()); - // java.lang.Object@f0f2775 - // System.out.println(handle.invoke()); } } diff --git a/java/fory-core/src/test/java/org/apache/fory/reflect/UnsafeOpsTest.java b/java/fory-core/src/test/java/org/apache/fory/reflect/UnsafeOpsTest.java deleted file mode 100644 index 804cf6a86b..0000000000 --- a/java/fory-core/src/test/java/org/apache/fory/reflect/UnsafeOpsTest.java +++ /dev/null @@ -1,93 +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 static org.testng.Assert.assertTrue; - -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import org.apache.fory.platform.UnsafeOps; -import org.testng.annotations.Test; - -public class UnsafeOpsTest { - @Test - public void testArrayEquals() { - byte[] bytes = "123456781234567".getBytes(StandardCharsets.UTF_8); - byte[] bytes2 = "123456781234567".getBytes(StandardCharsets.UTF_8); - assert bytes.length == bytes2.length; - assertTrue( - UnsafeOps.arrayEquals( - bytes, UnsafeOps.BYTE_ARRAY_OFFSET, bytes2, UnsafeOps.BYTE_ARRAY_OFFSET, bytes.length)); - } - - @Test(enabled = false) - public void benchmarkArrayEquals() { - byte[] bytes = "123456781234567".getBytes(StandardCharsets.UTF_8); - byte[] bytes2 = "123456781234567".getBytes(StandardCharsets.UTF_8); - arrayEquals(bytes, bytes2); - bytes = "1234567812345678".getBytes(StandardCharsets.UTF_8); - bytes2 = "1234567812345678".getBytes(StandardCharsets.UTF_8); - arrayEquals(bytes, bytes2); - } - - private boolean arrayEquals(byte[] bytes, byte[] bytes2) { - long nums = 200_000_000; - boolean eq = false; - { - // warm - for (int i = 0; i < nums; i++) { - eq = - bytes.length == bytes2.length - && UnsafeOps.arrayEquals( - bytes, - UnsafeOps.BYTE_ARRAY_OFFSET, - bytes2, - UnsafeOps.BYTE_ARRAY_OFFSET, - bytes.length); - } - long t = System.nanoTime(); - for (int i = 0; i < nums; i++) { - eq = - bytes.length == bytes2.length - && UnsafeOps.arrayEquals( - bytes, - UnsafeOps.BYTE_ARRAY_OFFSET, - bytes2, - UnsafeOps.BYTE_ARRAY_OFFSET, - bytes.length); - } - long duration = System.nanoTime() - t; - System.out.format("native cost %sns %sms\n", duration, duration / 1000_000); - } - { - // warm - for (int i = 0; i < nums; i++) { - eq = Arrays.equals(bytes, bytes2); - } - long t = System.nanoTime(); - for (int i = 0; i < nums; i++) { - eq = Arrays.equals(bytes, bytes2); - } - long duration = System.nanoTime() - t; - System.out.format("Arrays.equals cost %sns %sms\n", duration, duration / 1000_000); - } - return eq; - } -} diff --git a/java/fory-core/src/test/java/org/apache/fory/resolver/DisallowedListTest.java b/java/fory-core/src/test/java/org/apache/fory/resolver/DisallowedListTest.java index f30ede9db6..6c90705cb7 100644 --- a/java/fory-core/src/test/java/org/apache/fory/resolver/DisallowedListTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/resolver/DisallowedListTest.java @@ -19,12 +19,12 @@ package org.apache.fory.resolver; +import java.beans.Expression; import java.rmi.server.UnicastRemoteObject; import java.util.Set; import org.apache.fory.Fory; import org.apache.fory.ForyTestBase; import org.apache.fory.exception.InsecureException; -import org.apache.fory.platform.UnsafeOps; import org.testng.Assert; import org.testng.annotations.Test; @@ -85,7 +85,7 @@ public void testSerializeDisallowedClass() { .build(); if (requireClassRegistration) { // Registered or unregistered Classes should be subject to disallowed list restrictions. - fory.register(UnicastRemoteObject.class); + fory.register(Expression.class); } allFory[i] = fory; } @@ -93,7 +93,7 @@ public void testSerializeDisallowedClass() { for (Fory fory : allFory) { Assert.assertThrows( InsecureException.class, - () -> fory.serialize(UnsafeOps.newInstance(UnicastRemoteObject.class))); + () -> fory.serialize(new Expression(System.class, "exit", new Object[] {0}))); serDe(fory, new String[] {"a", "b"}); } } diff --git a/java/fory-core/src/test/java/org/apache/fory/resolver/GraalvmRuntimeArrayTest.java b/java/fory-core/src/test/java/org/apache/fory/resolver/GraalvmRuntimeArrayTest.java index 023796ea1e..94270d60bf 100644 --- a/java/fory-core/src/test/java/org/apache/fory/resolver/GraalvmRuntimeArrayTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/resolver/GraalvmRuntimeArrayTest.java @@ -22,26 +22,23 @@ import static org.testng.Assert.assertEquals; import java.io.ByteArrayOutputStream; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import org.apache.fory.Fory; +import org.apache.fory.TestUtils; import org.apache.fory.serializer.ArraySerializers; import org.testng.annotations.Test; public class GraalvmRuntimeArrayTest { @Test public void testGraalvmRuntimeFallsBackForUnregisteredArrayClass() throws Exception { - String javaBin = - System.getProperty("java.home") + File.separator + "bin" + File.separator + "java"; Process process = new ProcessBuilder( - javaBin, - "-Dorg.graalvm.nativeimage.imagecode=runtime", - "-cp", - System.getProperty("java.class.path"), - GraalvmRuntimeArrayMain.class.getName()) + TestUtils.javaCommand( + System.getProperty("java.class.path"), + GraalvmRuntimeArrayMain.class, + "-Dorg.graalvm.nativeimage.imagecode=runtime")) .redirectErrorStream(true) .start(); String output = readFully(process.getInputStream()); diff --git a/java/fory-core/src/test/java/org/apache/fory/serializer/AndroidDynamicFeatureTest.java b/java/fory-core/src/test/java/org/apache/fory/serializer/AndroidDynamicFeatureTest.java index 6d08427b52..716381df2b 100644 --- a/java/fory-core/src/test/java/org/apache/fory/serializer/AndroidDynamicFeatureTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/serializer/AndroidDynamicFeatureTest.java @@ -19,9 +19,7 @@ package org.apache.fory.serializer; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; @@ -37,6 +35,7 @@ import java.util.Set; import java.util.function.ToIntFunction; import org.apache.fory.Fory; +import org.apache.fory.TestUtils; import org.apache.fory.context.ReadContext; import org.apache.fory.context.WriteContext; import org.apache.fory.memory.MemoryUtils; @@ -54,16 +53,11 @@ public class AndroidDynamicFeatureTest { @Test public void testAndroidDynamicFeaturePaths() throws Exception { - String javaBin = - System.getProperty("java.home") + File.separator + "bin" + File.separator + "java"; - Process process = - new ProcessBuilder( - javaBin, - "-cp", - System.getProperty("java.class.path"), - AndroidDynamicFeatureProbe.class.getName()) - .redirectErrorStream(true) - .start(); + ProcessBuilder processBuilder = + new ProcessBuilder(TestUtils.javaCommand(AndroidDynamicFeatureProbe.class)) + .redirectErrorStream(true); + processBuilder.environment().put("FORY_ANDROID_ENABLED", "1"); + Process process = processBuilder.start(); String output = readFully(process.getInputStream()); Assert.assertEquals(process.waitFor(), 0, output); } @@ -87,7 +81,7 @@ public static void main(String[] args) { LambdaSerializer.STUB_LAMBDA_CLASS == LambdaSerializer.ReplaceStub.class, "Android must not create a runtime lambda stub class"); verifyReflectiveGetter(); - verifyMemoryUtilsStreamWrapGuards(); + verifyJdkInternalFieldAccessDisabled(); verifyXlangUnion(); verifyFory(false); @@ -172,18 +166,10 @@ private static void verifyOutputStreamSerialization(Fory fory) { checkEquals(fory.deserialize(outputStream.toByteArray()), value, "OutputStream round trip"); } - private static void verifyMemoryUtilsStreamWrapGuards() { - expectUnsupportedAndroidWrap( - () -> MemoryUtils.wrap(new ByteArrayOutputStream(), MemoryUtils.buffer(8)), - "ByteArrayOutputStream direct wrapping"); - expectUnsupportedAndroidWrap( - () -> MemoryUtils.wrap(MemoryUtils.buffer(8), new ByteArrayOutputStream()), - "ByteArrayOutputStream direct wrapping"); - expectUnsupportedAndroidWrap( - () -> - MemoryUtils.wrap( - new ByteArrayInputStream(new byte[] {1, 2, 3}), MemoryUtils.buffer(8)), - "ByteArrayInputStream direct wrapping"); + private static void verifyJdkInternalFieldAccessDisabled() { + check( + !MemoryUtils.JDK_INTERNAL_FIELD_ACCESS, + "Android must report JDK internal field access unsupported"); } private static void verifyXlangUnion() { @@ -257,17 +243,6 @@ private static void expectUnsupported(Serializer serializer) { } } - private static void expectUnsupportedAndroidWrap(Runnable operation, String messageFragment) { - try { - operation.run(); - throw new AssertionError("Expected Android unsafe stream wrapping to fail"); - } catch (UnsupportedOperationException expected) { - check( - expected.getMessage().contains(messageFragment), - "Unexpected unsupported message: " + expected.getMessage()); - } - } - private static void check(boolean value, String message) { if (!value) { throw new AssertionError(message); diff --git a/java/fory-core/src/test/java/org/apache/fory/serializer/AndroidJvmRoundTripTest.java b/java/fory-core/src/test/java/org/apache/fory/serializer/AndroidJvmRoundTripTest.java index 5b93035622..5b37f5a698 100644 --- a/java/fory-core/src/test/java/org/apache/fory/serializer/AndroidJvmRoundTripTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/serializer/AndroidJvmRoundTripTest.java @@ -158,8 +158,8 @@ private static void addAddOpens(ArrayList command) { if (System.getProperty("java.specification.version").startsWith("1.")) { return; } - addAddOpens(command, "java.base/java.io=ALL-UNNAMED"); addAddOpens(command, "java.base/java.lang=ALL-UNNAMED"); + addAddOpens(command, "java.base/java.lang.reflect=ALL-UNNAMED"); addAddOpens(command, "java.base/java.util=ALL-UNNAMED"); } diff --git a/java/fory-core/src/test/java/org/apache/fory/serializer/ArraySerializersTest.java b/java/fory-core/src/test/java/org/apache/fory/serializer/ArraySerializersTest.java index 652a98e7c9..2807acc31a 100644 --- a/java/fory-core/src/test/java/org/apache/fory/serializer/ArraySerializersTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/serializer/ArraySerializersTest.java @@ -517,6 +517,10 @@ static class GenericArrayWrapper { public GenericArrayWrapper(Class clazz, int capacity) { this.array = (T[]) Array.newInstance(clazz, capacity); } + + public GenericArrayWrapper(T[] array) { + this.array = array; + } } @SuppressWarnings("unchecked") diff --git a/java/fory-core/src/test/java/org/apache/fory/serializer/DuplicateFieldsTest.java b/java/fory-core/src/test/java/org/apache/fory/serializer/DuplicateFieldsTest.java index e9f665d5f9..073a7dcc87 100644 --- a/java/fory-core/src/test/java/org/apache/fory/serializer/DuplicateFieldsTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/serializer/DuplicateFieldsTest.java @@ -25,6 +25,7 @@ import lombok.ToString; import org.apache.fory.Fory; import org.apache.fory.ForyTestBase; +import org.apache.fory.annotation.ForyField; import org.apache.fory.builder.CodecUtils; import org.apache.fory.config.ForyBuilder; import org.apache.fory.memory.MemoryBuffer; @@ -46,6 +47,61 @@ public static class C extends B { int f1; } + public static class PrivateBase { + @ForyField(id = 1) + private int value; + + @ForyField(id = 2) + private final long finalValue; + + public PrivateBase() { + this(0, 0); + } + + public PrivateBase(@ForyField(id = 1) int value, @ForyField(id = 2) long finalValue) { + this.value = value; + this.finalValue = finalValue; + } + + int baseValue() { + return value; + } + + long baseFinalValue() { + return finalValue; + } + } + + public static class PrivateChild extends PrivateBase { + @ForyField(id = 3) + private int value; + + @ForyField(id = 4) + private final long finalValue; + + public PrivateChild() { + this(0, 0, 0, 0); + } + + public PrivateChild( + @ForyField(id = 1) int baseValue, + @ForyField(id = 2) long baseFinalValue, + @ForyField(id = 3) int value, + @ForyField(id = 4) long finalValue) { + super(baseValue, baseFinalValue); + this.value = value; + this.finalValue = finalValue; + } + + int childValue() { + return value; + } + + long childFinalValue() { + return finalValue; + } + } + @Test() public void testDuplicateFieldsNoCompatible() { C c = new C(); @@ -96,6 +152,30 @@ public void testDuplicateFieldsNoCompatible() { } } + @Test + public void testPrivateDuplicateFieldsNoCompatible() { + PrivateChild value = new PrivateChild(10, 20, -10, -20); + Fory fory = + Fory.builder() + .withXlang(false) + .withRefTracking(false) + .withCodegen(true) + .requireClassRegistration(false) + .build(); + Serializer serializer = + Serializers.newSerializer( + fory, + PrivateChild.class, + CodecUtils.loadOrGenObjectCodecClass(PrivateChild.class, fory)); + MemoryBuffer buffer = MemoryUtils.buffer(32); + writeSerializer(fory, serializer, buffer, value); + PrivateChild newValue = readSerializer(fory, serializer, buffer); + assertEquals(newValue.baseValue(), value.baseValue()); + assertEquals(newValue.baseFinalValue(), value.baseFinalValue()); + assertEquals(newValue.childValue(), value.childValue()); + assertEquals(newValue.childFinalValue(), value.childFinalValue()); + } + @Test public void testDuplicateFieldsCompatible() { C c = new C(); diff --git a/java/fory-core/src/test/java/org/apache/fory/serializer/FinalFieldReplaceResolveSerializerTest.java b/java/fory-core/src/test/java/org/apache/fory/serializer/FinalFieldReplaceResolveSerializerTest.java index a033c56018..49ca224047 100644 --- a/java/fory-core/src/test/java/org/apache/fory/serializer/FinalFieldReplaceResolveSerializerTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/serializer/FinalFieldReplaceResolveSerializerTest.java @@ -397,7 +397,6 @@ public void testNoClassNameWrittenForFinalField(boolean codegen) { byte[] bytesFinal = fory.serialize(containerFinal); byte[] bytesFinal2 = fory.serialize(containerFinal); assertEquals(bytesFinal, bytesFinal2); - assertEquals(bytesFinal.length, 109); // Create a container with a non-final ImmutableList field for comparison ContainerWithNonFinalImmutableIntArray containerNonFinal = @@ -405,7 +404,6 @@ public void testNoClassNameWrittenForFinalField(boolean codegen) { byte[] bytesNonFinal = fory.serialize(containerNonFinal); // The final field version should use fewer bytes because it doesn't write class name - System.out.println(bytesFinal.length + " " + bytesNonFinal.length); assertTrue( bytesFinal.length < bytesNonFinal.length, String.format( diff --git a/java/fory-core/src/test/java/org/apache/fory/serializer/JavaSerializerTest.java b/java/fory-core/src/test/java/org/apache/fory/serializer/JavaSerializerTest.java index 9afcd8b1c0..6c76f12723 100644 --- a/java/fory-core/src/test/java/org/apache/fory/serializer/JavaSerializerTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/serializer/JavaSerializerTest.java @@ -31,6 +31,7 @@ import lombok.Data; import org.apache.fory.Fory; import org.apache.fory.ForyTestBase; +import org.apache.fory.exception.CopyException; import org.apache.fory.memory.BigEndian; import org.apache.fory.memory.MemoryBuffer; import org.testng.Assert; @@ -62,7 +63,32 @@ public static class JavaBox implements Serializable { } } - public static class NestedValue implements Serializable {} + public static class NestedValue implements Serializable { + String value; + + NestedValue() { + this("nested"); + } + + NestedValue(String value) { + this.value = value; + } + } + + public static class JavaCopyState implements Serializable { + String name; + transient int nameLength; + + JavaCopyState(String name) { + this.name = name; + this.nameLength = name.length(); + } + + private void readObject(java.io.ObjectInputStream stream) throws Exception { + stream.defaultReadObject(); + nameLength = name.length(); + } + } @Test public void testWriteObject() { @@ -98,6 +124,25 @@ public void testJdkSerializationCopy(Fory fory) throws MalformedURLException { copyCheck(fory, url); } + @Test + public void testCopyUsesJavaSerialization() { + Fory fory = + Fory.builder() + .withXlang(false) + .withRefCopy(true) + .requireClassRegistration(false) + .suppressClassRegistrationWarnings(true) + .build(); + fory.registerSerializer(JavaCopyState.class, JavaSerializer.class); + JavaCopyState state = new JavaCopyState("fory"); + state.nameLength = -1; + JavaCopyState copy = fory.copy(state); + + Assert.assertNotSame(copy, state); + Assert.assertEquals(copy.name, state.name); + Assert.assertEquals(copy.nameLength, 4); + } + @Test public void testJdkStreamChecksNestedClass() { Fory fory = Fory.builder().withXlang(false).build(); @@ -109,4 +154,16 @@ public void testJdkStreamChecksNestedClass() { Assert.assertThrows( InvalidClassException.class, () -> readSerializer(fory, serializer, buffer)); } + + @Test + public void testCopyChecksNestedClass() { + Fory fory = Fory.builder().withXlang(false).build(); + fory.register(JavaBox.class); + fory.registerSerializer(JavaBox.class, JavaSerializer.class); + + CopyException exception = + Assert.expectThrows(CopyException.class, () -> fory.copy(new JavaBox(new NestedValue()))); + Assert.assertTrue(exception.getCause() instanceof InvalidClassException); + Assert.assertTrue(exception.getCause().getMessage().contains(NestedValue.class.getName())); + } } diff --git a/java/fory-core/src/test/java/org/apache/fory/serializer/ObjectSerializerTest.java b/java/fory-core/src/test/java/org/apache/fory/serializer/ObjectSerializerTest.java index 162b984c6f..72d4ec560b 100644 --- a/java/fory-core/src/test/java/org/apache/fory/serializer/ObjectSerializerTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/serializer/ObjectSerializerTest.java @@ -23,13 +23,15 @@ import static org.testng.Assert.assertNotSame; import java.io.ByteArrayOutputStream; -import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.io.Serializable; import java.nio.charset.StandardCharsets; import lombok.Data; import org.apache.fory.Fory; import org.apache.fory.ForyTestBase; +import org.apache.fory.TestUtils; +import org.apache.fory.builder.CodecUtils; import org.apache.fory.memory.MemoryBuffer; import org.apache.fory.memory.MemoryUtils; import org.apache.fory.platform.AndroidSupport; @@ -171,6 +173,189 @@ public void testCopyCircularReference(Fory fory) { assertNotSame(cyclic1, cyclic); } + public static final class FinalNoArgBean { + private final int id; + private final String name; + private int count; + + public FinalNoArgBean() { + id = -1; + name = "default"; + } + + private FinalNoArgBean(int value, String text, int total) { + id = value; + name = text; + count = total; + } + } + + public static final class FinalPostCtorBean { + private final int id; + private String label; + + public FinalPostCtorBean(String label) { + id = -1; + this.label = label; + } + + private FinalPostCtorBean(int value, String label) { + id = value; + this.label = label; + } + } + + 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; + } + } + + @Test + public void testFinalNoArgRestore() { + FinalNoArgBean value = new FinalNoArgBean(7, "source", 9); + for (boolean codegen : new boolean[] {false, true}) { + Fory fory = + Fory.builder() + .withXlang(false) + .withRefTracking(true) + .withCodegen(codegen) + .requireClassRegistration(false) + .build(); + FinalNoArgBean newValue = (FinalNoArgBean) fory.deserialize(fory.serialize(value)); + assertEquals(newValue.id, value.id); + assertEquals(newValue.name, value.name); + assertEquals(newValue.count, value.count); + } + } + + @Test + public void testFinalNoArgRestoreCodegen() { + FinalNoArgBean value = new FinalNoArgBean(7, "source", 9); + Fory fory = + Fory.builder() + .withXlang(false) + .withRefTracking(true) + .withCodegen(true) + .requireClassRegistration(false) + .build(); + Serializer serializer = + Serializers.newSerializer( + fory, + FinalNoArgBean.class, + CodecUtils.loadOrGenObjectCodecClass(FinalNoArgBean.class, fory)); + FinalNoArgBean newValue = roundTripWithSerializer(fory, serializer, value); + assertEquals(newValue.id, value.id); + assertEquals(newValue.name, value.name); + assertEquals(newValue.count, value.count); + } + + @Test + public void testFinalPostCtorRestore() { + FinalPostCtorBean value = new FinalPostCtorBean(8, "ctor"); + for (boolean codegen : new boolean[] {false, true}) { + Fory fory = + Fory.builder() + .withXlang(false) + .withRefTracking(true) + .withCodegen(codegen) + .requireClassRegistration(false) + .build(); + FinalPostCtorBean newValue = (FinalPostCtorBean) fory.deserialize(fory.serialize(value)); + assertEquals(newValue.id, value.id); + assertEquals(newValue.label, value.label); + } + } + + @Test + public void testFinalPostCtorCodegen() { + FinalPostCtorBean value = new FinalPostCtorBean(8, "ctor"); + Fory fory = + Fory.builder() + .withXlang(false) + .withRefTracking(true) + .withCodegen(true) + .requireClassRegistration(false) + .build(); + Serializer serializer = + Serializers.newSerializer( + fory, + FinalPostCtorBean.class, + CodecUtils.loadOrGenObjectCodecClass(FinalPostCtorBean.class, fory)); + FinalPostCtorBean newValue = roundTripWithSerializer(fory, serializer, value); + assertEquals(newValue.id, value.id); + assertEquals(newValue.label, value.label); + } + + @Test + public void testNormalObjectSerializerBypassesObjectStreamParentRules() { + SerializablePrivateParentBean value = new SerializablePrivateParentBean("parent", "child"); + for (boolean codegen : new boolean[] {false, true}) { + PrivateNoArgParent.noArgCalls = 0; + Fory fory = + Fory.builder() + .withXlang(false) + .withRefTracking(true) + .withCodegen(codegen) + .requireClassRegistration(false) + .build(); + Serializer serializer = + fory.getTypeResolver().getTypeInfo(SerializablePrivateParentBean.class).getSerializer(); + Assert.assertFalse( + serializer instanceof ObjectStreamSerializer, serializer.getClass().getName()); + SerializablePrivateParentBean newValue = + (SerializablePrivateParentBean) fory.deserialize(fory.serialize(value)); + assertEquals(newValue.parentName(), value.parentName()); + assertEquals(newValue.childName, value.childName); + assertEquals(PrivateNoArgParent.noArgCalls, 0); + } + } + + private static T roundTripWithSerializer(Fory fory, Serializer serializer, T value) { + MemoryBuffer buffer = MemoryUtils.buffer(32); + withWriteContext( + fory, + buffer, + context -> { + context.writeRefOrNull(value); + serializer.write(context, value); + }); + T newValue = + withReadContext( + fory, + buffer, + context -> { + byte tag = context.readRefOrNull(); + Preconditions.checkArgument(tag == Fory.REF_VALUE_FLAG); + context.preserveRefId(); + return serializer.read(context); + }); + fory.reset(); + return newValue; + } + @Data public static class A { Integer f1; @@ -199,17 +384,12 @@ public void testSerialization() { } @Test - public void testAndroidObjectSerializerReflectionPaths() throws Exception { - String javaBin = - System.getProperty("java.home") + File.separator + "bin" + File.separator + "java"; - Process process = - new ProcessBuilder( - javaBin, - "-cp", - System.getProperty("java.class.path"), - AndroidObjectSerializerProbe.class.getName()) - .redirectErrorStream(true) - .start(); + public void testAndroidReflectionPaths() throws Exception { + ProcessBuilder processBuilder = + new ProcessBuilder(TestUtils.javaCommand(AndroidObjectSerializerProbe.class)) + .redirectErrorStream(true); + processBuilder.environment().put("FORY_ANDROID_ENABLED", "1"); + Process process = processBuilder.start(); String output = readFully(process.getInputStream()); Assert.assertEquals(process.waitFor(), 0, output); } diff --git a/java/fory-core/src/test/java/org/apache/fory/serializer/ObjectStreamSerializerTest.java b/java/fory-core/src/test/java/org/apache/fory/serializer/ObjectStreamSerializerTest.java index 66a93e71ca..fc0b7735e2 100644 --- a/java/fory-core/src/test/java/org/apache/fory/serializer/ObjectStreamSerializerTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/serializer/ObjectStreamSerializerTest.java @@ -48,11 +48,13 @@ import org.apache.fory.config.ForyBuilder; import org.apache.fory.context.MetaReadContext; import org.apache.fory.context.MetaWriteContext; +import org.apache.fory.exception.ForyException; import org.apache.fory.exception.InsecureException; import org.apache.fory.memory.MemoryBuffer; import org.apache.fory.resolver.SharedRegistry; import org.apache.fory.serializer.collection.CollectionSerializers; import org.apache.fory.serializer.collection.MapSerializers; +import org.apache.fory.serializer.otherpkg.PackageNoArgParent; import org.apache.fory.util.Preconditions; import org.testng.Assert; import org.testng.annotations.DataProvider; @@ -116,6 +118,208 @@ public void testDispatch(Fory fory) { serDeCheckSerializer(fory, o, "ObjectStreamSerializer"); } + public static class AnnotatedObjectStreamType implements Serializable { + String name; + int age; + transient boolean readObjectCalled; + + public AnnotatedObjectStreamType(String name, int age) { + this.name = name; + this.age = age; + } + + private void writeObject(ObjectOutputStream s) throws IOException { + s.defaultWriteObject(); + } + + private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { + s.defaultReadObject(); + readObjectCalled = true; + } + } + + public static class RegisteredObjectStreamType implements Serializable { + String name; + int age; + transient boolean readObjectCalled; + + public RegisteredObjectStreamType(String name, int age) { + this.name = name; + this.age = age; + } + + private void writeObject(ObjectOutputStream s) throws IOException { + s.defaultWriteObject(); + } + + private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { + s.defaultReadObject(); + readObjectCalled = true; + } + } + + public static class ThrowingNoArgObjectStreamType implements Serializable { + String name; + + public ThrowingNoArgObjectStreamType() { + throw new AssertionError("ObjectStream reconstruction must not invoke this constructor"); + } + + public ThrowingNoArgObjectStreamType(String name) { + this.name = name; + } + + private void writeObject(ObjectOutputStream s) throws IOException { + s.defaultWriteObject(); + } + + private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { + s.defaultReadObject(); + } + } + + public static class SerializableParentCtorType implements Serializable { + static int noArgCalls; + String parentName; + + public SerializableParentCtorType() { + noArgCalls++; + } + + public SerializableParentCtorType(String parentName) { + this.parentName = parentName; + } + + private void writeObject(ObjectOutputStream s) throws IOException { + s.defaultWriteObject(); + } + + private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { + s.defaultReadObject(); + } + } + + public static class SerializableChildCtorType extends SerializableParentCtorType { + String childName; + + public SerializableChildCtorType(String parentName, String childName) { + super(parentName); + this.childName = childName; + } + + private void writeObject(ObjectOutputStream s) throws IOException { + s.defaultWriteObject(); + } + + private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { + s.defaultReadObject(); + } + } + + public static class PrivateNoArgParent { + private PrivateNoArgParent() {} + + public PrivateNoArgParent(String ignored) {} + } + + public static class InvalidCtorChild extends PrivateNoArgParent implements Serializable { + String name; + + public InvalidCtorChild(String name) { + super(name); + this.name = name; + } + + private void writeObject(ObjectOutputStream s) throws IOException { + s.defaultWriteObject(); + } + + private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { + s.defaultReadObject(); + } + } + + public static class InvalidPackageCtorChild extends PackageNoArgParent implements Serializable { + String name; + + public InvalidPackageCtorChild(String name) { + super(name); + this.name = name; + } + + private void writeObject(ObjectOutputStream s) throws IOException { + s.defaultWriteObject(); + } + + private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { + s.defaultReadObject(); + } + } + + @Test(dataProvider = "javaFory") + public void testObjectStreamNoArgBypassRead(Fory fory) { + AnnotatedObjectStreamType annotated = + serDeCheckSerializer( + fory, new AnnotatedObjectStreamType("annotated", 1), "ObjectStreamSerializer"); + Assert.assertEquals(annotated.name, "annotated"); + Assert.assertEquals(annotated.age, 1); + Assert.assertTrue(annotated.readObjectCalled); + + RegisteredObjectStreamType registered = + serDeCheckSerializer( + fory, new RegisteredObjectStreamType("registered", 2), "ObjectStreamSerializer"); + Assert.assertEquals(registered.name, "registered"); + Assert.assertEquals(registered.age, 2); + Assert.assertTrue(registered.readObjectCalled); + + ThrowingNoArgObjectStreamType throwing = + serDeCheckSerializer( + fory, new ThrowingNoArgObjectStreamType("throwing"), "ObjectStreamSerializer"); + Assert.assertEquals(throwing.name, "throwing"); + } + + @Test(dataProvider = "javaFory") + public void testSerializableParentCtor(Fory fory) { + SerializableParentCtorType.noArgCalls = 0; + SerializableChildCtorType child = new SerializableChildCtorType("parent", "child"); + SerializableChildCtorType decoded = serDeCheckSerializer(fory, child, "ObjectStreamSerializer"); + Assert.assertEquals(decoded.parentName, "parent"); + Assert.assertEquals(decoded.childName, "child"); + Assert.assertEquals(SerializableParentCtorType.noArgCalls, 0); + } + + @Test(dataProvider = "javaFory") + public void testInvalidParentCtor(Fory fory) { + Assert.assertThrows(ForyException.class, () -> fory.serialize(new InvalidCtorChild("child"))); + Assert.assertThrows( + ForyException.class, () -> fory.serialize(new InvalidPackageCtorChild("child"))); + } + + @Test(dataProvider = "foryCopyConfig") + public void testObjectStreamNoArgBypassCopy(Fory fory) { + AnnotatedObjectStreamType copy = fory.copy(new AnnotatedObjectStreamType("copy", 3)); + Assert.assertEquals(copy.name, "copy"); + Assert.assertEquals(copy.age, 3); + + RegisteredObjectStreamType registeredCopy = + fory.copy(new RegisteredObjectStreamType("registered-copy", 4)); + Assert.assertEquals(registeredCopy.name, "registered-copy"); + Assert.assertEquals(registeredCopy.age, 4); + + ThrowingNoArgObjectStreamType throwingCopy = + fory.copy(new ThrowingNoArgObjectStreamType("throwing-copy")); + Assert.assertEquals(throwingCopy.name, "throwing-copy"); + } + + @Test(dataProvider = "foryCopyConfig") + public void testSerializableParentCtorCopy(Fory fory) { + SerializableParentCtorType.noArgCalls = 0; + SerializableChildCtorType copy = fory.copy(new SerializableChildCtorType("parent", "child")); + Assert.assertEquals(copy.parentName, "parent"); + Assert.assertEquals(copy.childName, "child"); + Assert.assertEquals(SerializableParentCtorType.noArgCalls, 0); + } + @EqualsAndHashCode(callSuper = true) public static class WriteObjectTestClass2 extends WriteObjectTestClass { private final String data; diff --git a/java/fory-core/src/test/java/org/apache/fory/serializer/RegisterTest.java b/java/fory-core/src/test/java/org/apache/fory/serializer/RegisterTest.java index e78422a128..0b58813fea 100644 --- a/java/fory-core/src/test/java/org/apache/fory/serializer/RegisterTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/serializer/RegisterTest.java @@ -19,11 +19,14 @@ package org.apache.fory.serializer; +import java.util.concurrent.atomic.AtomicBoolean; import org.apache.fory.Fory; +import org.apache.fory.ForyModule; import org.apache.fory.ForyTestBase; import org.apache.fory.config.ForyBuilder; import org.apache.fory.context.ReadContext; import org.apache.fory.context.WriteContext; +import org.apache.fory.exception.ForyException; import org.apache.fory.resolver.TypeResolver; import org.testng.Assert; import org.testng.annotations.Test; @@ -163,6 +166,31 @@ public static class MyExt { public String id; } + @Test + public void testFrozenFacadeRegistration() { + Fory fory = + Fory.builder().withXlang(false).withCodegen(false).requireClassRegistration(false).build(); + fory.serialize(new MyExt()); + + AtomicBoolean moduleInstalled = new AtomicBoolean(); + Assert.assertThrows( + ForyException.class, + () -> fory.register((ForyModule) runtime -> moduleInstalled.set(true))); + Assert.assertFalse(moduleInstalled.get()); + + AtomicBoolean creatorCalled = new AtomicBoolean(); + Assert.assertThrows( + ForyException.class, + () -> + fory.registerSerializer( + MyExt.class, + resolver -> { + creatorCalled.set(true); + return new MyExtSerializer(resolver); + })); + Assert.assertFalse(creatorCalled.get()); + } + public static class MyExtSerializer extends Serializer { public MyExtSerializer(TypeResolver typeResolver) { super(typeResolver.getConfig(), MyExt.class); diff --git a/java/fory-core/src/test/java/org/apache/fory/serializer/ReplaceResolveSerializerTest.java b/java/fory-core/src/test/java/org/apache/fory/serializer/ReplaceResolveSerializerTest.java index 143a3e983e..128203fef7 100644 --- a/java/fory-core/src/test/java/org/apache/fory/serializer/ReplaceResolveSerializerTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/serializer/ReplaceResolveSerializerTest.java @@ -673,6 +673,8 @@ static class ReplaceSelfExternalizable implements Externalizable { private transient int f1; private transient boolean newInstance; + public ReplaceSelfExternalizable() {} + public ReplaceSelfExternalizable(int f1, boolean newInstance) { this.f1 = f1; this.newInstance = newInstance; diff --git a/java/fory-core/src/test/java/org/apache/fory/util/StringEncodingUtilsTest.java b/java/fory-core/src/test/java/org/apache/fory/serializer/StringEncodingUtilsTest.java similarity index 98% rename from java/fory-core/src/test/java/org/apache/fory/util/StringEncodingUtilsTest.java rename to java/fory-core/src/test/java/org/apache/fory/serializer/StringEncodingUtilsTest.java index 769e96c0d8..7df39320e1 100644 --- a/java/fory-core/src/test/java/org/apache/fory/util/StringEncodingUtilsTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/serializer/StringEncodingUtilsTest.java @@ -17,7 +17,7 @@ * under the License. */ -package org.apache.fory.util; +package org.apache.fory.serializer; import static org.testng.Assert.assertEquals; diff --git a/java/fory-core/src/test/java/org/apache/fory/serializer/StringSerializerTest.java b/java/fory-core/src/test/java/org/apache/fory/serializer/StringSerializerTest.java index d45f3271a4..efe625cc89 100644 --- a/java/fory-core/src/test/java/org/apache/fory/serializer/StringSerializerTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/serializer/StringSerializerTest.java @@ -37,8 +37,6 @@ import org.apache.fory.memory.MemoryBuffer; import org.apache.fory.memory.MemoryUtils; import org.apache.fory.platform.JdkVersion; -import org.apache.fory.platform.UnsafeOps; -import org.apache.fory.reflect.ReflectionUtils; import org.apache.fory.util.MathUtils; import org.apache.fory.util.StringUtils; import org.testng.Assert; @@ -139,8 +137,7 @@ private static boolean writeJavaStringZeroCopy(MemoryBuffer buffer, String value } static void writeJDK8String(MemoryBuffer buffer, String value) { - final char[] chars = - (char[]) UnsafeOps.getObject(value, ReflectionUtils.getFieldOffset(String.class, "value")); + final char[] chars = (char[]) PlatformStringUtils.getStringValue(value); int numBytes = MathUtils.doubleExact(value.length()); buffer.writeCharsWithSize(chars); } diff --git a/java/fory-core/src/test/java/org/apache/fory/serializer/collection/AndroidCollectionFeatureTest.java b/java/fory-core/src/test/java/org/apache/fory/serializer/collection/AndroidCollectionFeatureTest.java index 2d2e2d5736..ac4f0d7eb6 100644 --- a/java/fory-core/src/test/java/org/apache/fory/serializer/collection/AndroidCollectionFeatureTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/serializer/collection/AndroidCollectionFeatureTest.java @@ -69,6 +69,8 @@ public void testAndroidCollectionFeaturePaths() throws Exception { String jvmEnumMapPayload = encode(jvmFory, newEnumMapValue()); String jvmEmptyEnumMapPayload = encode(jvmFory, new EnumMap<>(AndroidCollectionFeatureProbe.TestEnum.class)); + String jvmArrayQueuePayload = encode(jvmFory, newArrayBlockingQueueValue()); + String jvmLinkedQueuePayload = encode(jvmFory, newLinkedBlockingQueueValue()); String javaBin = System.getProperty("java.home") + File.separator + "bin" + File.separator + "java"; @@ -77,6 +79,8 @@ public void testAndroidCollectionFeaturePaths() throws Exception { if (!System.getProperty("java.specification.version").startsWith("1.")) { command.add("--add-opens"); command.add("java.base/java.util=ALL-UNNAMED"); + command.add("--add-opens"); + command.add("java.base/java.lang.invoke=ALL-UNNAMED"); } command.add("-cp"); command.add(System.getProperty("java.class.path")); @@ -85,7 +89,11 @@ public void testAndroidCollectionFeaturePaths() throws Exception { command.add(jvmSubListPayload); command.add(jvmEnumMapPayload); command.add(jvmEmptyEnumMapPayload); - Process process = new ProcessBuilder(command).redirectErrorStream(true).start(); + command.add(jvmArrayQueuePayload); + command.add(jvmLinkedQueuePayload); + ProcessBuilder processBuilder = new ProcessBuilder(command).redirectErrorStream(true); + processBuilder.environment().put("FORY_ANDROID_ENABLED", "1"); + Process process = processBuilder.start(); String output = readFully(process.getInputStream()); Assert.assertEquals(process.waitFor(), 0, output); @@ -169,11 +177,25 @@ private static EnumMap newEnumMa return map; } + private static ArrayBlockingQueue newArrayBlockingQueueValue() { + ArrayBlockingQueue queue = new ArrayBlockingQueue<>(5); + queue.add("a"); + queue.add("b"); + return queue; + } + + private static LinkedBlockingQueue newLinkedBlockingQueueValue() { + LinkedBlockingQueue queue = new LinkedBlockingQueue<>(6); + queue.add("a"); + queue.add("b"); + return queue; + } + public static final class AndroidCollectionFeatureProbe { public static void main(String[] args) throws Exception { check( - args.length == 4, - "Expected JVM child-container, sublist, enum-map, and empty-enum-map payloads"); + args.length == 6, + "Expected JVM child-container, sublist, enum-map, empty-enum-map, and queue payloads"); System.setProperty("java.vm.name", "Dalvik"); System.setProperty("java.runtime.name", "Android Runtime"); check(AndroidSupport.IS_ANDROID, "AndroidSupport should detect Dalvik runtime"); @@ -193,7 +215,6 @@ public static void main(String[] args) throws Exception { verifySubList(fory); verifyArraysAsList(fory); verifySetFromMap(fory); - verifyQueues(fory); verifyEnumMap(fory); if (JdkVersion.MAJOR_VERSION >= 9) { verifyImmutableCollections(fory); @@ -202,6 +223,8 @@ public static void main(String[] args) throws Exception { verifyJvmSubListPayload(fory, Base64.getDecoder().decode(args[1])); verifyJvmEnumMapPayloads( fory, Base64.getDecoder().decode(args[2]), Base64.getDecoder().decode(args[3])); + verifyJvmQueuePayloads( + fory, Base64.getDecoder().decode(args[4]), Base64.getDecoder().decode(args[5])); writeAndroidFeaturePayloads(fory); } @@ -314,23 +337,24 @@ private static void verifySetFromMap(Fory fory) { checkEquals(copy, set, "SetFromMap copy"); } - private static void verifyQueues(Fory fory) { - ArrayBlockingQueue arrayQueue = new ArrayBlockingQueue<>(5); - arrayQueue.add("a"); - arrayQueue.add("b"); + private static void verifyJvmQueuePayloads( + Fory fory, byte[] jvmArrayQueuePayload, byte[] jvmLinkedQueuePayload) { + ArrayBlockingQueue arrayQueue = newArrayBlockingQueueValue(); ArrayBlockingQueue arrayRestored = - (ArrayBlockingQueue) roundTrip(fory, arrayQueue); + (ArrayBlockingQueue) fory.deserialize(jvmArrayQueuePayload); checkEquals( - new ArrayList<>(arrayRestored), new ArrayList<>(arrayQueue), "ArrayBlockingQueue"); + new ArrayList<>(arrayRestored), + new ArrayList<>(arrayQueue), + "JVM ArrayBlockingQueue payload"); checkEquals(arrayRestored.remainingCapacity(), 3, "ArrayBlockingQueue capacity"); - LinkedBlockingQueue linkedQueue = new LinkedBlockingQueue<>(6); - linkedQueue.add("a"); - linkedQueue.add("b"); + LinkedBlockingQueue linkedQueue = newLinkedBlockingQueueValue(); LinkedBlockingQueue linkedRestored = - (LinkedBlockingQueue) roundTrip(fory, linkedQueue); + (LinkedBlockingQueue) fory.deserialize(jvmLinkedQueuePayload); checkEquals( - new ArrayList<>(linkedRestored), new ArrayList<>(linkedQueue), "LinkedBlockingQueue"); + new ArrayList<>(linkedRestored), + new ArrayList<>(linkedQueue), + "JVM LinkedBlockingQueue payload"); checkEquals(linkedRestored.remainingCapacity(), 4, "LinkedBlockingQueue capacity"); } diff --git a/java/fory-core/src/test/java/org/apache/fory/serializer/collection/CollectionSerializersTest.java b/java/fory-core/src/test/java/org/apache/fory/serializer/collection/CollectionSerializersTest.java index 3f9e4b1d72..2a97fbf202 100644 --- a/java/fory-core/src/test/java/org/apache/fory/serializer/collection/CollectionSerializersTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/serializer/collection/CollectionSerializersTest.java @@ -72,8 +72,10 @@ import org.apache.fory.ForyTestBase; import org.apache.fory.context.ReadContext; import org.apache.fory.exception.DeserializationException; +import org.apache.fory.exception.SerializationException; import org.apache.fory.memory.MemoryBuffer; import org.apache.fory.memory.MemoryUtils; +import org.apache.fory.reflect.FieldAccessor; import org.apache.fory.reflect.TypeRef; import org.apache.fory.resolver.TypeResolver; import org.apache.fory.serializer.collection.CollectionSerializers.JDKCompatibleCollectionSerializer; @@ -98,6 +100,24 @@ public int compare(String left, String right) { } } + public static final class NonComparableSetItem { + public int id; + + public NonComparableSetItem() {} + + private NonComparableSetItem(int id) { + this.id = id; + } + } + + public static final class SetItemComparator + implements Comparator, Serializable { + @Override + public int compare(NonComparableSetItem left, NonComparableSetItem right) { + return Integer.compare(left.id, right.id); + } + } + private static final class SortedSetConstructorCase { private final String name; private final Class expectedType; @@ -289,6 +309,66 @@ public void testSortedSet(Fory fory) { Assert.assertEquals(Arrays.toString(copy.toArray()), "[str, str2, str11]"); } + @Test + public void testXlangTreeSetCopy() { + Fory fory = + builder().withXlang(true).withRefTracking(false).requireClassRegistration(true).build(); + TreeSet set = new TreeSet<>(Collections.reverseOrder()); + set.addAll(sortedCollectionInput()); + + Assert.assertTrue( + fory.getSerializer(TreeSet.class) instanceof CollectionSerializers.SortedSetSerializer); + TreeSet copy = fory.copy(set); + + Assert.assertNotSame(copy, set); + Assert.assertEquals(copy, set); + Assert.assertNotNull(copy.comparator()); + Assert.assertTrue(copy.comparator().compare("a", "b") > 0); + } + + @Test + public void testXlangTreeSetNaturalComparator() { + Fory fory = + builder().withXlang(true).withRefTracking(false).requireClassRegistration(true).build(); + TreeSet set = new TreeSet<>(Comparator.naturalOrder()); + set.addAll(sortedCollectionInput()); + + Object decoded = fory.deserialize(fory.serialize(set)); + + Assert.assertEquals(decoded, set); + } + + @Test + public void testXlangTreeSetComparatorWrite() { + Fory fory = + builder().withXlang(true).withRefTracking(false).requireClassRegistration(true).build(); + TreeSet set = new TreeSet<>(new SetItemComparator()); + set.add(new NonComparableSetItem(1)); + + SerializationException exception = + Assert.expectThrows(SerializationException.class, () -> fory.serialize(set)); + Assert.assertTrue(exception.getCause() instanceof UnsupportedOperationException); + } + + @Test + public void testXlangTreeSetObjectCopy() { + Fory fory = + builder().withXlang(true).withRefTracking(false).requireClassRegistration(true).build(); + fory.register(NonComparableSetItem.class); + fory.register(SetItemComparator.class); + TreeSet set = new TreeSet<>(new SetItemComparator()); + set.add(new NonComparableSetItem(2)); + set.add(new NonComparableSetItem(1)); + + TreeSet copy = fory.copy(set); + + Assert.assertNotSame(copy, set); + Assert.assertEquals(copy.size(), set.size()); + Assert.assertNotNull(copy.comparator()); + Assert.assertEquals(copy.first().id, 1); + Assert.assertEquals(copy.last().id, 2); + } + private class TestComparator implements Comparator { AtomicInteger i; @@ -343,7 +423,7 @@ public void testTreeSetConstructorMatrix(Fory fory) { } @Test(dataProvider = "referenceTrackingConfig") - public void testConcurrentSkipListSetConstructorMatrix(boolean referenceTrackingConfig) { + public void testSkipListSetCtorSerde(boolean referenceTrackingConfig) { Fory fory = Fory.builder() .withXlang(false) @@ -361,7 +441,7 @@ public void testConcurrentSkipListSetConstructorMatrix(boolean referenceTracking } @Test(dataProvider = "foryCopyConfig") - public void testConcurrentSkipListSetConstructorMatrix(Fory fory) { + public void testSkipListSetCtorCopy(Fory fory) { for (SortedSetConstructorCase testCase : concurrentSkipListSetConstructorCases()) { SortedSet original = testCase.factory.get(); assertSortedSetState(testCase, original); @@ -422,7 +502,7 @@ public ChildTreeSetWithComparator(Comparator comparator) { } @Test(dataProvider = "referenceTrackingConfig") - public void testSortedSetSubclassWithoutComparatorCtor(boolean referenceTrackingConfig) { + public void testSortedSetSubclassNoComparatorCtor(boolean referenceTrackingConfig) { Fory fory = Fory.builder() .withXlang(false) @@ -456,8 +536,7 @@ public void testSortedSetSubclassWithComparatorCtor(boolean referenceTrackingCon } @Test(dataProvider = "referenceTrackingConfig") - public void testSortedSetSubclassRegisteredWithSortedSetSerializer( - boolean referenceTrackingConfig) { + public void testSortedSetSubclassRegistered(boolean referenceTrackingConfig) { Fory fory = Fory.builder() .withXlang(false) @@ -480,8 +559,7 @@ public void testSortedSetSubclassRegisteredWithSortedSetSerializer( } @Test(dataProvider = "referenceTrackingConfig") - public void testSortedSetSubclassWithComparatorRegisteredWithSortedSetSerializer( - boolean referenceTrackingConfig) { + public void testSortedSetComparatorRegistered(boolean referenceTrackingConfig) { Fory fory = Fory.builder() .withXlang(false) @@ -881,10 +959,14 @@ public void testCopyOnWriteArrayList(Fory fory) { } @Data - @AllArgsConstructor public static class CollectionViewTestStruct { Collection collection; Set set; + + public CollectionViewTestStruct(Collection collection, Set set) { + this.collection = collection; + this.set = set; + } } @Test(dataProvider = "javaFory") @@ -909,10 +991,15 @@ public void testSetFromMap(Fory fory) { if (fory.getConfig().trackingRef()) { assertSame(struct2.collection, struct2.set); } + Map synchronizedMap = Collections.synchronizedMap(new HashMap<>()); + set = Collections.newSetFromMap(synchronizedMap); + set.add("a"); + set.add("b"); + serDeCheck(fory, set); } @Test - public void testSetFromMapNestedInExternalizablePreservesRefIds() { + public void testSetFromMapExternalizableRefs() { Fory fory = Fory.builder() .withXlang(false) @@ -971,12 +1058,28 @@ private RefMarker(String value) { } @Test(dataProvider = "foryCopyConfig") - public void testSetFromMapCopy(Fory fory) { + public void testSetFromMapCopy(Fory fory) throws Exception { final Set set = Collections.newSetFromMap(Maps.newConcurrentMap()); set.add("a"); set.add("b"); set.add(Cyclic.create(true)); copyCheck(fory, set); + + final Map synchronizedMap = Collections.synchronizedMap(new HashMap<>()); + final Set synchronizedSet = Collections.newSetFromMap(synchronizedMap); + synchronizedSet.add("a"); + synchronizedSet.add("b"); + synchronizedSet.add(Cyclic.create(true)); + Set copied = fory.copy(synchronizedSet); + assertEquals(copied, synchronizedSet); + if (MemoryUtils.JDK_COLLECTION_FIELD_ACCESS) { + assertEquals(setFromMapBackingMap(copied).getClass(), synchronizedMap.getClass()); + } + } + + private static Map setFromMapBackingMap(Set set) throws Exception { + Field mapField = set.getClass().getDeclaredField("m"); + return (Map) FieldAccessor.createAccessor(mapField).getObject(set); } @Test(dataProvider = "javaFory") @@ -1041,7 +1144,7 @@ public void testSerializeJavaBlockingQueue() { } @Test - public void testDeserializeJavaBlockingQueueRejectsMalformedCapacity() { + public void testBlockingQueueBadCapacity() { Fory fory = Fory.builder() .withXlang(false) @@ -1072,7 +1175,7 @@ public void testDeserializeJavaBlockingQueueRejectsMalformedCapacity() { } @Test - public void testCollectionReadRejectsOversizedElementCount() { + public void testCollectionRejectsTooManyElements() { Fory fory = Fory.builder() .withXlang(false) @@ -1090,7 +1193,7 @@ public void testCollectionReadRejectsOversizedElementCount() { } @Test - public void testBitSetReadRejectsNegativeDecodedBinaryPayload() { + public void testBitSetRejectsNegativeBinary() { Fory fory = Fory.builder().withXlang(false).build(); MemoryBuffer buffer = MemoryBuffer.newHeapBuffer(5); writeNegativeDecodedVarUInt32(buffer); @@ -1369,7 +1472,15 @@ public void testNestedCollection2Copy(Fory fory) { } public static class TestClassForDefaultCollectionSerializer extends AbstractCollection { - private final List data = new ArrayList<>(); + private final List data; + + public TestClassForDefaultCollectionSerializer() { + this(new ArrayList<>()); + } + + public TestClassForDefaultCollectionSerializer(List data) { + this.data = data; + } @Override public Iterator iterator() { @@ -1423,7 +1534,7 @@ public void testDefaultCollectionSerializer(Fory fory) { } @Test - public void testDefaultCollectionSerializerAsyncCompilation() { + public void testDefaultCollectionAsyncCompile() { Fory fory = Fory.builder() .withXlang(false) @@ -1549,7 +1660,7 @@ static class CollectionAbstractTest { } @Test(dataProvider = "enableCodegen") - public void testAbstractCollectionElementsSerialization(boolean enableCodegen) { + public void testAbstractCollectionElementsSerde(boolean enableCodegen) { Fory fory = Fory.builder() .withXlang(false) @@ -1569,7 +1680,7 @@ public void testAbstractCollectionElementsSerialization(boolean enableCodegen) { } @Test(dataProvider = "foryCopyConfig") - public void testAbstractCollectionElementsSerialization(Fory fory) { + public void testAbstractCollectionElementsCopy(Fory fory) { { CollectionAbstractTest test = new CollectionAbstractTest(); test.fooList = new ArrayList<>(ImmutableList.of(new Foo1(), new Foo1())); diff --git a/java/fory-core/src/test/java/org/apache/fory/serializer/collection/MapSerializersTest.java b/java/fory-core/src/test/java/org/apache/fory/serializer/collection/MapSerializersTest.java index 25c3b61866..595380bb06 100644 --- a/java/fory-core/src/test/java/org/apache/fory/serializer/collection/MapSerializersTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/serializer/collection/MapSerializersTest.java @@ -40,6 +40,7 @@ import java.util.EnumMap; import java.util.HashMap; import java.util.HashSet; +import java.util.IdentityHashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -58,6 +59,7 @@ import org.apache.fory.collection.LazyMap; import org.apache.fory.collection.MapEntry; import org.apache.fory.exception.DeserializationException; +import org.apache.fory.exception.SerializationException; import org.apache.fory.memory.MemoryBuffer; import org.apache.fory.memory.MemoryUtils; import org.apache.fory.reflect.TypeRef; @@ -84,6 +86,24 @@ public int compare(String left, String right) { } } + public static final class NonComparableMapKey { + public int id; + + public NonComparableMapKey() {} + + private NonComparableMapKey(int id) { + this.id = id; + } + } + + public static final class MapKeyComparator + implements Comparator, Serializable { + @Override + public int compare(NonComparableMapKey left, NonComparableMapKey right) { + return Integer.compare(left.id, right.id); + } + } + private static final class SortedMapConstructorCase { private final String name; private final Class expectedType; @@ -397,6 +417,104 @@ public void testTreeMap(Fory fory) { copyCheck(fory, beanForMap); } + @Test + public void testIdentityHashMapSerializer() { + Fory fory = + builder().withXlang(false).withRefTracking(true).requireClassRegistration(false).build(); + Assert.assertEquals( + fory.getTypeResolver().getSerializerClass(IdentityHashMap.class), + MapSerializers.IdentityHashMapSerializer.class); + Assert.assertTrue( + MapSerializers.JDKCompatibleMapSerializer.class.isAssignableFrom( + MapSerializers.IdentityHashMapSerializer.class)); + + IdentityHashMap map = newIdentityHashMap(); + IdentityHashMap restored = (IdentityHashMap) serDe(fory, map); + assertIdentityHashMap(restored); + + IdentityHashMap copied = fory.copy(map); + Assert.assertNotSame(copied, map); + assertIdentityHashMap(copied); + } + + private static IdentityHashMap newIdentityHashMap() { + IdentityHashMap map = new IdentityHashMap<>(); + map.put(new String("a"), "first"); + map.put(new String("a"), "second"); + return map; + } + + private static void assertIdentityHashMap(IdentityHashMap map) { + Assert.assertEquals(map.size(), 2); + int equalAKeys = 0; + for (Object key : map.keySet()) { + if ("a".equals(key)) { + equalAKeys++; + } + } + Assert.assertEquals(equalAKeys, 2); + } + + @Test + public void testXlangTreeMapCopy() { + Fory fory = + builder().withXlang(true).withRefTracking(false).requireClassRegistration(true).build(); + TreeMap map = new TreeMap<>(Collections.reverseOrder()); + map.putAll(sortedMapInput()); + + Assert.assertTrue( + fory.getSerializer(TreeMap.class) instanceof MapSerializers.SortedMapSerializer); + TreeMap copy = fory.copy(map); + + Assert.assertNotSame(copy, map); + Assert.assertEquals(copy, map); + Assert.assertNotNull(copy.comparator()); + Assert.assertTrue(copy.comparator().compare("a", "b") > 0); + } + + @Test + public void testXlangTreeMapNaturalComparator() { + Fory fory = + builder().withXlang(true).withRefTracking(false).requireClassRegistration(true).build(); + TreeMap map = new TreeMap<>(Comparator.naturalOrder()); + map.putAll(sortedMapInput()); + + Object decoded = fory.deserialize(fory.serialize(map)); + + Assert.assertEquals(decoded, map); + } + + @Test + public void testXlangTreeMapComparatorWrite() { + Fory fory = + builder().withXlang(true).withRefTracking(false).requireClassRegistration(true).build(); + TreeMap map = new TreeMap<>(new MapKeyComparator()); + map.put(new NonComparableMapKey(1), 1); + + SerializationException exception = + Assert.expectThrows(SerializationException.class, () -> fory.serialize(map)); + Assert.assertTrue(exception.getCause() instanceof UnsupportedOperationException); + } + + @Test + public void testXlangTreeMapObjectCopy() { + Fory fory = + builder().withXlang(true).withRefTracking(false).requireClassRegistration(true).build(); + fory.register(NonComparableMapKey.class); + fory.register(MapKeyComparator.class); + TreeMap map = new TreeMap<>(new MapKeyComparator()); + map.put(new NonComparableMapKey(2), 2); + map.put(new NonComparableMapKey(1), 1); + + TreeMap copy = fory.copy(map); + + Assert.assertNotSame(copy, map); + Assert.assertEquals(copy.size(), map.size()); + Assert.assertNotNull(copy.comparator()); + Assert.assertEquals(copy.firstKey().id, 1); + Assert.assertEquals(copy.lastKey().id, 2); + } + @Test(dataProvider = "referenceTrackingConfig") public void testTreeMapConstructorMatrix(boolean referenceTrackingConfig) { Fory fory = @@ -428,7 +546,7 @@ public void testTreeMapConstructorMatrix(Fory fory) { } @Test(dataProvider = "referenceTrackingConfig") - public void testConcurrentSkipListMapConstructorMatrix(boolean referenceTrackingConfig) { + public void testSkipListMapCtorSerde(boolean referenceTrackingConfig) { Fory fory = builder() .withXlang(false) @@ -446,7 +564,7 @@ public void testConcurrentSkipListMapConstructorMatrix(boolean referenceTracking } @Test(dataProvider = "foryCopyConfig") - public void testConcurrentSkipListMapConstructorMatrix(Fory fory) { + public void testSkipListMapCtorCopy(Fory fory) { for (SortedMapConstructorCase testCase : concurrentSkipListMapConstructorCases()) { SortedMap original = testCase.factory.get(); assertSortedMapState(testCase, original); @@ -476,7 +594,7 @@ public ChildTreeMapWithComparator(Comparator comparator) { } @Test(dataProvider = "referenceTrackingConfig") - public void testSortedMapSubclassWithoutComparatorCtor(boolean referenceTrackingConfig) { + public void testSortedMapSubclassNoComparatorCtor(boolean referenceTrackingConfig) { Fory fory = builder() .withXlang(false) @@ -510,8 +628,7 @@ public void testSortedMapSubclassWithComparatorCtor(boolean referenceTrackingCon } @Test(dataProvider = "referenceTrackingConfig") - public void testSortedMapSubclassRegisteredWithSortedMapSerializer( - boolean referenceTrackingConfig) { + public void testSortedMapSubclassRegistered(boolean referenceTrackingConfig) { Fory fory = builder() .withXlang(false) @@ -531,8 +648,7 @@ public void testSortedMapSubclassRegisteredWithSortedMapSerializer( } @Test(dataProvider = "referenceTrackingConfig") - public void testSortedMapSubclassWithComparatorRegisteredWithSortedMapSerializer( - boolean referenceTrackingConfig) { + public void testSortedMapComparatorRegistered(boolean referenceTrackingConfig) { Fory fory = builder() .withXlang(false) @@ -852,7 +968,15 @@ public static MapFields createMapFieldsObject(Map map) { } public static class TestClass1ForDefaultMap extends AbstractMap { - private final Set data = new HashSet<>(); + private final Set data; + + public TestClass1ForDefaultMap() { + this(new HashSet<>()); + } + + public TestClass1ForDefaultMap(Set data) { + this.data = data; + } @Override public Set> entrySet() { @@ -867,7 +991,15 @@ public Object put(String key, Object value) { } public static class TestClass2ForDefaultMap extends AbstractMap { - private final Set> data = new HashSet<>(); + private final Set> data; + + public TestClass2ForDefaultMap() { + this(new HashSet<>()); + } + + public TestClass2ForDefaultMap(Set> data) { + this.data = data; + } @Override public Set> entrySet() { @@ -1053,7 +1185,7 @@ public void testStringKeyMapSerializer() { } @Test(dataProvider = "enableCodegen") - public void testMapElementRefOverrideReadRespectsHeader(boolean enableCodegen) { + public void testMapElementRefOverrideHeader(boolean enableCodegen) { Fory foryNoRef = builder() .withXlang(false) @@ -1204,10 +1336,15 @@ public int hashCode() { } @Data - @AllArgsConstructor public static class LazyMapCollectionFieldStruct { List> mapList; PrivateMap map; + + LazyMapCollectionFieldStruct( + List> mapList, PrivateMap map) { + this.mapList = mapList; + this.map = map; + } } @Data @@ -1414,8 +1551,18 @@ public void testNestedMapFieldStructCodegen(boolean referenceTrackingConfig) { @Data public static class PrivateFinalMapFieldStruct { - private final Map valueMap = new LinkedHashMap<>(); - private final Map keyMap = new LinkedHashMap<>(); + private final Map valueMap; + private final Map keyMap; + + public PrivateFinalMapFieldStruct() { + this(new LinkedHashMap<>(), new LinkedHashMap<>()); + } + + public PrivateFinalMapFieldStruct( + Map valueMap, Map keyMap) { + this.valueMap = valueMap; + this.keyMap = keyMap; + } } @Data diff --git a/java/fory-core/src/test/java/org/apache/fory/serializer/collection/SynchronizedSerializersTest.java b/java/fory-core/src/test/java/org/apache/fory/serializer/collection/SynchronizedSerializersTest.java index c12d5661c2..bd1d7fb95a 100644 --- a/java/fory-core/src/test/java/org/apache/fory/serializer/collection/SynchronizedSerializersTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/serializer/collection/SynchronizedSerializersTest.java @@ -39,21 +39,13 @@ import org.apache.fory.ForyTestBase; import org.apache.fory.memory.MemoryBuffer; import org.apache.fory.memory.MemoryUtils; -import org.apache.fory.platform.UnsafeOps; +import org.apache.fory.reflect.FieldAccessor; import org.apache.fory.reflect.ReflectionUtils; import org.apache.fory.serializer.Serializer; import org.apache.fory.test.bean.CollectionFields; import org.testng.annotations.Test; public class SynchronizedSerializersTest extends ForyTestBase { - - static long SOURCE_COLLECTION_FIELD_OFFSET = - ReflectionUtils.getFieldOffset( - Collections.synchronizedCollection(Collections.emptyList()).getClass(), "c"); - static long SOURCE_MAP_FIELD_OFFSET = - ReflectionUtils.getFieldOffset( - Collections.synchronizedMap(Collections.emptyMap()).getClass(), "m"); - @Test public void testWrite() throws Exception { Fory fory = Fory.builder().withXlang(false).requireClassRegistration(false).build(); @@ -77,16 +69,13 @@ public void testWrite() throws Exception { writeSerializer(fory, serializer, buffer, value); Object newObj = readSerializer(fory, serializer, buffer); assertEquals(newObj.getClass(), value.getClass()); - long sourceCollectionFieldOffset = - Collection.class.isAssignableFrom(value.getClass()) - ? SOURCE_COLLECTION_FIELD_OFFSET - : SOURCE_MAP_FIELD_OFFSET; - Object innerValue = UnsafeOps.getObject(value, sourceCollectionFieldOffset); - Object newValue = UnsafeOps.getObject(newObj, sourceCollectionFieldOffset); + FieldAccessor sourceAccessor = sourceAccessor(value.getClass()); + Object innerValue = sourceAccessor.getObject(value); + Object newValue = sourceAccessor.getObject(newObj); assertEquals(innerValue, newValue); newObj = serDe(fory, value); - innerValue = UnsafeOps.getObject(value, sourceCollectionFieldOffset); - newValue = UnsafeOps.getObject(newObj, sourceCollectionFieldOffset); + innerValue = sourceAccessor.getObject(value); + newValue = sourceAccessor.getObject(newObj); assertEquals(innerValue, newValue); assertTrue( fory.getTypeResolver() @@ -96,6 +85,11 @@ public void testWrite() throws Exception { } } + private static FieldAccessor sourceAccessor(Class cls) { + String fieldName = Collection.class.isAssignableFrom(cls) ? "c" : "m"; + return FieldAccessor.createAccessor(ReflectionUtils.getField(cls, fieldName)); + } + @Test(dataProvider = "javaFory") public void testCollectionFieldSerializers(Fory fory) { CollectionFields obj = createCollectionFields(); diff --git a/java/fory-core/src/test/java/org/apache/fory/serializer/collection/UnmodifiableSerializersTest.java b/java/fory-core/src/test/java/org/apache/fory/serializer/collection/UnmodifiableSerializersTest.java index d2fa2ae345..74ea6c9181 100644 --- a/java/fory-core/src/test/java/org/apache/fory/serializer/collection/UnmodifiableSerializersTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/serializer/collection/UnmodifiableSerializersTest.java @@ -45,7 +45,7 @@ import org.apache.fory.ForyTestBase; import org.apache.fory.memory.MemoryBuffer; import org.apache.fory.memory.MemoryUtils; -import org.apache.fory.platform.UnsafeOps; +import org.apache.fory.reflect.FieldAccessor; import org.apache.fory.reflect.ReflectionUtils; import org.apache.fory.serializer.Serializer; import org.apache.fory.test.bean.CollectionFields; @@ -54,13 +54,6 @@ import org.testng.annotations.Test; public class UnmodifiableSerializersTest extends ForyTestBase { - static long SOURCE_COLLECTION_FIELD_OFFSET = - ReflectionUtils.getFieldOffset( - Collections.synchronizedCollection(Collections.emptyList()).getClass(), "c"); - static long SOURCE_MAP_FIELD_OFFSET = - ReflectionUtils.getFieldOffset( - Collections.synchronizedMap(Collections.emptyMap()).getClass(), "m"); - @SuppressWarnings("unchecked") @Test public void testWrite() throws Exception { @@ -84,17 +77,14 @@ public void testWrite() throws Exception { writeSerializer(fory, serializer, buffer, value); Object newObj = readSerializer(fory, serializer, buffer); assertEquals(newObj.getClass(), value.getClass()); - long sourceCollectionFieldOffset = - Collection.class.isAssignableFrom(value.getClass()) - ? SOURCE_COLLECTION_FIELD_OFFSET - : SOURCE_MAP_FIELD_OFFSET; - Object innerValue = UnsafeOps.getObject(value, sourceCollectionFieldOffset); - Object newValue = UnsafeOps.getObject(newObj, sourceCollectionFieldOffset); + FieldAccessor sourceAccessor = sourceAccessor(value.getClass()); + Object innerValue = sourceAccessor.getObject(value); + Object newValue = sourceAccessor.getObject(newObj); assertEquals(innerValue, newValue); newObj = serDe(fory, value); - innerValue = UnsafeOps.getObject(value, sourceCollectionFieldOffset); - newValue = UnsafeOps.getObject(newObj, sourceCollectionFieldOffset); + innerValue = sourceAccessor.getObject(value); + newValue = sourceAccessor.getObject(newObj); assertEquals(innerValue, newValue); assertTrue( fory.getTypeResolver() @@ -104,6 +94,11 @@ public void testWrite() throws Exception { } } + private static FieldAccessor sourceAccessor(Class cls) { + String fieldName = Collection.class.isAssignableFrom(cls) ? "c" : "m"; + return FieldAccessor.createAccessor(ReflectionUtils.getField(cls, fieldName)); + } + public static CollectionFields createCollectionFields() { CollectionFields obj = new CollectionFields(); obj.collection = Collections.unmodifiableCollection(Arrays.asList(1, 2)); diff --git a/java/fory-core/src/test/java/org/apache/fory/serializer/otherpkg/PackageNoArgParent.java b/java/fory-core/src/test/java/org/apache/fory/serializer/otherpkg/PackageNoArgParent.java new file mode 100644 index 0000000000..a2555858a2 --- /dev/null +++ b/java/fory-core/src/test/java/org/apache/fory/serializer/otherpkg/PackageNoArgParent.java @@ -0,0 +1,26 @@ +/* + * 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.serializer.otherpkg; + +public class PackageNoArgParent { + PackageNoArgParent() {} + + public PackageNoArgParent(String ignored) {} +} diff --git a/java/fory-core/src/test/java/org/apache/fory/util/StringUtilsTest.java b/java/fory-core/src/test/java/org/apache/fory/util/StringUtilsTest.java index 7aa1893ee7..2fd109cb67 100644 --- a/java/fory-core/src/test/java/org/apache/fory/util/StringUtilsTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/util/StringUtilsTest.java @@ -24,12 +24,10 @@ import static org.testng.Assert.assertTrue; import org.apache.fory.ForyTestBase; -import org.apache.fory.memory.NativeByteOrder; -import org.apache.fory.platform.UnsafeOps; +import org.apache.fory.serializer.StringEncodingUtils; import org.testng.annotations.Test; public class StringUtilsTest extends ForyTestBase { - @Test public void testEncodeHexString() { assertEquals( @@ -105,71 +103,33 @@ public void testVectorizedLatinCheckAlgorithm(boolean endian) { } private boolean isLatin(char[] chars, boolean isLittle) { - boolean reverseBytes = - (NativeByteOrder.IS_LITTLE_ENDIAN && !isLittle) - || (!NativeByteOrder.IS_LITTLE_ENDIAN && !isLittle); - if (reverseBytes) { - for (int i = 0; i < chars.length; i++) { - chars[i] = Character.reverseBytes(chars[i]); - } - } - long mask; - if (isLittle) { - // latin chars will be 0xXX,0x00;0xXX,0x00 in byte order; - // Using 0x00,0xff(0xff00) to clear latin bits. - mask = 0xff00ff00ff00ff00L; - } else { - // latin chars will be 0x00,0xXX;0x00,0xXX in byte order; - // Using 0x00,0xff(0x00ff) to clear latin bits. - mask = 0x00ff00ff00ff00ffL; - } - int numChars = chars.length; - int vectorizedLen = numChars >> 2; - int vectorizedChars = vectorizedLen << 2; - int endOffset = UnsafeOps.CHAR_ARRAY_OFFSET + (vectorizedChars << 1); - boolean isLatin = true; - for (int offset = UnsafeOps.CHAR_ARRAY_OFFSET; offset < endOffset; offset += 8) { - // check 4 chars in a vectorized way, 4 times faster than scalar check loop. - long multiChars = UnsafeOps.getLong(chars, offset); - if ((multiChars & mask) != 0) { - isLatin = false; - break; - } - } - if (isLatin) { - for (int i = vectorizedChars; i < numChars; i++) { - char c = chars[i]; - if (reverseBytes) { - c = Character.reverseBytes(c); - } - if (c > 0xFF) { - isLatin = false; - break; - } + for (char c : chars) { + if (c > 0xFF) { + return false; } } - return isLatin; + return true; } @Test public void testLatinCheck() { - assertTrue(StringUtils.isLatin("Fory".toCharArray())); - assertTrue(StringUtils.isLatin(StringUtils.random(8 * 10).toCharArray())); + assertTrue(StringEncodingUtils.isLatin("Fory".toCharArray())); + assertTrue(StringEncodingUtils.isLatin(StringUtils.random(8 * 10).toCharArray())); // test unaligned - assertTrue(StringUtils.isLatin((StringUtils.random(8 * 10) + "1").toCharArray())); - assertTrue(StringUtils.isLatin((StringUtils.random(8 * 10) + "12").toCharArray())); - assertTrue(StringUtils.isLatin((StringUtils.random(8 * 10) + "123").toCharArray())); - assertFalse(StringUtils.isLatin("你好, Fory".toCharArray())); - assertFalse(StringUtils.isLatin((StringUtils.random(8 * 10) + "你好").toCharArray())); - assertFalse(StringUtils.isLatin((StringUtils.random(8 * 10) + "1你好").toCharArray())); - assertFalse(StringUtils.isLatin((StringUtils.random(11) + "你").toCharArray())); - assertFalse(StringUtils.isLatin((StringUtils.random(10) + "你好").toCharArray())); - assertFalse(StringUtils.isLatin((StringUtils.random(9) + "性能好").toCharArray())); - assertFalse(StringUtils.isLatin("\u1234".toCharArray())); - assertFalse(StringUtils.isLatin("a\u1234".toCharArray())); - assertFalse(StringUtils.isLatin("ab\u1234".toCharArray())); - assertFalse(StringUtils.isLatin("abc\u1234".toCharArray())); - assertFalse(StringUtils.isLatin("abcd\u1234".toCharArray())); - assertFalse(StringUtils.isLatin("Javaone Keynote\u1234".toCharArray())); + assertTrue(StringEncodingUtils.isLatin((StringUtils.random(8 * 10) + "1").toCharArray())); + assertTrue(StringEncodingUtils.isLatin((StringUtils.random(8 * 10) + "12").toCharArray())); + assertTrue(StringEncodingUtils.isLatin((StringUtils.random(8 * 10) + "123").toCharArray())); + assertFalse(StringEncodingUtils.isLatin("你好, Fory".toCharArray())); + assertFalse(StringEncodingUtils.isLatin((StringUtils.random(8 * 10) + "你好").toCharArray())); + assertFalse(StringEncodingUtils.isLatin((StringUtils.random(8 * 10) + "1你好").toCharArray())); + assertFalse(StringEncodingUtils.isLatin((StringUtils.random(11) + "你").toCharArray())); + assertFalse(StringEncodingUtils.isLatin((StringUtils.random(10) + "你好").toCharArray())); + assertFalse(StringEncodingUtils.isLatin((StringUtils.random(9) + "性能好").toCharArray())); + assertFalse(StringEncodingUtils.isLatin("\u1234".toCharArray())); + assertFalse(StringEncodingUtils.isLatin("a\u1234".toCharArray())); + assertFalse(StringEncodingUtils.isLatin("ab\u1234".toCharArray())); + assertFalse(StringEncodingUtils.isLatin("abc\u1234".toCharArray())); + assertFalse(StringEncodingUtils.isLatin("abcd\u1234".toCharArray())); + assertFalse(StringEncodingUtils.isLatin("Javaone Keynote\u1234".toCharArray())); } } diff --git a/java/fory-extensions/src/main/java/org/apache/fory/extension/serializer/ProtobufSerializer.java b/java/fory-extensions/src/main/java/org/apache/fory/extension/serializer/ProtobufSerializer.java index 194967d687..f10aa30bf1 100644 --- a/java/fory-extensions/src/main/java/org/apache/fory/extension/serializer/ProtobufSerializer.java +++ b/java/fory-extensions/src/main/java/org/apache/fory/extension/serializer/ProtobufSerializer.java @@ -35,7 +35,6 @@ import org.apache.fory.serializer.Serializer; import org.apache.fory.serializer.Shareable; import org.apache.fory.util.ExceptionUtils; -import org.apache.fory.util.unsafe._JDKAccess; @SuppressWarnings({"rawtypes", "unchecked"}) public class ProtobufSerializer extends Serializer implements Shareable { @@ -46,7 +45,7 @@ public class ProtobufSerializer extends Serializer implements Shareable @Override protected MethodHandle[] computeValue(Class type) { try { - MethodHandles.Lookup lookup = _JDKAccess._trustedLookup(type); + MethodHandles.Lookup lookup = MethodHandles.publicLookup(); MethodHandle parseFrom1 = lookup.findStatic( type, "parseFrom", MethodType.methodType(type, CodedInputStream.class)); diff --git a/java/fory-format/pom.xml b/java/fory-format/pom.xml index 9d5729468f..cc245465de 100644 --- a/java/fory-format/pom.xml +++ b/java/fory-format/pom.xml @@ -111,13 +111,6 @@ - - org.apache.maven.plugins - maven-surefire-plugin - - --add-opens=java.base/java.nio=ALL-UNNAMED - - @@ -143,9 +136,24 @@ + + + + + + + - - + + + + + @@ -245,6 +253,15 @@ + + + + true + org.apache.fory.format + + + + diff --git a/java/fory-format/src/main/java/org/apache/fory/format/row/binary/BinaryArray.java b/java/fory-format/src/main/java/org/apache/fory/format/row/binary/BinaryArray.java index 6d65c53393..d1acd2e6ac 100644 --- a/java/fory-format/src/main/java/org/apache/fory/format/row/binary/BinaryArray.java +++ b/java/fory-format/src/main/java/org/apache/fory/format/row/binary/BinaryArray.java @@ -34,7 +34,7 @@ import org.apache.fory.memory.BitUtils; import org.apache.fory.memory.MemoryBuffer; import org.apache.fory.memory.MemoryUtils; -import org.apache.fory.platform.UnsafeOps; +import org.apache.fory.memory.NativeByteOrder; import org.apache.fory.util.Preconditions; /** @@ -47,6 +47,10 @@ *

Primitive type is always considered to be not null. */ public class BinaryArray extends UnsafeTrait implements ArrayData { + // Row-format stores multi-byte primitive values in little-endian order. MemoryBuffer typed + // array copies are native/raw copies, so big-endian runtimes must use element accessors. + private static final boolean LITTLE_ENDIAN = NativeByteOrder.IS_LITTLE_ENDIAN; + private final Field field; protected final int elementSize; private MemoryBuffer buffer; @@ -188,43 +192,73 @@ public byte[] toBytes() { public boolean[] toBooleanArray() { boolean[] values = new boolean[numElements]; - buffer.copyToUnsafe(elementOffset, values, UnsafeOps.BOOLEAN_ARRAY_OFFSET, numElements); + buffer.copyToBooleanArray(elementOffset, values, 0, numElements); return values; } public byte[] toByteArray() { byte[] values = new byte[numElements]; - buffer.copyToUnsafe(elementOffset, values, UnsafeOps.BYTE_ARRAY_OFFSET, numElements); + buffer.copyToByteArray(elementOffset, values, 0, numElements); return values; } public short[] toShortArray() { short[] values = new short[numElements]; - buffer.copyToUnsafe(elementOffset, values, UnsafeOps.SHORT_ARRAY_OFFSET, numElements * 2); + if (LITTLE_ENDIAN) { + buffer.copyToShortArray(elementOffset, values, 0, numElements * 2); + } else { + for (int i = 0, offset = elementOffset; i < numElements; i++, offset += 2) { + values[i] = buffer.getInt16(offset); + } + } return values; } public int[] toIntArray() { int[] values = new int[numElements]; - buffer.copyToUnsafe(elementOffset, values, UnsafeOps.INT_ARRAY_OFFSET, numElements * 4); + if (LITTLE_ENDIAN) { + buffer.copyToIntArray(elementOffset, values, 0, numElements * 4); + } else { + for (int i = 0, offset = elementOffset; i < numElements; i++, offset += 4) { + values[i] = buffer.getInt32(offset); + } + } return values; } public long[] toLongArray() { long[] values = new long[numElements]; - buffer.copyToUnsafe(elementOffset, values, UnsafeOps.LONG_ARRAY_OFFSET, numElements * 8); + if (LITTLE_ENDIAN) { + buffer.copyToLongArray(elementOffset, values, 0, numElements * 8); + } else { + for (int i = 0, offset = elementOffset; i < numElements; i++, offset += 8) { + values[i] = buffer.getInt64(offset); + } + } return values; } public float[] toFloatArray() { float[] values = new float[numElements]; - buffer.copyToUnsafe(elementOffset, values, UnsafeOps.FLOAT_ARRAY_OFFSET, numElements * 4); + if (LITTLE_ENDIAN) { + buffer.copyToFloatArray(elementOffset, values, 0, numElements * 4); + } else { + for (int i = 0, offset = elementOffset; i < numElements; i++, offset += 4) { + values[i] = buffer.getFloat32(offset); + } + } return values; } public double[] toDoubleArray() { double[] values = new double[numElements]; - buffer.copyToUnsafe(elementOffset, values, UnsafeOps.DOUBLE_ARRAY_OFFSET, numElements * 8); + if (LITTLE_ENDIAN) { + buffer.copyToDoubleArray(elementOffset, values, 0, numElements * 8); + } else { + for (int i = 0, offset = elementOffset; i < numElements; i++, offset += 8) { + values[i] = buffer.getFloat64(offset); + } + } return values; } @@ -253,7 +287,7 @@ public String toString() { return builder.toString(); } - private static BinaryArray fromPrimitiveArray(Object arr, int offset, int length, Field field) { + private static BinaryArray newPrimitiveArray(int length, Field field) { BinaryArray result = new BinaryArray(field); final long headerInBytes = calculateHeaderInBytes(length); final long valueRegionInBytes = result.elementSize * length; @@ -264,48 +298,82 @@ private static BinaryArray fromPrimitiveArray(Object arr, int offset, int length } final byte[] data = new byte[(int) totalSize]; - UnsafeOps.putLong(data, UnsafeOps.BYTE_ARRAY_OFFSET, length); - UnsafeOps.copyMemory( - arr, offset, data, UnsafeOps.BYTE_ARRAY_OFFSET + headerInBytes, valueRegionInBytes); - MemoryBuffer memoryBuffer = MemoryUtils.wrap(data); + memoryBuffer.putInt64(0, length); result.pointTo(memoryBuffer, 0, (int) totalSize); return result; } public static BinaryArray fromPrimitiveArray(byte[] arr) { - return fromPrimitiveArray( - arr, UnsafeOps.BYTE_ARRAY_OFFSET, arr.length, PRIMITIVE_BYTE_ARRAY_FIELD); + BinaryArray result = newPrimitiveArray(arr.length, PRIMITIVE_BYTE_ARRAY_FIELD); + result.buffer.copyFromByteArray(result.elementOffset, arr, 0, arr.length); + return result; } public static BinaryArray fromPrimitiveArray(boolean[] arr) { - return fromPrimitiveArray( - arr, UnsafeOps.BOOLEAN_ARRAY_OFFSET, arr.length, PRIMITIVE_BOOLEAN_ARRAY_FIELD); + BinaryArray result = newPrimitiveArray(arr.length, PRIMITIVE_BOOLEAN_ARRAY_FIELD); + result.buffer.copyFromBooleanArray(result.elementOffset, arr, 0, arr.length); + return result; } public static BinaryArray fromPrimitiveArray(short[] arr) { - return fromPrimitiveArray( - arr, UnsafeOps.SHORT_ARRAY_OFFSET, arr.length, PRIMITIVE_SHORT_ARRAY_FIELD); + BinaryArray result = newPrimitiveArray(arr.length, PRIMITIVE_SHORT_ARRAY_FIELD); + if (LITTLE_ENDIAN) { + result.buffer.copyFromShortArray(result.elementOffset, arr, 0, arr.length * 2); + } else { + for (int i = 0, offset = result.elementOffset; i < arr.length; i++, offset += 2) { + result.buffer.putInt16(offset, arr[i]); + } + } + return result; } public static BinaryArray fromPrimitiveArray(int[] arr) { - return fromPrimitiveArray( - arr, UnsafeOps.INT_ARRAY_OFFSET, arr.length, PRIMITIVE_INT_ARRAY_FIELD); + BinaryArray result = newPrimitiveArray(arr.length, PRIMITIVE_INT_ARRAY_FIELD); + if (LITTLE_ENDIAN) { + result.buffer.copyFromIntArray(result.elementOffset, arr, 0, arr.length * 4); + } else { + for (int i = 0, offset = result.elementOffset; i < arr.length; i++, offset += 4) { + result.buffer.putInt32(offset, arr[i]); + } + } + return result; } public static BinaryArray fromPrimitiveArray(long[] arr) { - return fromPrimitiveArray( - arr, UnsafeOps.LONG_ARRAY_OFFSET, arr.length, PRIMITIVE_LONG_ARRAY_FIELD); + BinaryArray result = newPrimitiveArray(arr.length, PRIMITIVE_LONG_ARRAY_FIELD); + if (LITTLE_ENDIAN) { + result.buffer.copyFromLongArray(result.elementOffset, arr, 0, arr.length * 8); + } else { + for (int i = 0, offset = result.elementOffset; i < arr.length; i++, offset += 8) { + result.buffer.putInt64(offset, arr[i]); + } + } + return result; } public static BinaryArray fromPrimitiveArray(float[] arr) { - return fromPrimitiveArray( - arr, UnsafeOps.FLOAT_ARRAY_OFFSET, arr.length, PRIMITIVE_FLOAT_ARRAY_FIELD); + BinaryArray result = newPrimitiveArray(arr.length, PRIMITIVE_FLOAT_ARRAY_FIELD); + if (LITTLE_ENDIAN) { + result.buffer.copyFromFloatArray(result.elementOffset, arr, 0, arr.length * 4); + } else { + for (int i = 0, offset = result.elementOffset; i < arr.length; i++, offset += 4) { + result.buffer.putFloat32(offset, arr[i]); + } + } + return result; } public static BinaryArray fromPrimitiveArray(double[] arr) { - return fromPrimitiveArray( - arr, UnsafeOps.DOUBLE_ARRAY_OFFSET, arr.length, PRIMITIVE_DOUBLE_ARRAY_FIELD); + BinaryArray result = newPrimitiveArray(arr.length, PRIMITIVE_DOUBLE_ARRAY_FIELD); + if (LITTLE_ENDIAN) { + result.buffer.copyFromDoubleArray(result.elementOffset, arr, 0, arr.length * 8); + } else { + for (int i = 0, offset = result.elementOffset; i < arr.length; i++, offset += 8) { + result.buffer.putFloat64(offset, arr[i]); + } + } + return result; } public static int calculateHeaderInBytes(int numElements) { diff --git a/java/fory-format/src/main/java/org/apache/fory/format/row/binary/BinaryMap.java b/java/fory-format/src/main/java/org/apache/fory/format/row/binary/BinaryMap.java index d607c2fe5b..6469965caf 100644 --- a/java/fory-format/src/main/java/org/apache/fory/format/row/binary/BinaryMap.java +++ b/java/fory-format/src/main/java/org/apache/fory/format/row/binary/BinaryMap.java @@ -25,7 +25,6 @@ import org.apache.fory.format.type.Field; import org.apache.fory.memory.MemoryBuffer; import org.apache.fory.memory.MemoryUtils; -import org.apache.fory.platform.UnsafeOps; /** * An BinaryMap implementation of Map which is backed by two BinaryArray./ForyStructOutput @@ -124,8 +123,8 @@ public MapData copy() { return mapCopy; } - public void writeToMemory(Object target, long targetOffset) { - buf.copyToUnsafe(baseOffset, target, targetOffset, sizeInBytes); + public void writeTo(byte[] target, int targetOffset) { + buf.copyToByteArray(baseOffset, target, targetOffset, sizeInBytes); } public void writeTo(ByteBuffer buffer) { @@ -133,7 +132,7 @@ public void writeTo(ByteBuffer buffer) { byte[] target = buffer.array(); int offset = buffer.arrayOffset(); int pos = buffer.position(); - writeToMemory(target, UnsafeOps.BYTE_ARRAY_OFFSET + offset + pos); + writeTo(target, offset + pos); buffer.position(pos + sizeInBytes); } diff --git a/java/fory-format/src/main/java/org/apache/fory/format/row/binary/writer/BinaryArrayWriter.java b/java/fory-format/src/main/java/org/apache/fory/format/row/binary/writer/BinaryArrayWriter.java index ed656b6329..7aff4a387e 100644 --- a/java/fory-format/src/main/java/org/apache/fory/format/row/binary/writer/BinaryArrayWriter.java +++ b/java/fory-format/src/main/java/org/apache/fory/format/row/binary/writer/BinaryArrayWriter.java @@ -33,7 +33,7 @@ import org.apache.fory.format.type.Field; import org.apache.fory.memory.MemoryBuffer; import org.apache.fory.memory.MemoryUtils; -import org.apache.fory.platform.UnsafeOps; +import org.apache.fory.memory.NativeByteOrder; /** * Writer for binary array. See {@link BinaryArray} @@ -45,6 +45,8 @@ * fromPrimitiveArray. */ public class BinaryArrayWriter extends BinaryWriter { + private static final boolean LITTLE_ENDIAN = NativeByteOrder.IS_LITTLE_ENDIAN; + public static int MAX_ROUNDED_ARRAY_LENGTH = Integer.MAX_VALUE - 15; protected final Field field; @@ -184,7 +186,7 @@ protected void primitiveArrayAdvance(int size) { // no need to increasewriterIndex, because reset has already increased writerIndex } - private void fromPrimitiveArray(Object arr, int offset, int numElements, Field type) { + private void checkPrimitiveArrayType(Field type) { DataTypes.ListType inputListType = (DataTypes.ListType) type.type(); DataTypes.ListType thisListType = (DataTypes.ListType) this.field.type(); if (DataTypes.getTypeId(inputListType.valueType()) @@ -194,39 +196,88 @@ private void fromPrimitiveArray(Object arr, int offset, int numElements, Field t "Element type %s is not %s", inputListType.valueType(), thisListType.valueType()); throw new IllegalArgumentException(msg); } + } + + private void finishPrimitiveArray(int numElements) { int size = numElements * elementSize; - buffer.copyFromUnsafe(startIndex + headerInBytes, arr, offset, size); primitiveArrayAdvance(size); } public void fromPrimitiveArray(byte[] arr) { - fromPrimitiveArray(arr, UnsafeOps.BYTE_ARRAY_OFFSET, arr.length, PRIMITIVE_BYTE_ARRAY_FIELD); + checkPrimitiveArrayType(PRIMITIVE_BYTE_ARRAY_FIELD); + buffer.copyFromByteArray(startIndex + headerInBytes, arr, 0, arr.length); + finishPrimitiveArray(arr.length); } public void fromPrimitiveArray(boolean[] arr) { - fromPrimitiveArray( - arr, UnsafeOps.BOOLEAN_ARRAY_OFFSET, arr.length, PRIMITIVE_BOOLEAN_ARRAY_FIELD); + checkPrimitiveArrayType(PRIMITIVE_BOOLEAN_ARRAY_FIELD); + buffer.copyFromBooleanArray(startIndex + headerInBytes, arr, 0, arr.length); + finishPrimitiveArray(arr.length); } public void fromPrimitiveArray(short[] arr) { - fromPrimitiveArray(arr, UnsafeOps.SHORT_ARRAY_OFFSET, arr.length, PRIMITIVE_SHORT_ARRAY_FIELD); + checkPrimitiveArrayType(PRIMITIVE_SHORT_ARRAY_FIELD); + int offset = startIndex + headerInBytes; + if (LITTLE_ENDIAN) { + buffer.copyFromShortArray(offset, arr, 0, arr.length * 2); + } else { + for (int i = 0; i < arr.length; i++, offset += 2) { + buffer.putInt16(offset, arr[i]); + } + } + finishPrimitiveArray(arr.length); } public void fromPrimitiveArray(int[] arr) { - fromPrimitiveArray(arr, UnsafeOps.INT_ARRAY_OFFSET, arr.length, PRIMITIVE_INT_ARRAY_FIELD); + checkPrimitiveArrayType(PRIMITIVE_INT_ARRAY_FIELD); + int offset = startIndex + headerInBytes; + if (LITTLE_ENDIAN) { + buffer.copyFromIntArray(offset, arr, 0, arr.length * 4); + } else { + for (int i = 0; i < arr.length; i++, offset += 4) { + buffer.putInt32(offset, arr[i]); + } + } + finishPrimitiveArray(arr.length); } public void fromPrimitiveArray(long[] arr) { - fromPrimitiveArray(arr, UnsafeOps.LONG_ARRAY_OFFSET, arr.length, PRIMITIVE_LONG_ARRAY_FIELD); + checkPrimitiveArrayType(PRIMITIVE_LONG_ARRAY_FIELD); + int offset = startIndex + headerInBytes; + if (LITTLE_ENDIAN) { + buffer.copyFromLongArray(offset, arr, 0, arr.length * 8); + } else { + for (int i = 0; i < arr.length; i++, offset += 8) { + buffer.putInt64(offset, arr[i]); + } + } + finishPrimitiveArray(arr.length); } public void fromPrimitiveArray(float[] arr) { - fromPrimitiveArray(arr, UnsafeOps.FLOAT_ARRAY_OFFSET, arr.length, PRIMITIVE_FLOAT_ARRAY_FIELD); + checkPrimitiveArrayType(PRIMITIVE_FLOAT_ARRAY_FIELD); + int offset = startIndex + headerInBytes; + if (LITTLE_ENDIAN) { + buffer.copyFromFloatArray(offset, arr, 0, arr.length * 4); + } else { + for (int i = 0; i < arr.length; i++, offset += 4) { + buffer.putFloat32(offset, arr[i]); + } + } + finishPrimitiveArray(arr.length); } public void fromPrimitiveArray(double[] arr) { - fromPrimitiveArray( - arr, UnsafeOps.DOUBLE_ARRAY_OFFSET, arr.length, PRIMITIVE_DOUBLE_ARRAY_FIELD); + checkPrimitiveArrayType(PRIMITIVE_DOUBLE_ARRAY_FIELD); + int offset = startIndex + headerInBytes; + if (LITTLE_ENDIAN) { + buffer.copyFromDoubleArray(offset, arr, 0, arr.length * 8); + } else { + for (int i = 0; i < arr.length; i++, offset += 8) { + buffer.putFloat64(offset, arr[i]); + } + } + finishPrimitiveArray(arr.length); } public BinaryArray toArray() { diff --git a/java/fory-format/src/main/java/org/apache/fory/format/vectorized/ArrowUtils.java b/java/fory-format/src/main/java/org/apache/fory/format/vectorized/ArrowUtils.java index 0d93894bf5..1daa138394 100644 --- a/java/fory-format/src/main/java/org/apache/fory/format/vectorized/ArrowUtils.java +++ b/java/fory-format/src/main/java/org/apache/fory/format/vectorized/ArrowUtils.java @@ -37,15 +37,44 @@ /** Arrow utils. */ public class ArrowUtils { + private static final RuntimeException ALLOCATOR_ERROR; + // RootAllocator is thread-safe, so we don't have to use thread-local. - // FIXME JDK17: Unable to make field long java.nio.Buffer.address - // accessible: module java.base does not "opens java.nio" to unnamed module @405e4200 - public static RootAllocator allocator = new RootAllocator(); + // Arrow 18.x initializes its own sun.misc.Unsafe memory facade eagerly. Keep Fory class loading + // possible under JDK25 deny mode and fail only when the vectorized Arrow path is used. + public static RootAllocator allocator; + + static { + RootAllocator rootAllocator = null; + RuntimeException allocatorError = null; + if (isUnsafeMemoryDenied()) { + allocatorError = + new UnsupportedOperationException( + "Apache Arrow vectorized format is unavailable when JDK Unsafe memory access is " + + "denied. Apache Arrow initializes sun.misc.Unsafe memory access internally."); + } else { + try { + rootAllocator = new RootAllocator(); + } catch (RuntimeException | ExceptionInInitializerError e) { + if (!isUnsafeMemoryAccessFailure(e)) { + throw e; + } + allocatorError = + new UnsupportedOperationException( + "Apache Arrow vectorized format is unavailable when Apache Arrow cannot initialize " + + "its sun.misc.Unsafe memory access.", + e); + } + } + allocator = rootAllocator; + ALLOCATOR_ERROR = allocatorError; + } + private static final ThreadLocal decimalArrowBuf = ThreadLocal.withInitial(() -> buffer(DecimalUtils.DECIMAL_BYTE_LENGTH)); public static ArrowBuf buffer(final long initialRequestSize) { - return allocator.buffer(initialRequestSize); + return requireAllocator().buffer(initialRequestSize); } public static ArrowBuf decimalArrowBuf() { @@ -53,22 +82,23 @@ public static ArrowBuf decimalArrowBuf() { } public static VectorSchemaRoot createVectorSchemaRoot(Schema arrowSchema) { - return VectorSchemaRoot.create(arrowSchema, allocator); + return VectorSchemaRoot.create(arrowSchema, requireAllocator()); } public static VectorSchemaRoot createVectorSchemaRoot( org.apache.fory.format.type.Schema forySchema) { - return VectorSchemaRoot.create(ArrowSchemaConverter.toArrowSchema(forySchema), allocator); + return VectorSchemaRoot.create( + ArrowSchemaConverter.toArrowSchema(forySchema), requireAllocator()); } public static ArrowWriter createArrowWriter(Schema arrowSchema) { - VectorSchemaRoot root = VectorSchemaRoot.create(arrowSchema, allocator); + VectorSchemaRoot root = VectorSchemaRoot.create(arrowSchema, requireAllocator()); return new ArrowWriter(root); } public static ArrowWriter createArrowWriter(org.apache.fory.format.type.Schema forySchema) { VectorSchemaRoot root = - VectorSchemaRoot.create(ArrowSchemaConverter.toArrowSchema(forySchema), allocator); + VectorSchemaRoot.create(ArrowSchemaConverter.toArrowSchema(forySchema), requireAllocator()); return new ArrowWriter(root); } @@ -87,9 +117,46 @@ public static ArrowRecordBatch deserializeRecordBatch(MemoryBuffer recordBatchMe try (ReadChannel channel = new ReadChannel( Channels.newChannel(new MemoryBufferInputStream(recordBatchMessageBuffer)))) { - return MessageSerializer.deserializeRecordBatch(channel, allocator); + return MessageSerializer.deserializeRecordBatch(channel, requireAllocator()); } catch (IOException e) { throw new RuntimeException("Deserialize record batch failed", e); } } + + private static RootAllocator requireAllocator() { + if (allocator != null) { + return allocator; + } + throw ALLOCATOR_ERROR; + } + + private static boolean isUnsafeMemoryDenied() { + return Runtime.version().feature() >= 25 + && "deny".equals(System.getProperty("sun.misc.unsafe.memory.access")); + } + + private static boolean isUnsafeMemoryAccessFailure(Throwable throwable) { + Throwable cause = throwable; + while (cause != null) { + if (cause instanceof UnsupportedOperationException + && cause.getMessage() != null + && cause.getMessage().contains("arrayBaseOffset")) { + return true; + } + if (cause instanceof UnsupportedOperationException) { + for (StackTraceElement element : cause.getStackTrace()) { + if ("sun.misc.Unsafe".equals(element.getClassName())) { + return true; + } + } + } + if ("java.lang.reflect.InaccessibleObjectException".equals(cause.getClass().getName()) + && cause.getMessage() != null + && cause.getMessage().contains("java.nio")) { + return true; + } + cause = cause.getCause(); + } + return false; + } } diff --git a/java/fory-format/src/test/java/org/apache/fory/format/row/binary/BinaryArrayTest.java b/java/fory-format/src/test/java/org/apache/fory/format/row/binary/BinaryArrayTest.java index 06aaa9d262..2e73dce860 100644 --- a/java/fory-format/src/test/java/org/apache/fory/format/row/binary/BinaryArrayTest.java +++ b/java/fory-format/src/test/java/org/apache/fory/format/row/binary/BinaryArrayTest.java @@ -19,6 +19,7 @@ package org.apache.fory.format.row.binary; +import java.util.Arrays; import java.util.Random; import org.apache.fory.format.row.binary.writer.BinaryArrayWriter; import org.apache.fory.format.type.DataTypes; @@ -40,6 +41,109 @@ public void fromPrimitiveArray() { writer.toArray(); } + @Test + public void primitiveArrayRoundTrip() { + assertRoundTrip(new byte[] {1, -2, 3}); + assertRoundTrip(new boolean[] {true, false, true}); + assertRoundTrip(new short[] {1, -2, Short.MAX_VALUE}); + assertRoundTrip(new int[] {1, -2, Integer.MAX_VALUE}); + assertRoundTrip(new long[] {1L, -2L, Long.MAX_VALUE}); + assertRoundTrip(new float[] {1.25f, -2.5f, Float.MAX_VALUE}); + assertRoundTrip(new double[] {1.25d, -2.5d, Double.MAX_VALUE}); + } + + @Test + public void primitiveWireEndian() { + assertValueBytes( + BinaryArray.fromPrimitiveArray(new short[] {(short) 0x1234}), bytes(0x34, 0x12)); + assertValueBytes( + BinaryArray.fromPrimitiveArray(new int[] {0x12345678}), bytes(0x78, 0x56, 0x34, 0x12)); + assertValueBytes( + BinaryArray.fromPrimitiveArray(new long[] {0x0102030405060708L}), + bytes(0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01)); + assertValueBytes( + BinaryArray.fromPrimitiveArray(new float[] {Float.intBitsToFloat(0x12345678)}), + bytes(0x78, 0x56, 0x34, 0x12)); + assertValueBytes( + BinaryArray.fromPrimitiveArray(new double[] {Double.longBitsToDouble(0x0102030405060708L)}), + bytes(0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01)); + + BinaryArrayWriter writer = new BinaryArrayWriter(DataTypes.PRIMITIVE_INT_ARRAY_FIELD); + writer.reset(1); + writer.fromPrimitiveArray(new int[] {0x12345678}); + assertValueBytes(writer.toArray(), bytes(0x78, 0x56, 0x34, 0x12)); + } + + private static void assertRoundTrip(byte[] arr) { + Assert.assertEquals(BinaryArray.fromPrimitiveArray(arr).toByteArray(), arr); + BinaryArrayWriter writer = new BinaryArrayWriter(DataTypes.PRIMITIVE_BYTE_ARRAY_FIELD); + writer.reset(arr.length); + writer.fromPrimitiveArray(arr); + Assert.assertEquals(writer.toArray().toByteArray(), arr); + } + + private static void assertRoundTrip(boolean[] arr) { + Assert.assertEquals(BinaryArray.fromPrimitiveArray(arr).toBooleanArray(), arr); + BinaryArrayWriter writer = new BinaryArrayWriter(DataTypes.PRIMITIVE_BOOLEAN_ARRAY_FIELD); + writer.reset(arr.length); + writer.fromPrimitiveArray(arr); + Assert.assertEquals(writer.toArray().toBooleanArray(), arr); + } + + private static void assertRoundTrip(short[] arr) { + Assert.assertEquals(BinaryArray.fromPrimitiveArray(arr).toShortArray(), arr); + BinaryArrayWriter writer = new BinaryArrayWriter(DataTypes.PRIMITIVE_SHORT_ARRAY_FIELD); + writer.reset(arr.length); + writer.fromPrimitiveArray(arr); + Assert.assertEquals(writer.toArray().toShortArray(), arr); + } + + private static void assertRoundTrip(int[] arr) { + Assert.assertEquals(BinaryArray.fromPrimitiveArray(arr).toIntArray(), arr); + BinaryArrayWriter writer = new BinaryArrayWriter(DataTypes.PRIMITIVE_INT_ARRAY_FIELD); + writer.reset(arr.length); + writer.fromPrimitiveArray(arr); + Assert.assertEquals(writer.toArray().toIntArray(), arr); + } + + private static void assertRoundTrip(long[] arr) { + Assert.assertEquals(BinaryArray.fromPrimitiveArray(arr).toLongArray(), arr); + BinaryArrayWriter writer = new BinaryArrayWriter(DataTypes.PRIMITIVE_LONG_ARRAY_FIELD); + writer.reset(arr.length); + writer.fromPrimitiveArray(arr); + Assert.assertEquals(writer.toArray().toLongArray(), arr); + } + + private static void assertRoundTrip(float[] arr) { + Assert.assertEquals(BinaryArray.fromPrimitiveArray(arr).toFloatArray(), arr); + BinaryArrayWriter writer = new BinaryArrayWriter(DataTypes.PRIMITIVE_FLOAT_ARRAY_FIELD); + writer.reset(arr.length); + writer.fromPrimitiveArray(arr); + Assert.assertEquals(writer.toArray().toFloatArray(), arr); + } + + private static void assertRoundTrip(double[] arr) { + Assert.assertEquals(BinaryArray.fromPrimitiveArray(arr).toDoubleArray(), arr); + BinaryArrayWriter writer = new BinaryArrayWriter(DataTypes.PRIMITIVE_DOUBLE_ARRAY_FIELD); + writer.reset(arr.length); + writer.fromPrimitiveArray(arr); + Assert.assertEquals(writer.toArray().toDoubleArray(), arr); + } + + private static void assertValueBytes(BinaryArray array, byte[] expected) { + byte[] bytes = array.toBytes(); + int offset = BinaryArray.calculateHeaderInBytes(1); + Assert.assertEquals(Arrays.copyOfRange(bytes, offset, offset + expected.length), expected); + } + + private static byte[] bytes(int... values) { + byte[] bytes = new byte[values.length]; + for (int i = 0; i < values.length; i++) { + bytes[i] = (byte) values[i]; + } + return bytes; + } + private int elem; @Test(enabled = false) diff --git a/java/fory-format/src/test/java/org/apache/fory/format/row/binary/BinaryRowTest.java b/java/fory-format/src/test/java/org/apache/fory/format/row/binary/BinaryRowTest.java index 803ce833f5..a68da3bb23 100644 --- a/java/fory-format/src/test/java/org/apache/fory/format/row/binary/BinaryRowTest.java +++ b/java/fory-format/src/test/java/org/apache/fory/format/row/binary/BinaryRowTest.java @@ -31,7 +31,6 @@ import org.apache.fory.logging.LoggerFactory; import org.apache.fory.memory.MemoryBuffer; import org.apache.fory.memory.MemoryUtils; -import org.apache.fory.platform.UnsafeOps; import org.testng.annotations.Test; public class BinaryRowTest { @@ -86,13 +85,14 @@ public void testOffsetAccessPerf() { offsetsArray[i] = i; } - byte[] bytes = new byte[numFields]; + int headerInBytes = 64; + byte[] bytes = new byte[headerInBytes + 8 * numFields]; int iterNums = 1000_000_000; // warm for (int i = 0; i < iterNums; i++) { for (int j = 0; j < numFields; j++) { int tmp = offsetsArray[j]; - UnsafeOps.getByte(bytes, tmp); + byte ignored = bytes[tmp]; } } // test access offset array @@ -100,18 +100,17 @@ public void testOffsetAccessPerf() { for (int i = 0; i < iterNums; i++) { for (int j = 0; j < numFields; j++) { int tmp = offsetsArray[j]; - UnsafeOps.getByte(bytes, tmp); + byte ignored = bytes[tmp]; } } long duration = System.nanoTime() - startTime; LOG.info("Array access offset take " + duration + "ns, " + duration / 1000_000 + " ms\n"); - int headerInBytes = 64; // warm for (int i = 0; i < iterNums; i++) { for (int j = 0; j < numFields; j++) { int tmp = headerInBytes + 8 * j; - UnsafeOps.getByte(bytes, tmp); + byte ignored = bytes[tmp]; } } // test calc offset @@ -119,7 +118,7 @@ public void testOffsetAccessPerf() { for (int i = 0; i < iterNums; i++) { for (int j = 0; j < numFields; j++) { int tmp = headerInBytes + 8 * j; - UnsafeOps.getByte(bytes, tmp); + byte ignored = bytes[tmp]; } } duration = System.nanoTime() - startTime; diff --git a/java/fory-format/src/test/java/org/apache/fory/format/vectorized/ArrowTestSupport.java b/java/fory-format/src/test/java/org/apache/fory/format/vectorized/ArrowTestSupport.java new file mode 100644 index 0000000000..ff46da5bca --- /dev/null +++ b/java/fory-format/src/test/java/org/apache/fory/format/vectorized/ArrowTestSupport.java @@ -0,0 +1,35 @@ +/* + * 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.format.vectorized; + +import org.apache.arrow.memory.ArrowBuf; +import org.testng.SkipException; + +final class ArrowTestSupport { + private ArrowTestSupport() {} + + static void skipIfArrowUnavailable() { + try (ArrowBuf ignored = ArrowUtils.buffer(0)) { + // No-op. This verifies that Arrow's allocator can initialize in this JVM mode. + } catch (UnsupportedOperationException e) { + throw new SkipException(e.getMessage(), e); + } + } +} diff --git a/java/fory-format/src/test/java/org/apache/fory/format/vectorized/ArrowUtilsTest.java b/java/fory-format/src/test/java/org/apache/fory/format/vectorized/ArrowUtilsTest.java index 48a00cc5aa..4b8079734c 100644 --- a/java/fory-format/src/test/java/org/apache/fory/format/vectorized/ArrowUtilsTest.java +++ b/java/fory-format/src/test/java/org/apache/fory/format/vectorized/ArrowUtilsTest.java @@ -53,6 +53,7 @@ public static VectorSchemaRoot createVectorSchemaRoot(int size) { @Test public void testSerializeRecordBatch() { + ArrowTestSupport.skipIfArrowUnavailable(); VectorSchemaRoot vectorSchemaRoot = createVectorSchemaRoot(2); VectorUnloader unloader = new VectorUnloader(vectorSchemaRoot); ArrowRecordBatch recordBatch = unloader.getRecordBatch(); diff --git a/java/fory-format/src/test/java/org/apache/fory/format/vectorized/ArrowWriterTest.java b/java/fory-format/src/test/java/org/apache/fory/format/vectorized/ArrowWriterTest.java index 66df4ff5d2..ffc83f2fde 100644 --- a/java/fory-format/src/test/java/org/apache/fory/format/vectorized/ArrowWriterTest.java +++ b/java/fory-format/src/test/java/org/apache/fory/format/vectorized/ArrowWriterTest.java @@ -60,6 +60,7 @@ private ArrowRecordBatch createArrowRecordBatch() { @Test public void testWrite() { + ArrowTestSupport.skipIfArrowUnavailable(); ArrowRecordBatch recordBatch = createArrowRecordBatch(); System.out.println("recordBatch " + recordBatch); recordBatch.close(); @@ -67,6 +68,7 @@ public void testWrite() { @Test public void testSerializeArrowRecordBatch() { + ArrowTestSupport.skipIfArrowUnavailable(); ArrowRecordBatch recordBatch = createArrowRecordBatch(); System.out.println("recordBatch serialized body size " + recordBatch.computeBodyLength()); MemoryBuffer buffer = MemoryUtils.buffer(32); diff --git a/java/fory-graalvm-feature/src/main/java/org/apache/fory/graalvm/feature/ForyGraalVMFeature.java b/java/fory-graalvm-feature/src/main/java/org/apache/fory/graalvm/feature/ForyGraalVMFeature.java index 51cd87fdf8..bf55617fa7 100644 --- a/java/fory-graalvm-feature/src/main/java/org/apache/fory/graalvm/feature/ForyGraalVMFeature.java +++ b/java/fory-graalvm-feature/src/main/java/org/apache/fory/graalvm/feature/ForyGraalVMFeature.java @@ -139,6 +139,21 @@ private void registerSerializableHierarchy(Class clazz) { current != null && current != Object.class && Serializable.class.isAssignableFrom(current); current = current.getSuperclass()) { RuntimeSerialization.registerIncludingAssociatedClasses(current); + registerSerializationConstructor(current); + } + } + + private void registerSerializationConstructor(Class clazz) { + Class targetConstructorClass = clazz.getSuperclass(); + while (targetConstructorClass != null + && Serializable.class.isAssignableFrom(targetConstructorClass)) { + targetConstructorClass = targetConstructorClass.getSuperclass(); + } + if (targetConstructorClass != null) { + // JDK25+ Fory can lazily build ObjectStreamClass descriptors at image runtime. GraalVM needs + // the matching serialization constructor accessor pre-registered for that target superclass, + // or ObjectStreamClass.lookupAny can fail for JDK classes such as TreeMap and TreeSet. + RuntimeSerialization.registerWithTargetConstructorClass(clazz, targetConstructorClass); } } diff --git a/java/fory-latest-jdk-tests/src/test/java/org/apache/fory/integration_tests/ImmutableCollectionSerializersTest.java b/java/fory-latest-jdk-tests/src/test/java/org/apache/fory/integration_tests/ImmutableCollectionSerializersTest.java index ae7db28118..81eac1a0f7 100644 --- a/java/fory-latest-jdk-tests/src/test/java/org/apache/fory/integration_tests/ImmutableCollectionSerializersTest.java +++ b/java/fory-latest-jdk-tests/src/test/java/org/apache/fory/integration_tests/ImmutableCollectionSerializersTest.java @@ -21,13 +21,15 @@ import static org.apache.fory.integration_tests.TestUtils.serDeCheck; +import java.util.Collections; +import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Set; -import lombok.AllArgsConstructor; import lombok.Data; import org.apache.fory.Fory; import org.apache.fory.ThreadSafeFory; +import org.apache.fory.platform.JdkVersion; import org.apache.fory.test.bean.CollectionFields; import org.apache.fory.test.bean.MapFields; import org.testng.Assert; @@ -93,10 +95,47 @@ public void testImmutableMapStruct() { serDeCheck(fory, collectionFields); } + @Test + public void testSetFromMapIdentityJdk25() { + if (JdkVersion.MAJOR_VERSION < 25) { + return; + } + Fory fory = Fory.builder().withXlang(false).requireClassRegistration(false).build(); + Set set = Collections.newSetFromMap(new IdentityHashMap<>()); + set.add(new String("a")); + set.add(new String("a")); + Assert.assertEquals(set.size(), 2); + + Set restored = (Set) fory.deserialize(fory.serialize(set)); + assertIdentitySet(restored); + + Set copied = fory.copy(set); + assertIdentitySet(copied); + } + + private static void assertIdentitySet(Set set) { + Assert.assertEquals(set.size(), 2); + Object first = null; + Object second = null; + for (Object value : set) { + if (first == null) { + first = value; + } else { + second = value; + } + } + Assert.assertEquals(first, "a"); + Assert.assertEquals(second, "a"); + Assert.assertNotSame(first, second); + } + @Data - @AllArgsConstructor public static class Pojo { List> data; + + public Pojo(List> data) { + this.data = data; + } } @DataProvider diff --git a/java/fory-test-core/src/main/java/org/apache/fory/test/bean/AccessBeans.java b/java/fory-test-core/src/main/java/org/apache/fory/test/bean/AccessBeans.java index 132d5def40..0ff485b42e 100644 --- a/java/fory-test-core/src/main/java/org/apache/fory/test/bean/AccessBeans.java +++ b/java/fory-test-core/src/main/java/org/apache/fory/test/bean/AccessBeans.java @@ -19,36 +19,55 @@ package org.apache.fory.test.bean; -import lombok.AllArgsConstructor; import lombok.Data; public class AccessBeans { @Data - @AllArgsConstructor private static class PrivateClass { public int f1; int f2; private int f3; + + PrivateClass() {} + + PrivateClass(int f1, int f2, int f3) { + this.f1 = f1; + this.f2 = f2; + this.f3 = f3; + } } @Data - @AllArgsConstructor private static final class FinalPrivateClass { public int f1; int f2; private int f3; + + FinalPrivateClass() {} + + FinalPrivateClass(int f1, int f2, int f3) { + this.f1 = f1; + this.f2 = f2; + this.f3 = f3; + } } @Data - @AllArgsConstructor static class DefaultLevelClass { public int f1; int f2; private int f3; + + DefaultLevelClass() {} + + DefaultLevelClass(int f1, int f2, int f3) { + this.f1 = f1; + this.f2 = f2; + this.f3 = f3; + } } @Data - @AllArgsConstructor public static class PublicClass { public int f1; int f2; @@ -56,6 +75,18 @@ public static class PublicClass { private DefaultLevelClass f4; private PrivateClass f5; private FinalPrivateClass f6; + + public PublicClass() {} + + public PublicClass( + int f1, int f2, int f3, DefaultLevelClass f4, PrivateClass f5, FinalPrivateClass f6) { + this.f1 = f1; + this.f2 = f2; + this.f3 = f3; + this.f4 = f4; + this.f5 = f5; + this.f6 = f6; + } } public static PrivateClass createPrivateClassObject() { diff --git a/java/fory-testsuite/pom.xml b/java/fory-testsuite/pom.xml index 0ca60605ca..9f141ca140 100644 --- a/java/fory-testsuite/pom.xml +++ b/java/fory-testsuite/pom.xml @@ -168,7 +168,7 @@ pathsep="${path.separator}"/> --add-modules=jdk.incubator.vector - ${project.basedir}/../fory-core/target/jpms-classes/java16 + ${project.basedir}/../fory-core/target/multi-release-classes/16 diff --git a/java/fory-testsuite/src/test/java/org/apache/fory/test/ReadResolveCircularTest.java b/java/fory-testsuite/src/test/java/org/apache/fory/test/ReadResolveCircularTest.java index 37133b1be6..47d397869c 100644 --- a/java/fory-testsuite/src/test/java/org/apache/fory/test/ReadResolveCircularTest.java +++ b/java/fory-testsuite/src/test/java/org/apache/fory/test/ReadResolveCircularTest.java @@ -95,9 +95,16 @@ private static final class SerializationProxy implements Serializable { private String containerLabel = ""; private List items; + private SerializationProxy() {} + public SerializationProxy(Container c) { this.containerLabel = c.getLabel(); - this.items = new ArrayList<>(c.getItems()); + this.items = new ArrayList<>(); + for (Item item : c.getItems()) { + // The proxy reconstructs parent links in readResolve; serializing the original back-link + // would point Item.parent at the proxy object after writeReplace. + this.items.add(new Item(item.getName())); + } } private Object readResolve() { diff --git a/java/pom.xml b/java/pom.xml index 282c040522..08d3cac451 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -72,8 +72,11 @@ UTF-8 32.1.2-jre 3.1.12 + 3.0.2 + 2.0.12 1.13 ${basedir} + 3.6.0 1.35.0 1.18.38 @@ -116,7 +119,7 @@ org.slf4j slf4j-api - 2.0.12 + ${slf4j.version} provided true diff --git a/kotlin/fory-kotlin-ksp/src/main/kotlin/org/apache/fory/kotlin/ksp/ForyKotlinSymbolProcessor.kt b/kotlin/fory-kotlin-ksp/src/main/kotlin/org/apache/fory/kotlin/ksp/ForyKotlinSymbolProcessor.kt index 245dbb9d27..827e4bec27 100644 --- a/kotlin/fory-kotlin-ksp/src/main/kotlin/org/apache/fory/kotlin/ksp/ForyKotlinSymbolProcessor.kt +++ b/kotlin/fory-kotlin-ksp/src/main/kotlin/org/apache/fory/kotlin/ksp/ForyKotlinSymbolProcessor.kt @@ -278,26 +278,46 @@ internal class ForyKotlinSymbolProcessor(private val environment: SymbolProcesso val foryIds = hashSetOf() var nextId = 0 for (parameter in primaryConstructor.parameters) { - val parameterName = parameter.name?.asString() ?: continue - val property = propertiesByName[parameterName] - if (property == null || (!parameter.isVal && !parameter.isVar)) { + val parameterName = parameter.name?.asString() + if (parameterName == null) { logger.error( - "Constructor parameter $parameterName is not a field-backed property", + "Kotlin KSP primary-constructor @ForyStruct requires named constructor parameters", + parameter, + ) + return null + } + val fieldName = parameterName + val property = propertiesByName[fieldName] + if (property == null) { + logger.error( + "Constructor parameter $parameterName is not declared as an accessible schema property", parameter ) return null } + val fieldType = property.type.resolve() + val parameterType = parameter.type.resolve() + val fieldTypeName = kotlinSourceTypeName(fieldType) + val parameterTypeName = kotlinSourceTypeName(parameterType) + if (fieldTypeName != parameterTypeName) { + logger.error( + "Schema property $fieldName type $fieldTypeName must match primary constructor parameter $parameterName type $parameterTypeName", + parameter, + ) + return null + } val field = parseField( declaration, property, - parameterName, - parameter.type.resolve(), + fieldName, + fieldType, parameter, nextId, parameter.hasDefault, foryIds, requireForyId = false, + constructorParameterName = parameterName, ) ?: return null fields.add(field) nextId++ @@ -336,6 +356,7 @@ internal class ForyKotlinSymbolProcessor(private val environment: SymbolProcesso hasDefault = false, foryIds, requireForyId = true, + constructorParameterName = property.simpleName.asString(), ) ?: return null fields.add(field) } @@ -352,6 +373,7 @@ internal class ForyKotlinSymbolProcessor(private val environment: SymbolProcesso hasDefault: Boolean, foryIds: MutableSet, requireForyId: Boolean, + constructorParameterName: String, ): KotlinSourceField? { if (Modifier.PRIVATE in property.modifiers) { logger.error("Private Fory field $fieldName is inaccessible to generated code", property) @@ -382,6 +404,7 @@ internal class ForyKotlinSymbolProcessor(private val environment: SymbolProcesso return KotlinSourceField( id = id, name = fieldName, + constructorParameterName = constructorParameterName, type = typeNode, hasForyField = fieldMeta.hasAnnotation, foryFieldId = fieldMeta.id, @@ -448,10 +471,7 @@ internal class ForyKotlinSymbolProcessor(private val environment: SymbolProcesso } val caseId = foryCaseId(caseDeclaration, reportMissing = false) ?: continue if (caseId < 0) { - logger.error( - "Schema Kotlin union @ForyCase ids must be non-negative", - caseDeclaration - ) + logger.error("Schema Kotlin union @ForyCase ids must be non-negative", caseDeclaration) return null } if (!caseIds.add(caseId)) { diff --git a/kotlin/fory-kotlin-ksp/src/main/kotlin/org/apache/fory/kotlin/ksp/KotlinSerializerSourceWriter.kt b/kotlin/fory-kotlin-ksp/src/main/kotlin/org/apache/fory/kotlin/ksp/KotlinSerializerSourceWriter.kt index 00ba8870a8..2d2980069d 100644 --- a/kotlin/fory-kotlin-ksp/src/main/kotlin/org/apache/fory/kotlin/ksp/KotlinSerializerSourceWriter.kt +++ b/kotlin/fory-kotlin-ksp/src/main/kotlin/org/apache/fory/kotlin/ksp/KotlinSerializerSourceWriter.kt @@ -26,6 +26,9 @@ import java.nio.charset.StandardCharsets internal class KotlinSerializerSourceWriter(private val struct: KotlinSourceStruct) { private val builder = StringBuilder(32768) + private val hasComparatorGuards = struct.fields.any { needsComparatorGuard(it.type) } + + private data class ContainerTarget(val init: String, val result: String) fun writeTo(codeGenerator: CodeGenerator) { val dependencies = @@ -92,6 +95,8 @@ internal class KotlinSerializerSourceWriter(private val struct: KotlinSourceStru builder.append(" private val allFields: Array\n") builder.append(" private val allFieldIds: IntArray\n") builder.append(" private val fieldsById: Array\n") + builder.append(" private val constructorFieldIds: IntArray?\n") + builder.append(" private val constructorFieldBits: LongArray?\n") builder.append(" private val classVersionHash: Int\n") builder.append(" private val sameSchemaCompatible: Boolean\n\n") } @@ -125,6 +130,12 @@ internal class KotlinSerializerSourceWriter(private val struct: KotlinSourceStru } builder.append(" return Collections.unmodifiableList(descriptors)\n") builder.append(" }\n") + if (hasComparatorGuards) { + builder.append(" private val NATURAL_ORDER_COMPARATOR: java.util.Comparator<*> =\n") + builder.append( + " java.util.Comparator.naturalOrder>() as java.util.Comparator<*>\n" + ) + } builder.append(" }\n\n") builder.append(" override fun getGeneratedDescriptors(): List = DESCRIPTORS\n\n") } @@ -134,6 +145,8 @@ internal class KotlinSerializerSourceWriter(private val struct: KotlinSourceStru builder.append(" this.allFields = emptyArray()\n") builder.append(" this.allFieldIds = IntArray(0)\n") builder.append(" this.fieldsById = arrayOfNulls(0)\n") + builder.append(" this.constructorFieldIds = null\n") + builder.append(" this.constructorFieldBits = null\n") builder.append(" this.classVersionHash = 0\n") builder.append(" this.sameSchemaCompatible = false\n") builder.append(" }\n\n") @@ -162,6 +175,21 @@ internal class KotlinSerializerSourceWriter(private val struct: KotlinSourceStru builder.append(" for (i in this.allFields.indices) {\n") builder.append(" this.fieldsById[this.allFieldIds[i]] = this.allFields[i]\n") builder.append(" }\n") + if (struct.construction == KotlinStructConstruction.CONSTRUCTOR) { + builder.append(" this.constructorFieldIds = intArrayOf(") + for (i in struct.fields.indices) { + if (i > 0) { + builder.append(", ") + } + builder.append(struct.fields[i].id) + } + builder.append(")\n") + } else { + builder.append(" this.constructorFieldIds = null\n") + } + builder.append( + " this.constructorFieldBits = buildConstructorFieldBits(DESCRIPTORS.size, constructorFieldIds)\n" + ) writeScalarBindings() builder.append( " this.classVersionHash = if (typeResolver.checkClassVersion()) computeClassVersionHash(DESCRIPTORS) else 0\n" @@ -209,6 +237,7 @@ internal class KotlinSerializerSourceWriter(private val struct: KotlinSourceStru .append(" override fun write(writeContext: WriteContext, value: ") .append(struct.typeName) .append(") {\n") + writeComparatorPreflight() builder.append(" val buffer = writeContext.buffer\n") builder.append(" if (typeResolver.checkClassVersion()) {\n") builder.append(" buffer.writeInt32(classVersionHash)\n") @@ -234,6 +263,202 @@ internal class KotlinSerializerSourceWriter(private val struct: KotlinSourceStru builder.append(" }\n") builder.append(" }\n") builder.append(" }\n\n") + if (hasComparatorGuards) { + builder.append( + " private fun requireXlangNaturalOrdering(typeName: String, comparator: java.util.Comparator<*>?) {\n" + ) + builder.append(" if (comparator != null && comparator !== NATURAL_ORDER_COMPARATOR) {\n") + builder.append(" throw UnsupportedOperationException(\n") + builder.append( + " \"Xlang serialization of \$typeName with a custom comparator is unsupported because the xlang container wire format does not encode comparators\"\n" + ) + builder.append(" )\n") + builder.append(" }\n") + builder.append(" }\n\n") + } + if (struct.construction != KotlinStructConstruction.CONSTRUCTOR) { + return + } + + builder + .append(" private fun readCompatibleConstructor(readContext: ReadContext): ") + .append(struct.typeName) + .append(" {\n") + builder.append(" val fieldValues = arrayOfNulls(DESCRIPTORS.size)\n") + builder.append(" val bufferedFields = newFieldBits(DESCRIPTORS.size)\n") + builder.append(" val presentFields = newFieldBits(DESCRIPTORS.size)\n") + builder.append(" beginConstructorRef(readContext)\n") + builder.append(" try {\n") + builder.append(" var remaining = countConstructorFields(constructorFieldBits!!)\n") + builder.append(" var value: ").append(struct.typeName).append("? = null\n") + builder.append(" if (remaining == 0) {\n") + builder.append(" val constructed = newConstructorObject(fieldValues)\n") + builder.append(" value = constructed\n") + builder.append(" referenceConstructorRef(readContext, constructed)\n") + builder.append(" }\n") + builder.append(" for (i in remoteFields.indices) {\n") + builder.append(" val remoteField = remoteFields[i]\n") + builder.append(" val fieldId = remoteField.matchedId\n") + builder.append(" if (fieldId < 0) {\n") + builder.append(" skipField(readContext, remoteField)\n") + builder.append(" continue\n") + builder.append(" }\n") + builder.append(" val localField = fieldsById[fieldId]!!\n") + builder.append(" if (!canReadRemoteField(remoteField, localField)) {\n") + builder.append(" skipField(readContext, remoteField)\n") + builder.append(" continue\n") + builder.append(" }\n") + builder.append( + " val fieldValue = readCompatibleConstructorField(readContext, remoteField, localField, fieldId)\n" + ) + builder.append(" markField(presentFields, fieldId)\n") + builder.append(" if (hasField(constructorFieldBits!!, fieldId)) {\n") + builder.append( + " fieldValues[fieldId] = ctorFieldValue(readContext, fieldValue, type)\n" + ) + builder.append(" remaining--\n") + builder.append(" if (remaining == 0) {\n") + builder.append(" checkNoUnresolvedReadRef(readContext)\n") + builder.append(" val constructed = newConstructorObject(fieldValues)\n") + builder.append(" value = constructed\n") + builder.append(" referenceConstructorRef(readContext, constructed)\n") + builder.append(" setBufferedFields(constructed, fieldValues, bufferedFields)\n") + builder.append(" }\n") + builder.append(" } else if (value == null) {\n") + builder.append( + " fieldValues[fieldId] = bufferFieldValue(readContext, fieldValue, type)\n" + ) + builder.append(" markField(bufferedFields, fieldId)\n") + builder.append(" } else {\n") + builder.append(" setFieldById(value!!, localField, fieldId, fieldValue)\n") + builder.append(" }\n") + builder.append(" }\n") + for (field in struct.fields) { + if (field.hasDefault || field.nullable) { + continue + } + builder.append(" if (!hasField(presentFields, ").append(field.id).append(")) {\n") + builder + .append(" throw DeserializationException(\"Required Kotlin field ") + .append(struct.qualifiedTypeName) + .append('.') + .append(field.name) + .append(" is missing in compatible xlang payload\")\n") + builder.append(" }\n") + } + builder.append(" if (value == null) {\n") + builder.append(" checkNoUnresolvedReadRef(readContext)\n") + builder.append(" val constructed = newConstructorObject(fieldValues)\n") + builder.append(" value = constructed\n") + builder.append(" referenceConstructorRef(readContext, constructed)\n") + builder.append(" setBufferedFields(constructed, fieldValues, bufferedFields)\n") + builder.append(" }\n") + builder.append(" return value!!\n") + builder.append(" } finally {\n") + builder.append(" endConstructorRef(readContext)\n") + builder.append(" }\n") + builder.append(" }\n\n") + + builder.append( + " private fun readCompatibleConstructorField(readContext: ReadContext, remoteField: StaticGeneratedStructSerializer.RemoteFieldInfo, localField: SerializationFieldInfo, fieldId: Int): Any? {\n" + ) + builder.append(" val buffer = readContext.buffer\n") + builder.append(" return when (fieldId) {\n") + for (field in struct.fields) { + val readExpression = + castReadExpression( + field, + "readCompatibleFieldValue(readContext, remoteField, localField)", + compatible = true, + ) + val expression = constructorReadExpression(field, readExpression) + if (field.trackingRef) { + builder.append(" ").append(field.id).append(" -> {\n") + builder.append(" trackConstructorRefRead(readContext, buffer)\n") + builder.append(" ").append(expression).append("\n") + builder.append(" }\n") + } else { + builder.append(" ").append(field.id).append(" -> ").append(expression).append("\n") + } + } + builder.append( + " else -> throw IllegalStateException(\"Unknown generated field id \${fieldId}\")\n" + ) + builder.append(" }\n") + builder.append(" }\n\n") + + builder + .append(" private fun newConstructorObject(fieldValues: Array): ") + .append(struct.typeName) + .append(" {\n") + builder.append(" return ") + appendFieldValuesConstructorCall() + builder.append("\n") + builder.append(" }\n\n") + + builder.append( + " private fun setBufferedFields(value: ${struct.typeName}, fieldValues: Array, bufferedFields: LongArray) {\n" + ) + builder.append(" for (fieldId in fieldsById.indices) {\n") + builder.append(" if (hasField(bufferedFields, fieldId)) {\n") + builder.append( + " setFieldById(value, fieldsById[fieldId]!!, fieldId, resolveBufferedValue(fieldValues[fieldId], value))\n" + ) + builder.append(" }\n") + builder.append(" }\n") + builder.append(" }\n\n") + + builder.append( + " private fun setFieldById(value: ${struct.typeName}, fieldInfo: SerializationFieldInfo, fieldId: Int, fieldValue: Any?) {\n" + ) + builder.append(" when (fieldId) {\n") + for (field in struct.fields) { + builder + .append(" ") + .append(field.id) + .append(" -> setGeneratedFieldValue(value, fieldInfo, fieldValue)\n") + } + builder.append( + " else -> throw IllegalStateException(\"Unknown generated field id \${fieldId}\")\n" + ) + builder.append(" }\n") + builder.append(" }\n\n") + + builder + .append(" private fun copyConstructorObject(copyContext: CopyContext, value: ") + .append(struct.typeName) + .append("): ") + .append(struct.typeName) + .append(" {\n") + builder.append(" val fieldValues = arrayOfNulls(DESCRIPTORS.size)\n") + builder.append(" val pendingMarker = beginConstructorCopy(copyContext, value)\n") + for (field in struct.fields) { + builder.append(" if (hasField(constructorFieldBits!!, ").append(field.id).append(")) {\n") + builder + .append(" fieldValues[") + .append(field.id) + .append("] = ") + .append(constructorCopyExpression(field)) + .append("\n") + builder.append(" }\n") + } + builder.append( + " checkNoConstructorCopyBackrefs(fieldValues, constructorFieldIds!!, pendingMarker)\n" + ) + builder.append(" val copied = newConstructorObject(fieldValues)\n") + builder.append(" copyContext.reference(value, copied)\n") + for (field in struct.fields) { + builder.append(" if (!hasField(constructorFieldBits!!, ").append(field.id).append(")) {\n") + builder + .append(" setGeneratedFieldValue(copied, fieldsById[") + .append(field.id) + .append("]!!, ") + .append(copyExpression(field)) + .append(")\n") + builder.append(" }\n") + } + builder.append(" return copied\n") + builder.append(" }\n\n") } private fun writeRead() { @@ -262,34 +487,278 @@ internal class KotlinSerializerSourceWriter(private val struct: KotlinSourceStru builder.append(" }\n\n") return } - writeLocalDeclarations() - builder.append(" for (i in allFields.indices) {\n") - builder.append(" val fieldInfo = allFields[i]\n") - builder.append(" when (allFieldIds[i]) {\n") + builder.append(" return readSchemaConstructor(readContext)\n") + builder.append(" }\n\n") + writeConstructorRead() + } + + private fun writeConstructorRead() { + builder + .append(" private fun readSchemaConstructor(readContext: ReadContext): ") + .append(struct.typeName) + .append(" {\n") + builder.append(" val fieldValues = arrayOfNulls(DESCRIPTORS.size)\n") + builder.append(" val bufferedFields = newFieldBits(DESCRIPTORS.size)\n") + builder.append(" beginConstructorRef(readContext)\n") + builder.append(" try {\n") + builder.append(" var remaining = countConstructorFields(constructorFieldBits!!)\n") + builder.append(" var value: ").append(struct.typeName).append("? = null\n") + builder.append(" if (remaining == 0) {\n") + builder.append(" val constructed = newConstructorObject(fieldValues)\n") + builder.append(" value = constructed\n") + builder.append(" referenceConstructorRef(readContext, constructed)\n") + builder.append(" }\n") + builder.append(" for (i in allFields.indices) {\n") + builder.append(" val fieldInfo = allFields[i]\n") + builder.append(" val fieldId = allFieldIds[i]\n") + builder.append( + " val fieldValue = readSchemaConstructorField(readContext, fieldInfo, fieldId)\n" + ) + builder.append(" if (hasField(constructorFieldBits!!, fieldId)) {\n") + builder.append( + " fieldValues[fieldId] = ctorFieldValue(readContext, fieldValue, type)\n" + ) + builder.append(" remaining--\n") + builder.append(" if (remaining == 0) {\n") + builder.append(" checkNoUnresolvedReadRef(readContext)\n") + builder.append(" val constructed = newConstructorObject(fieldValues)\n") + builder.append(" value = constructed\n") + builder.append(" referenceConstructorRef(readContext, constructed)\n") + builder.append(" setBufferedFields(constructed, fieldValues, bufferedFields)\n") + builder.append(" }\n") + builder.append(" } else if (value == null) {\n") + builder.append( + " fieldValues[fieldId] = bufferFieldValue(readContext, fieldValue, type)\n" + ) + builder.append(" markField(bufferedFields, fieldId)\n") + builder.append(" } else {\n") + builder.append(" setFieldById(value!!, fieldInfo, fieldId, fieldValue)\n") + builder.append(" }\n") + builder.append(" }\n") + builder.append(" if (value == null) {\n") + builder.append(" checkNoUnresolvedReadRef(readContext)\n") + builder.append(" val constructed = newConstructorObject(fieldValues)\n") + builder.append(" value = constructed\n") + builder.append(" referenceConstructorRef(readContext, constructed)\n") + builder.append(" setBufferedFields(constructed, fieldValues, bufferedFields)\n") + builder.append(" }\n") + builder.append(" return value!!\n") + builder.append(" } finally {\n") + builder.append(" endConstructorRef(readContext)\n") + builder.append(" }\n") + builder.append(" }\n\n") + + builder.append( + " private fun readSchemaConstructorField(readContext: ReadContext, fieldInfo: SerializationFieldInfo, fieldId: Int): Any? {\n" + ) + builder.append(" val buffer = readContext.buffer\n") + builder.append(" return when (fieldId) {\n") for (field in struct.fields) { - builder.append(" ").append(field.id).append(" -> ") val direct = directReadExpression(field) - if (direct == null) { - builder - .append(field.localName) - .append(" = ") - .append(castReadExpression(field, "readFieldValue(readContext, fieldInfo)")) - .append("\n") + val readExpression = + direct ?: castReadExpression(field, "readFieldValue(readContext, fieldInfo)") + val expression = constructorReadExpression(field, readExpression) + if (field.trackingRef) { + builder.append(" ").append(field.id).append(" -> {\n") + builder.append(" trackConstructorRefRead(readContext, buffer)\n") + builder.append(" ").append(expression).append("\n") + builder.append(" }\n") } else { - builder.append(field.localName).append(" = ").append(direct).append("\n") + builder.append(" ").append(field.id).append(" -> ").append(expression).append("\n") } } builder.append( - " else -> throw IllegalStateException(\"Unknown generated field id \${allFieldIds[i]}\")\n" + " else -> throw IllegalStateException(\"Unknown generated field id \${fieldId}\")\n" ) - builder.append(" }\n") builder.append(" }\n") - builder.append(" return ") - appendConstructorCall(defaultMask = 0L) - builder.append("\n") builder.append(" }\n\n") } + private fun writeComparatorPreflight() { + if (!hasComparatorGuards) { + return + } + for (field in struct.fields) { + appendComparatorGuard( + field.type, + "value.${field.name}", + " ", + "${struct.qualifiedTypeName}.${field.name}", + "guard${field.id}", + 0, + ) + } + } + + private fun needsComparatorGuard(type: KotlinSourceTypeNode): Boolean = + when { + type.nullable -> needsComparatorGuard(type.copy(nullable = false)) + type.collectionFactory == CollectionFactory.TREE_SET || + type.collectionFactory == CollectionFactory.CONCURRENT_SKIP_LIST_SET || + type.collectionFactory == CollectionFactory.TREE_MAP || + type.collectionFactory == CollectionFactory.CONCURRENT_SKIP_LIST_MAP -> true + type.typeId == "Types.LIST" || type.typeId == "Types.SET" -> + type.typeArguments.any { needsComparatorGuard(it) } + type.typeId == "Types.MAP" -> type.typeArguments.any { needsComparatorGuard(it) } + else -> false + } + + private fun appendComparatorGuard( + type: KotlinSourceTypeNode, + expression: String, + indent: String, + path: String, + prefix: String, + depth: Int, + ) { + if (!needsComparatorGuard(type)) { + return + } + if (type.nullable) { + val nullableValue = "${prefix}Value$depth" + builder + .append(indent) + .append("val ") + .append(nullableValue) + .append(" = ") + .append(expression) + .append("\n") + builder.append(indent).append("if (").append(nullableValue).append(" != null) {\n") + appendComparatorGuard( + type.copy(nullable = false), + nullableValue, + "$indent ", + path, + prefix, + depth + 1, + ) + builder.append(indent).append("}\n") + return + } + when (type.collectionFactory) { + CollectionFactory.TREE_SET, + CollectionFactory.CONCURRENT_SKIP_LIST_SET -> + builder + .append(indent) + .append("requireXlangNaturalOrdering(\"") + .append(escape(path)) + .append("\", (") + .append(expression) + .append(" as java.util.SortedSet<*>).comparator())\n") + CollectionFactory.TREE_MAP, + CollectionFactory.CONCURRENT_SKIP_LIST_MAP -> + builder + .append(indent) + .append("requireXlangNaturalOrdering(\"") + .append(escape(path)) + .append("\", (") + .append(expression) + .append(" as java.util.SortedMap<*, *>).comparator())\n") + else -> {} + } + when (type.typeId) { + "Types.LIST" -> { + val element = type.typeArguments[0] + if (!needsComparatorGuard(element)) { + return + } + val source = "${prefix}List$depth" + val index = "${prefix}Index$depth" + val elementName = "${prefix}Element$depth" + builder + .append(indent) + .append("val ") + .append(source) + .append(" = ") + .append(expression) + .append("\n") + builder + .append(indent) + .append("if (") + .append(source) + .append(" is java.util.RandomAccess) {\n") + builder.append(indent).append(" var ").append(index).append(" = 0\n") + builder + .append(indent) + .append(" ") + .append("while (") + .append(index) + .append(" < ") + .append(source) + .append(".size) {\n") + appendComparatorGuard( + element, + "$source[$index]", + "$indent ", + "$path element", + prefix, + depth + 1, + ) + builder.append(indent).append(" ").append(index).append("++\n") + builder.append(indent).append(" }\n") + builder.append(indent).append("} else {\n") + builder + .append(indent) + .append(" for (") + .append(elementName) + .append(" in ") + .append(source) + .append(") {\n") + appendComparatorGuard( + element, + elementName, + "$indent ", + "$path element", + prefix, + depth + 1, + ) + builder.append(indent).append(" }\n") + builder.append(indent).append("}\n") + } + "Types.SET" -> { + val element = type.typeArguments[0] + if (!needsComparatorGuard(element)) { + return + } + val elementName = "${prefix}Element$depth" + builder + .append(indent) + .append("for (") + .append(elementName) + .append(" in ") + .append(expression) + .append(") {\n") + appendComparatorGuard( + element, + elementName, + "$indent ", + "$path element", + prefix, + depth + 1, + ) + builder.append(indent).append("}\n") + } + "Types.MAP" -> { + val key = type.typeArguments[0] + val value = type.typeArguments[1] + if (!needsComparatorGuard(key) && !needsComparatorGuard(value)) { + return + } + val entry = "${prefix}Entry$depth" + builder + .append(indent) + .append("for (") + .append(entry) + .append(" in ") + .append(expression) + .append(".entries) {\n") + appendComparatorGuard(key, "$entry.key", "$indent ", "$path key", prefix, depth + 1) + appendComparatorGuard(value, "$entry.value", "$indent ", "$path value", prefix, depth + 2) + builder.append(indent).append("}\n") + } + } + } + private fun writeMutableReadBody() { builder.append(" val value = ").append(struct.typeName).append("()\n") builder.append(" if (readContext.hasPreservedRefId()) {\n") @@ -326,70 +795,121 @@ internal class KotlinSerializerSourceWriter(private val struct: KotlinSourceStru builder.append(" if (sameSchemaCompatible) {\n") builder.append(" return readSchemaConsistent(readContext)\n") builder.append(" }\n") + if ( + struct.construction == KotlinStructConstruction.CONSTRUCTOR && + struct.fields.any { it.hasDefault } + ) { + builder.append(" return readCompatibleDefaultConstructor(readContext)\n") + builder.append(" }\n\n") + writeCompatibleDefaultConstructorRead() + return + } + if (struct.construction == KotlinStructConstruction.CONSTRUCTOR) { + builder.append(" return readCompatibleConstructor(readContext)\n") + builder.append(" }\n\n") + return + } if (struct.construction == KotlinStructConstruction.MUTABLE) { writeMutableCompatibleReadBody() builder.append(" }\n\n") return } - writePresenceVars() - writeLocalDeclarations() - builder.append(" for (i in remoteFields.indices) {\n") - builder.append(" val remoteField = remoteFields[i]\n") - builder.append(" when (remoteField.matchedId) {\n") + writeCompatibleValueReadBody(" ", constructorRefs = false) + builder.append(" }\n\n") + } + + private fun writeCompatibleDefaultConstructorRead() { + builder + .append(" private fun readCompatibleDefaultConstructor(readContext: ReadContext): ") + .append(struct.typeName) + .append(" {\n") + builder.append(" beginConstructorRef(readContext)\n") + builder.append(" try {\n") + writeCompatibleValueReadBody(" ", constructorRefs = true) + builder.append(" } finally {\n") + builder.append(" endConstructorRef(readContext)\n") + builder.append(" }\n") + builder.append(" }\n\n") + } + + private fun writeCompatibleValueReadBody(indent: String, constructorRefs: Boolean) { + writePresenceVars(indent) + writeLocalDeclarations(indent) + builder.append(indent).append("for (i in remoteFields.indices) {\n") + builder.append(indent).append(" val remoteField = remoteFields[i]\n") + builder.append(indent).append(" when (remoteField.matchedId) {\n") for (field in struct.fields) { - builder.append(" ").append(field.id).append(" -> {\n") - builder.append(" val localField = fieldsById[").append(field.id).append("]!!\n") - builder.append(" if (canReadRemoteField(remoteField, localField)) {\n") + builder.append(indent).append(" ").append(field.id).append(" -> {\n") + builder + .append(indent) + .append(" val localField = fieldsById[") + .append(field.id) + .append("]!!\n") + builder.append(indent).append(" if (canReadRemoteField(remoteField, localField)) {\n") + val readExpression = "readCompatibleFieldValue(readContext, remoteField, localField)" + val constructorReadExpression = + if (constructorRefs && field.trackingRef) { + "run { trackConstructorRefRead(readContext, readContext.buffer); ctorFieldValue(readContext, $readExpression, type) }" + } else if (constructorRefs) { + "ctorFieldValue(readContext, $readExpression, type)" + } else { + readExpression + } builder - .append(" ") + .append(indent) + .append(" ") .append(field.localName) .append(" = ") .append( castReadExpression( field, - "readCompatibleFieldValue(readContext, remoteField, localField)", + constructorReadExpression, compatible = true, ) ) .append("\n") - builder.append(" ") + builder.append(indent).append(" ") appendPresenceSet(field) builder.append("\n") - builder.append(" } else {\n") - builder.append(" skipField(readContext, remoteField)\n") - builder.append(" }\n") - builder.append(" }\n") + builder.append(indent).append(" } else {\n") + builder.append(indent).append(" skipField(readContext, remoteField)\n") + builder.append(indent).append(" }\n") + builder.append(indent).append(" }\n") } - builder.append(" else -> skipField(readContext, remoteField)\n") - builder.append(" }\n") - builder.append(" }\n") - builder.append(" var missingDefaultMask = 0L\n") + builder.append(indent).append(" else -> skipField(readContext, remoteField)\n") + builder.append(indent).append(" }\n") + builder.append(indent).append("}\n") + builder.append(indent).append("var missingDefaultMask = 0L\n") val defaultFields = struct.fields.filter { it.hasDefault } for (field in struct.fields) { if (!field.hasDefault && field.nullable) { continue } - builder.append(" if (") + builder.append(indent).append("if (") appendPresenceMissing(field) builder.append(") {\n") when { field.hasDefault -> builder - .append(" missingDefaultMask = missingDefaultMask or ") + .append(indent) + .append(" missingDefaultMask = missingDefaultMask or ") .append(1L shl defaultFields.indexOf(field)) .append("L\n") else -> builder - .append(" throw DeserializationException(\"Required Kotlin field ") + .append(indent) + .append(" throw DeserializationException(\"Required Kotlin field ") .append(struct.qualifiedTypeName) .append('.') .append(field.name) .append(" is missing in compatible xlang payload\")\n") } - builder.append(" }\n") + builder.append(indent).append("}\n") } - writeDefaultDispatch() - builder.append(" }\n\n") + if (constructorRefs) { + builder.append(indent).append("checkNoUnresolvedReadRef(readContext)\n") + } + writeDefaultDispatch(indent, constructorRefs) } private fun writeMutableCompatibleReadBody() { @@ -446,11 +966,11 @@ internal class KotlinSerializerSourceWriter(private val struct: KotlinSourceStru builder.append(" return value\n") } - private fun writePresenceVars() { + private fun writePresenceVars(indent: String = " ") { val maxId = struct.fields.maxOfOrNull { it.id } ?: -1 val chunks = maxId / java.lang.Long.SIZE + 1 for (index in 0 until chunks) { - builder.append(" var presentMask").append(index).append(" = 0L\n") + builder.append(indent).append("var presentMask").append(index).append(" = 0L\n") } } @@ -493,49 +1013,7 @@ internal class KotlinSerializerSourceWriter(private val struct: KotlinSourceStru builder.append(" }\n") return } - for (field in struct.fields) { - if (field.type.primitive || isScalarUnsigned(field)) { - builder - .append(" val ") - .append(field.localName) - .append(" = value.") - .append(field.name) - .append("\n") - } else if (isDenseUnsignedArray(field)) { - builder.append(" val ").append(field.localName).append(" = value.").append(field.name) - if (field.nullable) { - builder.append("?.copyOf()\n") - } else { - builder.append(".copyOf()\n") - } - } else if (field.type.isCollectionOrMap()) { - builder - .append(" val ") - .append(field.localName) - .append(": ") - .append(localVariableType(field)) - .append(" = ") - .append(copyContainerExpression(field.type, "value.${field.name}", 0)) - .append("\n") - } else { - builder - .append(" val ") - .append(field.localName) - .append(": ") - .append(localVariableType(field)) - .append(" = ") - .append( - castReadExpression( - field, - "copyFieldValue(copyContext, value.${field.name}, fieldsById[${field.id}]!!)" - ) - ) - .append("\n") - } - } - builder.append(" return ") - appendConstructorCall(defaultMask = 0L) - builder.append("\n") + builder.append(" return copyConstructorObject(copyContext, value)\n") builder.append(" }\n") } @@ -544,7 +1022,7 @@ internal class KotlinSerializerSourceWriter(private val struct: KotlinSourceStru builder.append(" copyContext.reference(value, copy)\n") for (field in struct.fields) { builder.append(" copy.").append(field.name).append(" = ") - if (field.type.primitive || isScalarUnsigned(field)) { + if (isDirectCopyValue(field.type)) { builder.append("value.").append(field.name).append("\n") } else if (isDenseUnsignedArray(field)) { builder.append("value.").append(field.name) @@ -569,10 +1047,38 @@ internal class KotlinSerializerSourceWriter(private val struct: KotlinSourceStru builder.append(" return copy\n") } - private fun writeLocalDeclarations() { + private fun copyExpression(field: KotlinSourceField): String = + when { + isDirectCopyValue(field.type) -> "value.${field.name}" + isDenseUnsignedArray(field) -> + if (field.nullable) "value.${field.name}?.copyOf()" else "value.${field.name}.copyOf()" + field.type.isCollectionOrMap() -> + copyContainerExpression(field.type, "value.${field.name}", 0) + else -> + castReadExpression( + field, + "copyFieldValue(copyContext, value.${field.name}, fieldsById[${field.id}]!!)" + ) + } + + private fun constructorCopyExpression(field: KotlinSourceField): String { + val expression = + when { + isDirectCopyValue(field.type) -> "value.${field.name}" + isDenseUnsignedArray(field) -> + if (field.nullable) "value.${field.name}?.copyOf()" else "value.${field.name}.copyOf()" + field.type.isCollectionOrMap() -> + return copyContainerExpression(field.type, "value.${field.name}", 0) + else -> "copyFieldValue(copyContext, value.${field.name}, fieldsById[${field.id}]!!)" + } + return constructorReadExpression(field, expression) + } + + private fun writeLocalDeclarations(indent: String = " ") { for (field in struct.fields) { builder - .append(" var ") + .append(indent) + .append("var ") .append(field.localName) .append(": ") .append(localVariableType(field)) @@ -582,15 +1088,27 @@ internal class KotlinSerializerSourceWriter(private val struct: KotlinSourceStru } } - private fun writeDefaultDispatch() { + private fun writeDefaultDispatch(indent: String = " ", referenceConstructor: Boolean = false) { val defaultFields = struct.fields.filter { it.hasDefault } if (defaultFields.isEmpty()) { - builder.append(" return ") - appendConstructorCall(defaultMask = 0L) - builder.append("\n") + if (referenceConstructor) { + builder.append(indent).append("val constructed = ") + appendConstructorCall(defaultMask = 0L) + builder.append("\n") + builder.append(indent).append("referenceConstructorRef(readContext, constructed)\n") + builder.append(indent).append("return constructed\n") + } else { + builder.append(indent).append("return ") + appendConstructorCall(defaultMask = 0L) + builder.append("\n") + } return } - builder.append(" return when (missingDefaultMask) {\n") + if (referenceConstructor) { + builder.append(indent).append("val constructed = when (missingDefaultMask) {\n") + } else { + builder.append(indent).append("return when (missingDefaultMask) {\n") + } val combinations = 1L shl defaultFields.size for (combination in 0 until combinations) { var mask = 0L @@ -599,15 +1117,20 @@ internal class KotlinSerializerSourceWriter(private val struct: KotlinSourceStru mask = mask or (1L shl i) } } - builder.append(" ").append(mask).append("L -> ") + builder.append(indent).append(" ").append(mask).append("L -> ") appendConstructorCall(defaultMask = mask) builder.append("\n") } builder.append( - " else -> throw DeserializationException(\"Unsupported Kotlin default argument mask \${missingDefaultMask} for " + indent + + " else -> throw DeserializationException(\"Unsupported Kotlin default argument mask \${missingDefaultMask} for " ) builder.append(struct.qualifiedTypeName).append("\")\n") - builder.append(" }\n") + builder.append(indent).append("}\n") + if (referenceConstructor) { + builder.append(indent).append("referenceConstructorRef(readContext, constructed)\n") + builder.append(indent).append("return constructed\n") + } } private fun appendConstructorCall(defaultMask: Long) { @@ -624,12 +1147,39 @@ internal class KotlinSerializerSourceWriter(private val struct: KotlinSourceStru if (!first) { builder.append(", ") } - builder.append(field.name).append(" = ").append(constructorValueExpression(field)) + builder + .append(field.constructorParameterName) + .append(" = ") + .append(constructorValueExpression(field)) first = false } builder.append(")") } + private fun appendFieldValuesConstructorCall() { + builder.append(struct.typeName).append("(") + var first = true + for (field in struct.fields) { + if (!first) { + builder.append(", ") + } + builder + .append(field.constructorParameterName) + .append(" = ") + .append(constructorFieldValueExpression(field)) + first = false + } + builder.append(")") + } + + private fun constructorFieldValueExpression(field: KotlinSourceField): String { + val source = "fieldValues[${field.id}]" + if (field.propertyTypeName == "Any?") { + return source + } + return "($source as ${field.propertyTypeName})" + } + private fun constructorValueExpression(field: KotlinSourceField): String { val localValue = if (field.nullable || field.type.primitive || isScalarUnsigned(field)) { @@ -637,12 +1187,164 @@ internal class KotlinSerializerSourceWriter(private val struct: KotlinSourceStru } else { "${field.localName}!!" } - if (field.type.collectionFactory == CollectionFactory.NONE) { - return localValue + return constructorReadExpression(field, localValue) + } + + private fun constructorReadExpression(field: KotlinSourceField, valueExpression: String): String { + return collectionReadExpression(field.type, valueExpression, 0, erasedInput = false) + } + + private fun collectionReadExpression( + type: KotlinSourceTypeNode, + valueExpression: String, + depth: Int, + erasedInput: Boolean + ): String { + if (!type.isCollectionOrMap()) { + return valueExpression + } + if (type.nullable) { + val value = "readValue$depth" + return "$valueExpression?.let { $value -> ${collectionReadExpression(type.copy(nullable = false), value, depth + 1, erasedInput)} }" + } + if (type.typeArguments.any { needsCollectionReadAdaptation(it) }) { + return readContainerExpression(type, valueExpression, depth) + } + return applyCollectionFactory(type, valueExpression, erasedInput) + } + + private fun needsCollectionReadAdaptation(type: KotlinSourceTypeNode): Boolean = + type.collectionFactory != CollectionFactory.NONE || + (type.isCollectionOrMap() && type.typeArguments.any { needsCollectionReadAdaptation(it) }) + + private fun readContainerExpression( + type: KotlinSourceTypeNode, + expression: String, + depth: Int + ): String { + val source = "readSource$depth" + val target = "readTarget$depth" + return when (type.typeId) { + "Types.LIST" -> { + val element = "readElement$depth" + val adaptedElement = readElementExpression(type.typeArguments[0], element, depth + 1) + val targetBuild = readTarget(type, source, target) + "run { val $source = ${erasedCollectionExpression(type, expression)}; val $target = ${targetBuild.init}; for ($element in $source) { $target.add($adaptedElement) }; ${targetBuild.result} }" + } + "Types.SET" -> { + val element = "readElement$depth" + val adaptedElement = readElementExpression(type.typeArguments[0], element, depth + 1) + val targetBuild = readTarget(type, source, target) + "run { val $source = ${erasedCollectionExpression(type, expression)}; val $target = ${targetBuild.init}; for ($element in $source) { $target.add($adaptedElement) }; ${targetBuild.result} }" + } + "Types.MAP" -> { + val entry = "readEntry$depth" + val adaptedKey = readElementExpression(type.typeArguments[0], "$entry.key", depth + 1) + val adaptedValue = readElementExpression(type.typeArguments[1], "$entry.value", depth + 2) + val targetBuild = readTarget(type, source, target) + "run { val $source = ${erasedCollectionExpression(type, expression)}; val $target = ${targetBuild.init}; for ($entry in $source.entries) { $target[$adaptedKey] = $adaptedValue }; ${targetBuild.result} }" + } + else -> expression + } + } + + private fun readElementExpression( + type: KotlinSourceTypeNode, + expression: String, + depth: Int + ): String { + if (!needsCollectionReadAdaptation(type)) { + return expression + } + return collectionReadExpression(type, expression, depth, erasedInput = true) + } + + private fun erasedCollectionExpression(type: KotlinSourceTypeNode, expression: String): String = + when (type.typeId) { + "Types.LIST", + "Types.SET" -> "($expression as Collection<*>)" + "Types.MAP" -> "($expression as Map<*, *>)" + else -> expression + } + + private fun typedCollectionExpression(type: KotlinSourceTypeNode, expression: String): String { + if (!type.isCollectionOrMap()) { + return expression + } + return "(${erasedCollectionExpression(type, expression)} as ${type.valueTypeName.removeSuffix("?")})" + } + + private fun applyCollectionFactory( + type: KotlinSourceTypeNode, + valueExpression: String, + erasedInput: Boolean = false + ): String { + if (type.collectionFactory == CollectionFactory.NONE) { + return valueExpression + } + val conversion = collectionConversionFunction(type.collectionFactory) + if (!erasedInput) { + return "$conversion($valueExpression)" + } + return typedCollectionExpression( + type, + "$conversion(${erasedCollectionExpression(type, valueExpression)})" + ) + } + + private fun readTarget( + type: KotlinSourceTypeNode, + source: String, + target: String + ): ContainerTarget { + val valueType = type.valueTypeName.removeSuffix("?") + fun castTarget(): String = "$target as $valueType" + return when (type.typeId) { + "Types.LIST" -> + when (type.collectionFactory) { + CollectionFactory.LINKED_LIST -> + ContainerTarget("java.util.LinkedList()", castTarget()) + CollectionFactory.COPY_ON_WRITE_ARRAY_LIST -> + ContainerTarget( + "java.util.ArrayList($source.size)", + "java.util.concurrent.CopyOnWriteArrayList($target) as $valueType" + ) + else -> ContainerTarget("java.util.ArrayList($source.size)", castTarget()) + } + "Types.SET" -> + when (type.collectionFactory) { + CollectionFactory.HASH_SET -> + ContainerTarget("java.util.HashSet($source.size)", castTarget()) + CollectionFactory.TREE_SET -> ContainerTarget("java.util.TreeSet()", castTarget()) + CollectionFactory.COPY_ON_WRITE_ARRAY_SET -> + ContainerTarget( + "java.util.LinkedHashSet($source.size)", + "java.util.concurrent.CopyOnWriteArraySet($target) as $valueType" + ) + CollectionFactory.CONCURRENT_SKIP_LIST_SET -> + ContainerTarget("java.util.concurrent.ConcurrentSkipListSet()", castTarget()) + else -> ContainerTarget("java.util.LinkedHashSet($source.size)", castTarget()) + } + "Types.MAP" -> + when (type.collectionFactory) { + CollectionFactory.HASH_MAP -> + ContainerTarget("java.util.HashMap($source.size)", castTarget()) + CollectionFactory.TREE_MAP -> + ContainerTarget("java.util.TreeMap()", castTarget()) + CollectionFactory.CONCURRENT_HASH_MAP -> + ContainerTarget( + "java.util.concurrent.ConcurrentHashMap($source.size)", + castTarget() + ) + CollectionFactory.CONCURRENT_SKIP_LIST_MAP -> + ContainerTarget( + "java.util.concurrent.ConcurrentSkipListMap()", + castTarget() + ) + else -> ContainerTarget("java.util.LinkedHashMap($source.size)", castTarget()) + } + else -> ContainerTarget("null", castTarget()) } - val conversion = collectionConversionFunction(field.type.collectionFactory) - return if (field.nullable) "$localValue?.let { $conversion(it) }" - else "$conversion($localValue)" } private fun localVariableType(field: KotlinSourceField): String { @@ -713,6 +1415,46 @@ internal class KotlinSerializerSourceWriter(private val struct: KotlinSourceStru private fun KotlinSourceTypeNode.isCollectionOrMap(): Boolean = typeId == "Types.LIST" || typeId == "Types.SET" || typeId == "Types.MAP" + private fun isDirectCopyValue(type: KotlinSourceTypeNode): Boolean = + type.typeArguments.isEmpty() && + type.componentType == null && + when (type.typeId) { + "Types.BOOL", + "Types.INT8", + "Types.UINT8", + "Types.INT16", + "Types.UINT16", + "Types.INT32", + "Types.VARINT32", + "Types.UINT32", + "Types.VAR_UINT32", + "Types.INT64", + "Types.VARINT64", + "Types.TAGGED_INT64", + "Types.UINT64", + "Types.VAR_UINT64", + "Types.TAGGED_UINT64", + "Types.FLOAT32", + "Types.FLOAT64", + "Types.STRING", + "Types.DATE", + "Types.TIMESTAMP", + "Types.DURATION", + "Types.DECIMAL", + "Types.FLOAT16", + "Types.BFLOAT16" -> true + else -> false + } + + private fun copiesWithContainerSerializer(type: KotlinSourceTypeNode): Boolean = + when (type.collectionFactory) { + CollectionFactory.NONE, + CollectionFactory.MUTABLE_LIST, + CollectionFactory.MUTABLE_SET, + CollectionFactory.MUTABLE_MAP -> false + else -> true + } + private fun copyContainerExpression( type: KotlinSourceTypeNode, expression: String, @@ -722,27 +1464,50 @@ internal class KotlinSerializerSourceWriter(private val struct: KotlinSourceStru val source = "copySource$depth" return "$expression?.let { $source -> ${copyContainerExpression(type.copy(nullable = false), source, depth + 1)} }" } + if (copiesWithContainerSerializer(type)) { + return "(copyContext.copyObject($expression) as ${type.valueTypeName.removeSuffix("?")})" + } val source = "copySource$depth" val target = "copyTarget$depth" + val copied = + when (type.typeId) { + "Types.LIST" -> { + val element = "copyElement$depth" + val copiedElement = copyElementExpression(type.typeArguments[0], element, depth + 1) + val targetBuild = copyTarget(type, source, target) + "run { val $source = $expression; val $target = ${targetBuild.init}; copyContext.reference($source, $target); for ($element in $source) { $target.add($copiedElement) }; ${targetBuild.result} }" + } + "Types.SET" -> { + val element = "copyElement$depth" + val copiedElement = copyElementExpression(type.typeArguments[0], element, depth + 1) + val targetBuild = copyTarget(type, source, target) + "run { val $source = $expression; val $target = ${targetBuild.init}; copyContext.reference($source, $target); for ($element in $source) { $target.add($copiedElement) }; ${targetBuild.result} }" + } + "Types.MAP" -> { + val entry = "copyEntry$depth" + val copiedKey = copyElementExpression(type.typeArguments[0], "$entry.key", depth + 1) + val copiedValue = copyElementExpression(type.typeArguments[1], "$entry.value", depth + 2) + val targetBuild = copyTarget(type, source, target) + "run { val $source = $expression; val $target = ${targetBuild.init}; copyContext.reference($source, $target); for ($entry in $source.entries) { $target[$copiedKey] = $copiedValue }; ${targetBuild.result} }" + } + else -> "copyContext.copyObject($expression) as ${type.valueTypeName.removeSuffix("?")}" + } + return copied + } + + private fun copyTarget( + type: KotlinSourceTypeNode, + source: String, + target: String + ): ContainerTarget { val valueType = type.valueTypeName.removeSuffix("?") + fun castTarget(): String = "$target as $valueType" return when (type.typeId) { - "Types.LIST" -> { - val element = "copyElement$depth" - val copiedElement = copyElementExpression(type.typeArguments[0], element, depth + 1) - "run { val $source = $expression; val $target = java.util.ArrayList($source.size); for ($element in $source) { $target.add($copiedElement) }; $target as $valueType }" - } - "Types.SET" -> { - val element = "copyElement$depth" - val copiedElement = copyElementExpression(type.typeArguments[0], element, depth + 1) - "run { val $source = $expression; val $target = java.util.LinkedHashSet($source.size); for ($element in $source) { $target.add($copiedElement) }; $target as $valueType }" - } - "Types.MAP" -> { - val entry = "copyEntry$depth" - val copiedKey = copyElementExpression(type.typeArguments[0], "$entry.key", depth + 1) - val copiedValue = copyElementExpression(type.typeArguments[1], "$entry.value", depth + 2) - "run { val $source = $expression; val $target = java.util.LinkedHashMap($source.size); for ($entry in $source.entries) { $target[$copiedKey] = $copiedValue }; $target as $valueType }" - } - else -> "copyContext.copyObject($expression) as $valueType" + "Types.LIST" -> ContainerTarget("java.util.ArrayList($source.size)", castTarget()) + "Types.SET" -> ContainerTarget("java.util.LinkedHashSet($source.size)", castTarget()) + "Types.MAP" -> + ContainerTarget("java.util.LinkedHashMap($source.size)", castTarget()) + else -> ContainerTarget("null", castTarget()) } } @@ -755,9 +1520,11 @@ internal class KotlinSerializerSourceWriter(private val struct: KotlinSourceStru val value = "copyValue$depth" return "$expression?.let { $value -> ${copyElementExpression(type.copy(nullable = false), value, depth + 1)} }" } - if ( - type.primitive || type.unsigned || type.typeId == "Types.STRING" || type.componentType != null - ) { + val arrayCopy = copyArrayExpression(type, expression) + if (arrayCopy != null) { + return arrayCopy + } + if (isDirectCopyValue(type)) { return expression } if (type.isCollectionOrMap()) { @@ -766,6 +1533,30 @@ internal class KotlinSerializerSourceWriter(private val struct: KotlinSourceStru return "(copyContext.copyObject($expression) as ${type.valueTypeName.removeSuffix("?")})" } + private fun copyArrayExpression(type: KotlinSourceTypeNode, expression: String): String? { + val valueType = type.valueTypeName.removeSuffix("?") + if (type.typeId == "Types.BINARY" && valueType == "ByteArray") { + return "$expression.copyOf()" + } + if (type.componentType == null) { + return null + } + return when (valueType) { + "BooleanArray", + "ByteArray", + "ShortArray", + "IntArray", + "LongArray", + "FloatArray", + "DoubleArray", + "UByteArray", + "UShortArray", + "UIntArray", + "ULongArray" -> "$expression.copyOf()" + else -> "(copyContext.copyObject($expression) as $valueType)" + } + } + private fun fromJavaCompatExpr( type: KotlinSourceTypeNode, expression: String, diff --git a/kotlin/fory-kotlin-ksp/src/main/kotlin/org/apache/fory/kotlin/ksp/Model.kt b/kotlin/fory-kotlin-ksp/src/main/kotlin/org/apache/fory/kotlin/ksp/Model.kt index 687f150bfb..a50b0ca62f 100644 --- a/kotlin/fory-kotlin-ksp/src/main/kotlin/org/apache/fory/kotlin/ksp/Model.kt +++ b/kotlin/fory-kotlin-ksp/src/main/kotlin/org/apache/fory/kotlin/ksp/Model.kt @@ -81,6 +81,7 @@ internal data class KotlinSourceField( val hasDefault: Boolean, val nullable: Boolean, val propertyTypeName: String, + val constructorParameterName: String = name, ) { val localName: String = "field$id" } diff --git a/kotlin/fory-kotlin-ksp/src/test/kotlin/org/apache/fory/kotlin/ksp/ProcessorValidationTest.kt b/kotlin/fory-kotlin-ksp/src/test/kotlin/org/apache/fory/kotlin/ksp/ProcessorValidationTest.kt index 21f7e4fc58..9213512db3 100644 --- a/kotlin/fory-kotlin-ksp/src/test/kotlin/org/apache/fory/kotlin/ksp/ProcessorValidationTest.kt +++ b/kotlin/fory-kotlin-ksp/src/test/kotlin/org/apache/fory/kotlin/ksp/ProcessorValidationTest.kt @@ -22,6 +22,7 @@ package org.apache.fory.kotlin.ksp import com.google.devtools.ksp.symbol.ClassKind import com.google.devtools.ksp.symbol.Modifier import org.testng.Assert.assertEquals +import org.testng.Assert.assertFalse import org.testng.Assert.assertNull import org.testng.Assert.assertTrue import org.testng.annotations.Test @@ -75,6 +76,560 @@ class ProcessorValidationTest { ) } + @Test + fun constructorNamesArguments() { + val stringType = + KotlinSourceTypeNode( + rawClassExpression = "String::class.java", + kotlinTypeName = "kotlin.String", + valueTypeName = "String", + typeName = "java.lang.String", + typeId = "Types.STRING", + nullable = false, + trackingRef = false, + primitive = false, + unsigned = false, + ) + val source = + KotlinSerializerSourceWriter( + KotlinSourceStruct( + packageName = "example", + typeName = "User", + qualifiedTypeName = "example.User", + serializerName = "User_ForySerializer", + serializerVisibility = KotlinSerializerVisibility.PUBLIC, + fields = + listOf( + KotlinSourceField( + id = 0, + name = "name", + type = stringType, + hasForyField = true, + foryFieldId = 1, + trackingRef = false, + dynamic = "AUTO", + arrayType = false, + hasDefault = false, + nullable = false, + propertyTypeName = "String", + constructorParameterName = "userName", + ) + ), + originatingFiles = emptyList(), + ) + ) + .write() + + assertTrue(source.contains("writeContext.writeString(value.name)")) + assertTrue(source.contains("this.constructorFieldIds = intArrayOf(0)")) + assertTrue(source.contains("return User(userName = (fieldValues[0] as String))")) + assertTrue(!source.contains("return User(name = field0!!)")) + assertTrue(source.contains("fieldValues[0] = value.name")) + assertFalse(source.contains("NATURAL_ORDER_COMPARATOR")) + assertFalse(source.contains("requireXlangNaturalOrdering")) + assertFalse(source.contains("trackConstructorRefRead(readContext, buffer)")) + } + + @Test + fun tracksCtorRefs() { + val childType = + KotlinSourceTypeNode( + rawClassExpression = "Child::class.java", + kotlinTypeName = "example.Child", + valueTypeName = "Child", + typeName = "example.Child", + typeId = null, + nullable = false, + trackingRef = true, + primitive = false, + unsigned = false, + ) + val source = + KotlinSerializerSourceWriter( + KotlinSourceStruct( + packageName = "example", + typeName = "Node", + qualifiedTypeName = "example.Node", + serializerName = "Node_ForySerializer", + serializerVisibility = KotlinSerializerVisibility.PUBLIC, + fields = + listOf( + KotlinSourceField( + id = 0, + name = "child", + type = childType, + hasForyField = true, + foryFieldId = 1, + trackingRef = true, + dynamic = "AUTO", + arrayType = false, + hasDefault = false, + nullable = false, + propertyTypeName = "Child", + constructorParameterName = "child", + ) + ), + originatingFiles = emptyList(), + ) + ) + .write() + + assertTrue(source.contains("private fun readSchemaConstructorField")) + assertTrue(source.contains("private fun readCompatibleConstructorField")) + assertTrue(source.contains("trackConstructorRefRead(readContext, buffer)")) + } + + @Test + fun copyUsesDirectScalarValues() { + fun scalar(name: String, typeName: String, typeId: String) = + KotlinSourceTypeNode( + rawClassExpression = "$typeName::class.java", + kotlinTypeName = typeName, + valueTypeName = name, + typeName = typeName, + typeId = typeId, + nullable = false, + trackingRef = false, + primitive = false, + unsigned = false, + ) + + val fields = + listOf( + KotlinSourceField( + id = 0, + name = "date", + type = scalar("java.time.LocalDate", "java.time.LocalDate", "Types.DATE"), + hasForyField = true, + foryFieldId = 1, + trackingRef = false, + dynamic = "AUTO", + arrayType = false, + hasDefault = false, + nullable = false, + propertyTypeName = "java.time.LocalDate", + ), + KotlinSourceField( + id = 1, + name = "instant", + type = scalar("java.time.Instant", "java.time.Instant", "Types.TIMESTAMP"), + hasForyField = true, + foryFieldId = 2, + trackingRef = false, + dynamic = "AUTO", + arrayType = false, + hasDefault = false, + nullable = false, + propertyTypeName = "java.time.Instant", + ), + KotlinSourceField( + id = 2, + name = "duration", + type = scalar("kotlin.time.Duration", "java.time.Duration", "Types.DURATION"), + hasForyField = true, + foryFieldId = 3, + trackingRef = false, + dynamic = "AUTO", + arrayType = false, + hasDefault = false, + nullable = false, + propertyTypeName = "kotlin.time.Duration", + ), + KotlinSourceField( + id = 3, + name = "decimal", + type = scalar("java.math.BigDecimal", "java.math.BigDecimal", "Types.DECIMAL"), + hasForyField = true, + foryFieldId = 4, + trackingRef = false, + dynamic = "AUTO", + arrayType = false, + hasDefault = false, + nullable = false, + propertyTypeName = "java.math.BigDecimal", + ), + KotlinSourceField( + id = 4, + name = "float16", + type = + scalar("org.apache.fory.type.Float16", "org.apache.fory.type.Float16", "Types.FLOAT16"), + hasForyField = true, + foryFieldId = 5, + trackingRef = false, + dynamic = "AUTO", + arrayType = false, + hasDefault = false, + nullable = false, + propertyTypeName = "org.apache.fory.type.Float16", + ), + KotlinSourceField( + id = 5, + name = "bfloat16", + type = + scalar( + "org.apache.fory.type.BFloat16", + "org.apache.fory.type.BFloat16", + "Types.BFLOAT16", + ), + hasForyField = true, + foryFieldId = 6, + trackingRef = false, + dynamic = "AUTO", + arrayType = false, + hasDefault = false, + nullable = false, + propertyTypeName = "org.apache.fory.type.BFloat16", + ), + KotlinSourceField( + id = 6, + name = "child", + type = + KotlinSourceTypeNode( + rawClassExpression = "example.Child::class.java", + kotlinTypeName = "example.Child", + valueTypeName = "Child", + typeName = "example.Child", + typeId = null, + nullable = false, + trackingRef = false, + primitive = false, + unsigned = false, + ), + hasForyField = true, + foryFieldId = 7, + trackingRef = false, + dynamic = "AUTO", + arrayType = false, + hasDefault = false, + nullable = false, + propertyTypeName = "Child", + ), + ) + val source = + KotlinSerializerSourceWriter( + KotlinSourceStruct( + packageName = "example", + typeName = "Scalars", + qualifiedTypeName = "example.Scalars", + serializerName = "Scalars_ForySerializer", + serializerVisibility = KotlinSerializerVisibility.PUBLIC, + fields = fields, + originatingFiles = emptyList(), + ) + ) + .write() + + for (field in fields.take(6)) { + assertTrue(source.contains("fieldValues[${field.id}] = value.${field.name}")) + } + assertTrue(source.contains("fieldValues[6] = copyFieldValue(copyContext, value.child")) + } + + @Test + fun constructorFieldsAdaptCollections() { + val stringType = + KotlinSourceTypeNode( + rawClassExpression = "String::class.java", + kotlinTypeName = "kotlin.String", + valueTypeName = "String", + typeName = "java.lang.String", + typeId = "Types.STRING", + nullable = false, + trackingRef = false, + primitive = false, + unsigned = false, + ) + val intType = + KotlinSourceTypeNode( + rawClassExpression = "Int::class.javaPrimitiveType!!", + kotlinTypeName = "kotlin.Int", + valueTypeName = "Int", + typeName = "int32", + typeId = "Types.VARINT32", + nullable = false, + trackingRef = false, + primitive = true, + unsigned = false, + ) + val intArrayType = + KotlinSourceTypeNode( + rawClassExpression = "IntArray::class.java", + kotlinTypeName = "kotlin.IntArray", + valueTypeName = "IntArray", + typeName = "int[]", + typeId = "Types.INT32_ARRAY", + nullable = false, + trackingRef = false, + primitive = false, + unsigned = false, + componentType = intType, + ) + val setType = + KotlinSourceTypeNode( + rawClassExpression = "java.util.Set::class.java", + kotlinTypeName = "java.util.TreeSet", + valueTypeName = "kotlin.collections.Set", + typeName = "java.util.Set", + typeId = "Types.SET", + nullable = false, + trackingRef = false, + primitive = false, + unsigned = false, + collectionFactory = CollectionFactory.TREE_SET, + typeArguments = listOf(stringType), + ) + val listType = + KotlinSourceTypeNode( + rawClassExpression = "java.util.List::class.java", + kotlinTypeName = "java.util.List>", + valueTypeName = "kotlin.collections.List>", + typeName = "java.util.List", + typeId = "Types.LIST", + nullable = false, + trackingRef = false, + primitive = false, + unsigned = false, + typeArguments = listOf(setType), + ) + val arrayListType = + KotlinSourceTypeNode( + rawClassExpression = "java.util.List::class.java", + kotlinTypeName = "java.util.List", + valueTypeName = "kotlin.collections.List", + typeName = "java.util.List", + typeId = "Types.LIST", + nullable = false, + trackingRef = false, + primitive = false, + unsigned = false, + typeArguments = listOf(intArrayType), + ) + val mapType = + KotlinSourceTypeNode( + rawClassExpression = "java.util.Map::class.java", + kotlinTypeName = "java.util.TreeMap", + valueTypeName = "kotlin.collections.Map", + typeName = "java.util.Map", + typeId = "Types.MAP", + nullable = false, + trackingRef = false, + primitive = false, + unsigned = false, + collectionFactory = CollectionFactory.TREE_MAP, + typeArguments = listOf(stringType, intType), + ) + val nestedMapType = + KotlinSourceTypeNode( + rawClassExpression = "java.util.Map::class.java", + kotlinTypeName = "java.util.TreeMap>", + valueTypeName = "java.util.TreeMap>", + typeName = "java.util.Map", + typeId = "Types.MAP", + nullable = false, + trackingRef = false, + primitive = false, + unsigned = false, + collectionFactory = CollectionFactory.TREE_MAP, + typeArguments = listOf(stringType, setType), + ) + val source = + KotlinSerializerSourceWriter( + KotlinSourceStruct( + packageName = "example", + typeName = "User", + qualifiedTypeName = "example.User", + serializerName = "User_ForySerializer", + serializerVisibility = KotlinSerializerVisibility.PUBLIC, + fields = + listOf( + KotlinSourceField( + id = 0, + name = "counts", + type = mapType, + hasForyField = true, + foryFieldId = 1, + trackingRef = true, + dynamic = "AUTO", + arrayType = false, + hasDefault = false, + nullable = false, + propertyTypeName = "java.util.TreeMap", + constructorParameterName = "counts", + ), + KotlinSourceField( + id = 1, + name = "names", + type = listType, + hasForyField = true, + foryFieldId = 2, + trackingRef = false, + dynamic = "AUTO", + arrayType = false, + hasDefault = false, + nullable = false, + propertyTypeName = "java.util.List>", + constructorParameterName = "names", + ), + KotlinSourceField( + id = 2, + name = "arrays", + type = arrayListType, + hasForyField = true, + foryFieldId = 3, + trackingRef = false, + dynamic = "AUTO", + arrayType = false, + hasDefault = false, + nullable = false, + propertyTypeName = "java.util.List", + constructorParameterName = "arrays", + ), + KotlinSourceField( + id = 3, + name = "nestedCounts", + type = nestedMapType, + hasForyField = true, + foryFieldId = 4, + trackingRef = false, + dynamic = "AUTO", + arrayType = false, + hasDefault = false, + nullable = false, + propertyTypeName = "java.util.TreeMap>", + constructorParameterName = "nestedCounts", + ) + ), + originatingFiles = emptyList(), + ) + ) + .write() + + assertTrue( + source.contains( + "return User(counts = (fieldValues[0] as java.util.TreeMap), names = (fieldValues[1] as java.util.List>), arrays = (fieldValues[2] as java.util.List), nestedCounts = (fieldValues[3] as java.util.TreeMap>))" + ) + ) + assertTrue( + source.contains("KotlinCollectionAdapters.toTreeSet((readElement0 as Collection<*>))") + ) + assertTrue( + source.contains( + "KotlinCollectionAdapters.toTreeMap((readFieldValue(readContext, fieldInfo) as kotlin.collections.Map))" + ) + ) + assertTrue(source.contains("0 -> {\n trackConstructorRefRead(readContext, buffer)")) + assertTrue( + source.contains( + "1 -> run { val readSource0 = ((readFieldValue(readContext, fieldInfo) as kotlin.collections.List>) as Collection<*>);" + ) + ) + assertTrue( + source.contains( + "KotlinCollectionAdapters.toTreeMap((readCompatibleFieldValue(readContext, remoteField, localField) as kotlin.collections.Map))" + ) + ) + assertTrue( + source.contains( + "1 -> run { val readSource0 = ((readCompatibleFieldValue(readContext, remoteField, localField) as kotlin.collections.List>) as Collection<*>);" + ) + ) + assertTrue( + source.contains( + "3 -> run { val readSource0 = ((readCompatibleFieldValue(readContext, remoteField, localField) as java.util.TreeMap>) as Map<*, *>); val readTarget0 = java.util.TreeMap();" + ) + ) + assertTrue( + source.contains("KotlinCollectionAdapters.toTreeSet((readEntry0.value as Collection<*>))") + ) + val comparatorGuardIndex = source.indexOf("requireXlangNaturalOrdering(\"example.User.counts\"") + assertTrue(comparatorGuardIndex >= 0) + assertTrue(comparatorGuardIndex < source.indexOf("val buffer = writeContext.buffer")) + assertTrue(source.contains("requireXlangNaturalOrdering(\"example.User.names element\"")) + assertTrue(source.contains("requireXlangNaturalOrdering(\"example.User.nestedCounts value\"")) + assertTrue(source.contains("private val NATURAL_ORDER_COMPARATOR")) + assertTrue(source.contains("if (guard1List0 is java.util.RandomAccess)")) + assertTrue(source.contains("for (guard1Element0 in guard1List0)")) + assertTrue( + source.contains( + "fieldValues[0] = (copyContext.copyObject(value.counts) as kotlin.collections.Map)" + ) + ) + assertTrue( + source.contains( + "fieldValues[1] = run { val copySource0 = value.names; val copyTarget0 = java.util.ArrayList(copySource0.size); copyContext.reference(copySource0, copyTarget0); for (copyElement0 in copySource0) { copyTarget0.add((copyContext.copyObject(copyElement0) as kotlin.collections.Set))" + ) + ) + assertTrue( + source.contains( + "fieldValues[3] = (copyContext.copyObject(value.nestedCounts) as java.util.TreeMap>)" + ) + ) + assertTrue(source.contains("fieldValues[2] = run { val copySource0 = value.arrays;")) + assertTrue(source.contains("copyTarget0.add(copyElement0.copyOf())")) + assertFalse(source.contains("fieldValues[0] = KotlinCollectionAdapters.toTreeMap(run")) + assertFalse(source.contains("java.util.TreeMap((copySource0.comparator()")) + } + + @Test + fun defaultsUseGeneratedCompatibleRead() { + val stringType = + KotlinSourceTypeNode( + rawClassExpression = "String::class.java", + kotlinTypeName = "kotlin.String", + valueTypeName = "String", + typeName = "java.lang.String", + typeId = "Types.STRING", + nullable = false, + trackingRef = true, + primitive = false, + unsigned = false, + ) + val source = + KotlinSerializerSourceWriter( + KotlinSourceStruct( + packageName = "example", + typeName = "User", + qualifiedTypeName = "example.User", + serializerName = "User_ForySerializer", + serializerVisibility = KotlinSerializerVisibility.PUBLIC, + fields = + listOf( + KotlinSourceField( + id = 0, + name = "name", + type = stringType, + hasForyField = true, + foryFieldId = 1, + trackingRef = true, + dynamic = "AUTO", + arrayType = false, + hasDefault = true, + nullable = false, + propertyTypeName = "String", + constructorParameterName = "name", + ) + ), + originatingFiles = emptyList(), + ) + ) + .write() + val compatibleStart = source.indexOf("override fun readCompatible") + val copyStart = source.indexOf("override fun copy") + val compatibleSource = source.substring(compatibleStart, copyStart) + + assertFalse(compatibleSource.contains("return readCompatibleConstructor(readContext)")) + assertTrue(compatibleSource.contains("return readCompatibleDefaultConstructor(readContext)")) + assertTrue(compatibleSource.contains("beginConstructorRef(readContext)")) + assertTrue(compatibleSource.contains("checkNoUnresolvedReadRef(readContext)")) + assertTrue( + compatibleSource.contains("trackConstructorRefRead(readContext, readContext.buffer)") + ) + assertTrue(compatibleSource.contains("referenceConstructorRef(readContext, constructed)")) + assertTrue(compatibleSource.contains("endConstructorRef(readContext)")) + assertTrue(compatibleSource.contains("missingDefaultMask")) + assertTrue(compatibleSource.contains("val constructed = when (missingDefaultMask)")) + } + @Test fun tracksWidePresence() { val intType = @@ -97,6 +652,7 @@ class ProcessorValidationTest { qualifiedTypeName = "example.WideStruct", serializerName = "WideStruct_ForySerializer", serializerVisibility = KotlinSerializerVisibility.PUBLIC, + construction = KotlinStructConstruction.MUTABLE, fields = (0 until 70).map { id -> KotlinSourceField( @@ -358,6 +914,8 @@ class ProcessorValidationTest { assertTrue(source.contains("presentMask0 = presentMask0 or (1L shl 0)")) assertTrue(source.contains("Required Kotlin field example.Node.id is missing")) assertTrue(source.contains("copyContext.reference(value, copy)")) + assertFalse(source.contains("readCompatibleConstructor(")) + assertFalse(source.contains("newConstructorObject(")) assertTrue(!source.contains("return Node(parent =")) } diff --git a/kotlin/fory-kotlin-tests/pom.xml b/kotlin/fory-kotlin-tests/pom.xml index 46ddc2f917..35f12436b0 100644 --- a/kotlin/fory-kotlin-tests/pom.xml +++ b/kotlin/fory-kotlin-tests/pom.xml @@ -125,6 +125,16 @@ fory-kotlin-tests-xlang-peer false + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + diff --git a/kotlin/fory-kotlin-tests/src/main/kotlin/org/apache/fory/kotlin/xlang/KotlinXlangPeer.kt b/kotlin/fory-kotlin-tests/src/main/kotlin/org/apache/fory/kotlin/xlang/KotlinXlangPeer.kt index 0608fbc813..3f3504af12 100644 --- a/kotlin/fory-kotlin-tests/src/main/kotlin/org/apache/fory/kotlin/xlang/KotlinXlangPeer.kt +++ b/kotlin/fory-kotlin-tests/src/main/kotlin/org/apache/fory/kotlin/xlang/KotlinXlangPeer.kt @@ -21,6 +21,9 @@ package org.apache.fory.kotlin.xlang +import java.math.BigDecimal +import java.time.Instant +import java.time.LocalDate import java.util.TreeMap import java.util.TreeSet import java.util.concurrent.ConcurrentHashMap @@ -37,6 +40,7 @@ import org.apache.fory.annotation.ForyUnion import org.apache.fory.annotation.ForyUnknownCase import org.apache.fory.annotation.Ref import org.apache.fory.exception.ForyException +import org.apache.fory.exception.SerializationException import org.apache.fory.kotlin.Fixed import org.apache.fory.kotlin.ForyKotlin import org.apache.fory.kotlin.VarInt @@ -44,26 +48,31 @@ import org.apache.fory.kotlin.register import org.apache.fory.memory.MemoryUtils import org.apache.fory.serializer.StaticGeneratedStructSerializer import org.apache.fory.serializer.kotlin.KotlinSerializers +import org.apache.fory.type.BFloat16 import org.apache.fory.type.BFloat16Array +import org.apache.fory.type.Float16 import org.apache.fory.type.Float16Array import org.apache.fory.type.Types import org.apache.fory.type.union.UnknownCase @ForyStruct -public data class KotlinUser( +public data class KotlinUser +constructor( @ForyField(id = 1) val id: @Fixed UInt, @ForyField(id = 2) val name: String = "anonymous", @ForyField(id = 3) val score: @VarInt Long, ) @ForyStruct -internal data class KotlinInternalUser( +internal data class KotlinInternalUser +constructor( @ForyField(id = 1) val id: UInt, @ForyField(id = 2) val name: String = "internal", ) @ForyStruct -public data class KotlinConcreteCollections( +public data class KotlinConcreteCollections +constructor( @ForyField(id = 1) val names: ArrayList, @ForyField(id = 2) val values: java.util.LinkedList, @ForyField(id = 3) val tags: CopyOnWriteArrayList, @@ -76,7 +85,8 @@ public data class KotlinConcreteCollections( ) @ForyStruct -public data class KotlinUnsignedCollections( +public data class KotlinUnsignedCollections +constructor( @ForyField(id = 1) val ids: List, @ForyField(id = 2) val optionalIds: List, @ForyField(id = 3) val totals: Set, @@ -85,7 +95,8 @@ public data class KotlinUnsignedCollections( ) @ForyStruct -public data class KotlinSchemaSurface( +public data class KotlinSchemaSurface +constructor( @ForyField(id = 1) val nullableNames: List?, @ForyField(id = 2) val dynamicList: List<*>, @ForyField(id = 3) val dynamicValues: Map, @@ -98,10 +109,12 @@ public data class KotlinSchemaSurface( @ForyField(id = 10) val noRefUsers: List, @ForyField(id = 11) val chunks: List<@ArrayType ByteArray>, @ForyField(id = 12) val chunksByName: Map, + @ForyField(id = 13) val nestedSortedNames: List>, ) @ForyStruct -public data class KotlinDenseArrays( +public data class KotlinDenseArrays +constructor( @ForyField(id = 1) val ubytes: UByteArray, @ForyField(id = 2) val ushorts: UShortArray, @ForyField(id = 3) val uints: UIntArray, @@ -116,10 +129,13 @@ public data class KotlinDenseArrays( @ForyField(id = 12) val nullableUInts: UIntArray?, ) -@ForyStruct public data class KotlinNullableCompatibleWriter(@ForyField(id = 1) val anchor: String) +@ForyStruct +public data class KotlinNullableCompatibleWriter +constructor(@ForyField(id = 1) val anchor: String) @ForyStruct -public data class KotlinNullableCompatibleReader( +public data class KotlinNullableCompatibleReader +constructor( @ForyField(id = 1) val anchor: String, @ForyField(id = 2) val maybeBoolean: Boolean?, @ForyField(id = 3) val maybeInt: Int?, @@ -129,10 +145,42 @@ public data class KotlinNullableCompatibleReader( ) @ForyStruct -public data class KotlinDurationAndHalfArrays( +public data class KotlinDefaultCompatibleWriter +constructor(@ForyField(id = 1) val id: Int) + +@ForyStruct +public data class KotlinDefaultCompatibleReader +constructor( + @ForyField(id = 1) val id: Int, + @ForyField(id = 2) val name: String = "generated-default", +) + +@ForyStruct +public data class KotlinDefaultRefWriter +constructor( + @ForyField(id = 1) val id: Int, + @Ref @ForyField(id = 2) var next: KotlinDefaultRefWriter?, +) + +@ForyStruct +public data class KotlinDefaultRefReader +constructor( + @ForyField(id = 1) val id: Int, + @ForyField(id = 3) val name: String = "generated-default", + @Ref @ForyField(id = 2) var next: KotlinDefaultRefReader?, +) + +@ForyStruct +public data class KotlinDurationAndHalfArrays +constructor( @ForyField(id = 1) val duration: kotlin.time.Duration, - @ForyField(id = 2) val float16s: Float16Array, - @ForyField(id = 3) val bfloat16s: BFloat16Array, + @ForyField(id = 2) val date: LocalDate, + @ForyField(id = 3) val instant: Instant, + @ForyField(id = 4) val decimal: BigDecimal, + @ForyField(id = 5) val float16: Float16, + @ForyField(id = 6) val bfloat16: BFloat16, + @ForyField(id = 7) val float16s: Float16Array, + @ForyField(id = 8) val bfloat16s: BFloat16Array, ) @ForyStruct @@ -142,6 +190,15 @@ public class KotlinMutableNode() { @Ref @ForyField(id = 2) public var parent: KotlinMutableNode? = null } +@ForyStruct +public class KotlinCtorBackrefRoot +constructor(@ForyField(id = 1) val child: KotlinCtorBackrefChild) + +@ForyStruct +public class KotlinCtorBackrefChild { + @ForyField(id = 1) public var root: KotlinCtorBackrefRoot? = null +} + @ForyUnion public sealed class KotlinPet { @ForyUnknownCase public data class Unknown(val value: UnknownCase) : KotlinPet() @@ -157,6 +214,8 @@ public fun main(args: Array) { } when (args[0]) { "static_serializer_round_trip" -> staticSerializerRoundTrip(args[1]) + "compatible_default_round_trip" -> compatibleDefaultRoundTrip() + "constructor_backref_copy" -> constructorBackrefCopy() "dense_array_round_trip" -> denseArrayRoundTrip(args[1]) "unsigned_collection_round_trip" -> unsignedCollectionRoundTrip(args[1]) else -> throw IllegalArgumentException("Unsupported Kotlin xlang case ${args[0]}") @@ -218,6 +277,7 @@ private fun staticSerializerRoundTrip(dataFile: String) { noRefUsers = emptyList(), chunks = listOf(byteArrayOf(1, 2), byteArrayOf(3, 4)), chunksByName = mapOf("left" to byteArrayOf(5, 6)), + nestedSortedNames = listOf(TreeSet(listOf("blue", "green"))), ) val decodedSchemaSurface = fory.deserialize(fory.serialize(schemaSurface), KotlinSchemaSurface::class.java) @@ -239,6 +299,25 @@ private fun staticSerializerRoundTrip(dataFile: String) { checkNotNull(decodedSchemaSurface.chunksByName["left"]) contentEquals schemaSurface.chunksByName["left"]!! ) + check(decodedSchemaSurface.nestedSortedNames == schemaSurface.nestedSortedNames) + check(decodedSchemaSurface.nestedSortedNames[0].javaClass == TreeSet::class.java) + val copiedSchemaSurface = fory.copy(schemaSurface) + check(copiedSchemaSurface.bytesAsArray !== schemaSurface.bytesAsArray) + check(copiedSchemaSurface.chunks !== schemaSurface.chunks) + check(copiedSchemaSurface.chunks[0] !== schemaSurface.chunks[0]) + check(copiedSchemaSurface.chunksByName !== schemaSurface.chunksByName) + check( + checkNotNull(copiedSchemaSurface.chunksByName["left"]) !== schemaSurface.chunksByName["left"] + ) + check(copiedSchemaSurface.nestedSortedNames !== schemaSurface.nestedSortedNames) + check(copiedSchemaSurface.nestedSortedNames[0] !== schemaSurface.nestedSortedNames[0]) + check(copiedSchemaSurface.nestedSortedNames[0].javaClass == TreeSet::class.java) + schemaSurface.bytesAsArray[0] = 99.toByte() + schemaSurface.chunks[0][0] = 98.toByte() + checkNotNull(schemaSurface.chunksByName["left"])[0] = 97.toByte() + check(copiedSchemaSurface.bytesAsArray[0] == 1.toByte()) + check(copiedSchemaSurface.chunks[0][0] == 1.toByte()) + check(checkNotNull(copiedSchemaSurface.chunksByName["left"])[0] == 5.toByte()) val schemaDescriptors = checkNotNull( fory.getSerializer(KotlinSchemaSurface::class.java) as? StaticGeneratedStructSerializer<*> @@ -279,6 +358,23 @@ private fun staticSerializerRoundTrip(dataFile: String) { decoded.mutableCounts["round-trip-mutable-map"] = 14L check(decoded.sortedNames.javaClass == TreeSet::class.java) check(decoded.concurrentCounts.javaClass == ConcurrentHashMap::class.java) + val reverseCounts = TreeMap(java.util.Collections.reverseOrder()) + reverseCounts.putAll(mapOf("x" to 7, "y" to 8)) + val reverseSortedNames = TreeSet(java.util.Collections.reverseOrder()) + reverseSortedNames.addAll(listOf("e", "f")) + val copyCollections = collections.copy(counts = reverseCounts, sortedNames = reverseSortedNames) + try { + fory.serialize(copyCollections) + error("Kotlin concrete sorted collections with custom comparators were serialized") + } catch (e: SerializationException) { + check(e.cause is UnsupportedOperationException) + } + val copiedCollections = fory.copy(copyCollections) + check(copiedCollections == copyCollections) + check(copiedCollections.counts !== copyCollections.counts) + check(checkNotNull(copiedCollections.counts.comparator()).compare("a", "b") > 0) + check(copiedCollections.sortedNames !== copyCollections.sortedNames) + check(checkNotNull(copiedCollections.sortedNames.comparator()).compare("a", "b") > 0) val unsignedCollections = KotlinUnsignedCollections( @@ -354,9 +450,16 @@ private fun staticSerializerRoundTrip(dataFile: String) { check(compatibleDecoded.maybeUInt == null) check(compatibleDecoded.maybeULong == null) + compatibleDefaultRoundTrip() + val durationAndHalfArrays = KotlinDurationAndHalfArrays( duration = (-500).milliseconds, + date = LocalDate.of(2026, 6, 2), + instant = Instant.parse("2026-06-02T04:00:00Z"), + decimal = BigDecimal("12345.6789"), + float16 = Float16.valueOf(1.5f), + bfloat16 = BFloat16.valueOf(-2.5f), float16s = Float16Array.of(1.0f, -2.0f), bfloat16s = BFloat16Array.of(3.0f, -4.0f), ) @@ -366,8 +469,19 @@ private fun staticSerializerRoundTrip(dataFile: String) { KotlinDurationAndHalfArrays::class.java, ) check(decodedDurationAndHalfArrays.duration == durationAndHalfArrays.duration) + check(decodedDurationAndHalfArrays.date == durationAndHalfArrays.date) + check(decodedDurationAndHalfArrays.instant == durationAndHalfArrays.instant) + check(decodedDurationAndHalfArrays.decimal == durationAndHalfArrays.decimal) + check(decodedDurationAndHalfArrays.float16 == durationAndHalfArrays.float16) + check(decodedDurationAndHalfArrays.bfloat16 == durationAndHalfArrays.bfloat16) check(decodedDurationAndHalfArrays.float16s == durationAndHalfArrays.float16s) check(decodedDurationAndHalfArrays.bfloat16s == durationAndHalfArrays.bfloat16s) + val copiedDurationAndHalfArrays = fory.copy(durationAndHalfArrays) + check(copiedDurationAndHalfArrays.date === durationAndHalfArrays.date) + check(copiedDurationAndHalfArrays.instant === durationAndHalfArrays.instant) + check(copiedDurationAndHalfArrays.decimal === durationAndHalfArrays.decimal) + check(copiedDurationAndHalfArrays.float16 === durationAndHalfArrays.float16) + check(copiedDurationAndHalfArrays.bfloat16 === durationAndHalfArrays.bfloat16) try { fory.serialize(durationAndHalfArrays.copy(duration = Duration.INFINITE)) error("Infinite Kotlin xlang duration was serialized") @@ -406,6 +520,53 @@ private fun staticSerializerRoundTrip(dataFile: String) { } } +private fun constructorBackrefCopy() { + val refFory = + ForyKotlin.builder() + .withXlang(false) + .requireClassRegistration(true) + .withRefTracking(true) + .withRefCopy(true) + .build() + refFory.register() + refFory.register() + checkConstructorBackrefCopy(refFory) +} + +private fun checkConstructorBackrefCopy(fory: Fory) { + val child = KotlinCtorBackrefChild() + val root = KotlinCtorBackrefRoot(child) + child.root = root + try { + fory.copy(root) + error("Constructor back-reference was copied") + } catch (_: ForyException) {} +} + +private fun compatibleDefaultRoundTrip() { + val writer = newCompatibleFory() + writer.register("kotlin", "DefaultCompatible") + val reader = newCompatibleFory() + reader.register("kotlin", "DefaultCompatible") + val decoded = + reader.deserialize( + writer.serialize(KotlinDefaultCompatibleWriter(7)), + KotlinDefaultCompatibleReader::class.java, + ) + check(decoded == KotlinDefaultCompatibleReader(id = 7)) + + val refWriter = newRefCompatibleFory() + refWriter.register("kotlin", "DefaultRef") + val refReader = newRefCompatibleFory() + refReader.register("kotlin", "DefaultRef") + val node = KotlinDefaultRefWriter(id = 9, next = null) + node.next = node + try { + refReader.deserialize(refWriter.serialize(node), KotlinDefaultRefReader::class.java) + error("Constructor self-reference with a defaulted compatible field was deserialized") + } catch (_: ForyException) {} +} + private fun checkNoArgRegisterReceivers() { checkNoArgRegister(newFory()) checkNoArgRegister( @@ -503,6 +664,14 @@ private fun newCompatibleFory(): Fory = .withRefTracking(false) .build() +private fun newRefCompatibleFory(): Fory = + ForyKotlin.builder() + .withXlang(true) + .withCompatible(true) + .requireClassRegistration(true) + .withRefTracking(true) + .build() + private fun newRefFory(): Fory = ForyKotlin.builder() .withXlang(true) diff --git a/kotlin/fory-kotlin/src/main/kotlin/org/apache/fory/serializer/kotlin/KotlinDefaultValueSupport.kt b/kotlin/fory-kotlin/src/main/kotlin/org/apache/fory/serializer/kotlin/KotlinDefaultValueSupport.kt index aa8caf93b2..dff2ebce21 100644 --- a/kotlin/fory-kotlin/src/main/kotlin/org/apache/fory/serializer/kotlin/KotlinDefaultValueSupport.kt +++ b/kotlin/fory-kotlin/src/main/kotlin/org/apache/fory/serializer/kotlin/KotlinDefaultValueSupport.kt @@ -19,7 +19,10 @@ package org.apache.fory.serializer.kotlin +import java.lang.reflect.Array +import java.lang.reflect.Modifier import java.lang.reflect.Type +import java.util.IdentityHashMap import kotlin.reflect.KParameter import kotlin.reflect.full.memberProperties import kotlin.reflect.full.primaryConstructor @@ -28,7 +31,7 @@ import org.apache.fory.collection.ClassValueCache import org.apache.fory.logging.Logger import org.apache.fory.logging.LoggerFactory import org.apache.fory.platform.AndroidSupport -import org.apache.fory.platform.UnsafeOps +import org.apache.fory.reflect.ObjectInstantiators import org.apache.fory.util.DefaultValueUtils /** @@ -68,7 +71,7 @@ internal class KotlinDefaultValueSupport : DefaultValueUtils.DefaultValueSupport // Provide default values for all required (non-optional) parameters for (parameter in parameters) { if (!parameter.isOptional) { - val defaultValue = getDefaultValueForType(parameter.type.javaType) + val defaultValue = getDefaultValueForType(parameter.type.javaType, IdentityHashMap()) if (defaultValue != null) { argsMap[parameter] = defaultValue } else { @@ -87,7 +90,7 @@ internal class KotlinDefaultValueSupport : DefaultValueUtils.DefaultValueSupport val property = kClass.memberProperties.find { it.name == parameter.name } property?.let { prop -> @Suppress("UNCHECKED_CAST") - val value = (prop as kotlin.reflect.KProperty1).get(instance as Any) + val value = (prop as kotlin.reflect.KProperty1).get(instance) if (value != null) { defaultValues[parameter.name!!] = value } @@ -114,7 +117,7 @@ internal class KotlinDefaultValueSupport : DefaultValueUtils.DefaultValueSupport return cls.kotlin.isData } - private fun getDefaultValueForType(type: Type): Any? { + private fun getDefaultValueForType(type: Type, seen: IdentityHashMap, Boolean>): Any? { val clazz = when (type) { is Class<*> -> type @@ -140,7 +143,59 @@ internal class KotlinDefaultValueSupport : DefaultValueUtils.DefaultValueSupport Char::class.java, Char::class.javaPrimitiveType -> '\u0000' String::class.java -> "" - else -> if (AndroidSupport.IS_ANDROID) null else UnsafeOps.newInstance(clazz) + else -> getDefaultValueForClass(clazz, seen) } } + + private fun getDefaultValueForClass( + clazz: Class<*>, + seen: IdentityHashMap, Boolean> + ): Any? { + if (clazz.isArray) { + return Array.newInstance(clazz.componentType, 0) + } + if (clazz.isEnum) { + return clazz.enumConstants?.firstOrNull() + } + if (List::class.java.isAssignableFrom(clazz)) { + return emptyList() + } + if (Set::class.java.isAssignableFrom(clazz)) { + return emptySet() + } + if (Map::class.java.isAssignableFrom(clazz)) { + return emptyMap() + } + if (clazz.isInterface || Modifier.isAbstract(clazz.modifiers) || seen.containsKey(clazz)) { + return null + } + seen[clazz] = true + try { + if (!AndroidSupport.IS_ANDROID) { + return newDefaultInstance(clazz) + } + return newPublicDefaultInstance(clazz, seen) + } finally { + seen.remove(clazz) + } + } + + private fun newDefaultInstance(clazz: Class<*>): Any? { + return ObjectInstantiators.getObjectInstantiator(clazz).newInstance() + } + + private fun newPublicDefaultInstance( + clazz: Class<*>, + seen: IdentityHashMap, Boolean> + ): Any? { + val constructor = + clazz.constructors.minWithOrNull( + compareBy> { it.parameterCount } + ) ?: return null + val args = + constructor.genericParameterTypes.map { parameterType -> + getDefaultValueForType(parameterType, seen) ?: return null + } + return constructor.newInstance(*args.toTypedArray()) + } } diff --git a/kotlin/fory-kotlin/src/test/kotlin/org/apache/fory/serializer/kotlin/DefaultValueTest.kt b/kotlin/fory-kotlin/src/test/kotlin/org/apache/fory/serializer/kotlin/DefaultValueTest.kt index 62fa3ef5e3..67c76a1ab2 100644 --- a/kotlin/fory-kotlin/src/test/kotlin/org/apache/fory/serializer/kotlin/DefaultValueTest.kt +++ b/kotlin/fory-kotlin/src/test/kotlin/org/apache/fory/serializer/kotlin/DefaultValueTest.kt @@ -131,7 +131,12 @@ class DefaultValueTest { @Test fun testDefaultValueDeserialization() { - val fory = ForyKotlin.builder().withXlang(false).requireClassRegistration(false).withCompatible(true).build() + val fory = + ForyKotlin.builder() + .withXlang(false) + .requireClassRegistration(false) + .withCompatible(true) + .build() val obj = ClassNoDefaults("test") val serialized = fory.serialize(obj) val deserialized = fory.deserialize(serialized, ClassWithDefaults::class.java) diff --git a/scala/build.sbt b/scala/build.sbt index 8e0ac981fd..f72df29bee 100644 --- a/scala/build.sbt +++ b/scala/build.sbt @@ -47,6 +47,8 @@ libraryDependencies ++= Seq( "dev.zio" %% "zio" % "2.1.7" % Test, ) +Test / fork := true + lazy val writeTestClasspath = taskKey[File]("Writes the Scala test runtime classpath") writeTestClasspath := { diff --git a/scala/src/main/java/org/apache/fory/serializer/scala/ToFactorySerializers.java b/scala/src/main/java/org/apache/fory/serializer/scala/ToFactorySerializers.java index 81c0b7019c..8fddbd8d2c 100644 --- a/scala/src/main/java/org/apache/fory/serializer/scala/ToFactorySerializers.java +++ b/scala/src/main/java/org/apache/fory/serializer/scala/ToFactorySerializers.java @@ -19,31 +19,33 @@ package org.apache.fory.serializer.scala; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; import org.apache.fory.config.Config; import org.apache.fory.context.ReadContext; import org.apache.fory.context.WriteContext; -import org.apache.fory.memory.MemoryBuffer; -import org.apache.fory.platform.UnsafeOps; +import org.apache.fory.exception.ForyException; +import org.apache.fory.reflect.FieldAccessor; import org.apache.fory.reflect.ReflectionUtils; import org.apache.fory.serializer.Shareable; import org.apache.fory.serializer.Serializer; -import java.lang.reflect.Field; - -public class ToFactorySerializers { - static final Class IterableToFactoryClass = ReflectionUtils.loadClass( - "scala.collection.IterableFactory$ToFactory"); - static final Class MapToFactoryClass = ReflectionUtils.loadClass( - "scala.collection.MapFactory$ToFactory"); +public class ToFactorySerializers { + static final Class IterableToFactoryClass = + ReflectionUtils.loadClass("scala.collection.IterableFactory$ToFactory"); + static final Class MapToFactoryClass = + ReflectionUtils.loadClass("scala.collection.MapFactory$ToFactory"); public static class IterableToFactorySerializer extends Serializer implements Shareable { - private static final long fieldOffset; + private static final FieldAccessor FACTORY_ACCESSOR; + private static final Constructor CONSTRUCTOR; static { try { - // for graalvm field offset auto rewrite - Field field = Class.forName("scala.collection.IterableFactory$ToFactory").getDeclaredField("factory"); - fieldOffset = UnsafeOps.objectFieldOffset(field); + Field field = IterableToFactoryClass.getDeclaredField("factory"); + FACTORY_ACCESSOR = FieldAccessor.createAccessor(field); + CONSTRUCTOR = IterableToFactoryClass.getDeclaredConstructor(field.getType()); + CONSTRUCTOR.setAccessible(true); } catch (final Exception e) { throw new RuntimeException(e); } @@ -55,25 +57,29 @@ public IterableToFactorySerializer(Config config) { @Override public void write(WriteContext writeContext, Object value) { - writeContext.writeRef(UnsafeOps.getObject(value, fieldOffset)); + writeContext.writeRef(FACTORY_ACCESSOR.getObject(value)); } @Override public Object read(ReadContext readContext) { - Object o = UnsafeOps.newInstance(type); - UnsafeOps.putObject(o, fieldOffset, readContext.readRef()); - return o; + try { + return CONSTRUCTOR.newInstance(readContext.readRef()); + } catch (Exception e) { + throw new ForyException("Failed to create Scala IterableFactory.ToFactory", e); + } } } public static class MapToFactorySerializer extends Serializer implements Shareable { - private static final long fieldOffset; + private static final FieldAccessor FACTORY_ACCESSOR; + private static final Constructor CONSTRUCTOR; static { try { - // for graalvm field offset auto rewrite - Field field = Class.forName("scala.collection.MapFactory$ToFactory").getDeclaredField("factory"); - fieldOffset = UnsafeOps.objectFieldOffset(field); + Field field = MapToFactoryClass.getDeclaredField("factory"); + FACTORY_ACCESSOR = FieldAccessor.createAccessor(field); + CONSTRUCTOR = MapToFactoryClass.getDeclaredConstructor(field.getType()); + CONSTRUCTOR.setAccessible(true); } catch (final Exception e) { throw new RuntimeException(e); } @@ -85,14 +91,16 @@ public MapToFactorySerializer(Config config) { @Override public void write(WriteContext writeContext, Object value) { - writeContext.writeRef(UnsafeOps.getObject(value, fieldOffset)); + writeContext.writeRef(FACTORY_ACCESSOR.getObject(value)); } @Override public Object read(ReadContext readContext) { - Object o = UnsafeOps.newInstance(type); - UnsafeOps.putObject(o, fieldOffset, readContext.readRef()); - return o; + try { + return CONSTRUCTOR.newInstance(readContext.readRef()); + } catch (Exception e) { + throw new ForyException("Failed to create Scala MapFactory.ToFactory", e); + } } } } diff --git a/scala/src/main/scala/org/apache/fory/serializer/scala/RangeSerializer.scala b/scala/src/main/scala/org/apache/fory/serializer/scala/RangeSerializer.scala index 70d5fed928..8bfb30e4e2 100644 --- a/scala/src/main/scala/org/apache/fory/serializer/scala/RangeSerializer.scala +++ b/scala/src/main/scala/org/apache/fory/serializer/scala/RangeSerializer.scala @@ -27,7 +27,6 @@ import org.apache.fory.serializer.Serializer import org.apache.fory.serializer.collection.CollectionLikeSerializer import org.apache.fory.resolver.TypeResolver import java.util -import org.apache.fory.util.unsafe._JDKAccess import java.lang.invoke.{MethodHandle, MethodHandles} import scala.collection.immutable.NumericRange @@ -63,10 +62,10 @@ class RangeSerializer[T <: Range](typeResolver: TypeResolver, cls: Class[T]) private object RangeUtils { - val lookupCache: ClassValue[MethodHandle] = new ClassValue[MethodHandle]() { + private val publicLookup = MethodHandles.publicLookup() + val lookupCache: ClassValue[MethodHandle] = new ClassValue[MethodHandle]() { override protected def computeValue(cls: Class[_]): MethodHandle = { - val lookup: MethodHandles.Lookup = _JDKAccess._trustedLookup(cls) - lookup.unreflectConstructor(cls.getDeclaredConstructors()(0)) + publicLookup.unreflectConstructor(cls.getConstructors()(0)) } } }