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/benchmarks/java/pom.xml b/benchmarks/java/pom.xml index bbd9149b6d..a2ba15d9cf 100644 --- a/benchmarks/java/pom.xml +++ b/benchmarks/java/pom.xml @@ -236,6 +236,117 @@ + + jdk25-benchmark-mrjar-check + + [25,) + + + + + org.apache.maven.plugins + maven-antrun-plugin + + + verify-benchmark-mrjar + package + + run + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -266,6 +377,7 @@ + true org.apache.fory.benchmark @@ -287,6 +399,10 @@ org.openjdk.jmh.Main + + true + org.apache.fory.benchmark + @@ -298,6 +414,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..41aae1fdae --- /dev/null +++ b/benchmarks/java/src/main/java/org/apache/fory/benchmark/Jdk25MrJarCheck.java @@ -0,0 +1,65 @@ +/* + * 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; +import org.apache.fory.platform.internal._JDKAccess; +import org.apache.fory.reflect.FieldAccessor; + +/** 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) { + verifyClass(MemoryBuffer.class); + verifyMissing("org.apache.fory.platform.UnsafeOps"); + verifyClass(_JDKAccess.class); + verifyClass(FieldAccessor.class); + verifyClass("org.apache.fory.serializer.PlatformStringUtils"); + if (_JDKAccess.UNSAFE != null) { + throw new IllegalStateException("JDK25 benchmark jar loaded Unsafe-backed _JDKAccess"); + } + } + + 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 void verifyClass(String className) { + try { + verifyClass(Class.forName(className)); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("JDK25 benchmark jar is missing " + className, e); + } + } + + private static void verifyClass(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); + } + } +} 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..f46b5aacc5 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; @@ -33,10 +32,13 @@ import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.State; +import sun.misc.Unsafe; @BenchmarkMode(Mode.Throughput) @CompilerControl(value = CompilerControl.Mode.INLINE) public class MemorySuite { + private static final Unsafe UNSAFE = UnsafeAccess.load(); + private static final int BYTE_ARRAY_OFFSET = UNSAFE.arrayBaseOffset(byte[].class); static int arrLen = 32; static { @@ -138,12 +140,8 @@ public Object systemArrayCopy(MemoryState state) { @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); + UNSAFE.copyMemory( + state.bytes, BYTE_ARRAY_OFFSET, target, BYTE_ARRAY_OFFSET, state.bytes.length); return target; } 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..762ec646a1 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 @@ -22,14 +22,14 @@ 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; +import sun.misc.Unsafe; public class NewJava11StringSuite { + private static final Unsafe UNSAFE = UnsafeAccess.load(); static String str = StringUtils.random(10); static byte[] strBytes; @@ -37,16 +37,13 @@ public class NewJava11StringSuite { 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")); + strBytes = (byte[]) UNSAFE.getObject(str, fieldOffset(String.class, "value")); + coder = UNSAFE.getByte(str, fieldOffset(String.class, "coder")); } } - 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 final long STRING_VALUE_FIELD_OFFSET = fieldOffset(String.class, "value"); + private static final long STRING_CODER_FIELD_OFFSET = fieldOffset(String.class, "coder"); private static String stubStr = new String(new char[] {Character.MAX_VALUE, Character.MIN_VALUE}); private static Fory fory = @@ -58,6 +55,14 @@ public class NewJava11StringSuite { stringSerializer.writeString(buffer, str); } + private static long fieldOffset(Class type, String fieldName) { + try { + return UNSAFE.objectFieldOffset(type.getDeclaredField(fieldName)); + } catch (NoSuchFieldException e) { + throw new IllegalStateException(e); + } + } + // @Benchmark public Object createJDK11StringByCopyStr() { return new String(str); @@ -66,8 +71,8 @@ public Object createJDK11StringByCopyStr() { // @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); + UNSAFE.putObject(str, STRING_VALUE_FIELD_OFFSET, strBytes); + UNSAFE.putByte(str, STRING_CODER_FIELD_OFFSET, coder); return str; } 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..43e9fe4a1f 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,13 +19,13 @@ 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; +import sun.misc.Unsafe; public class NewStringSuite { + private static final Unsafe UNSAFE = UnsafeAccess.load(); static String str = StringUtils.random(230); static char[] strData = str.toCharArray(); @@ -41,14 +41,21 @@ public Object createJDK8StringByCopy() { return new String(strData); } - private static final long STRING_VALUE_FIELD_OFFSET = - ReflectionUtils.getFieldOffset(String.class, "value"); + private static final long STRING_VALUE_FIELD_OFFSET = fieldOffset(String.class, "value"); private static String stubStr = new String(new char[] {Character.MAX_VALUE, Character.MIN_VALUE}); + private static long fieldOffset(Class type, String fieldName) { + try { + return UNSAFE.objectFieldOffset(type.getDeclaredField(fieldName)); + } catch (NoSuchFieldException e) { + throw new IllegalStateException(e); + } + } + // @Benchmark public Object createJDK8StringByUnsafe() { String str = new String(stubStr); - UnsafeOps.putObject(str, STRING_VALUE_FIELD_OFFSET, strData); + UNSAFE.putObject(str, STRING_VALUE_FIELD_OFFSET, strData); return str; } diff --git a/benchmarks/java/src/main/java/org/apache/fory/benchmark/UnsafeAccess.java b/benchmarks/java/src/main/java/org/apache/fory/benchmark/UnsafeAccess.java new file mode 100644 index 0000000000..c99cc81183 --- /dev/null +++ b/benchmarks/java/src/main/java/org/apache/fory/benchmark/UnsafeAccess.java @@ -0,0 +1,37 @@ +/* + * 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 java.lang.reflect.Field; +import sun.misc.Unsafe; + +final class UnsafeAccess { + private UnsafeAccess() {} + + static Unsafe load() { + try { + Field field = Unsafe.class.getDeclaredField("theUnsafe"); + field.setAccessible(true); + return (Unsafe) field.get(null); + } catch (ReflectiveOperationException e) { + throw new ExceptionInInitializerError(e); + } + } +} diff --git a/benchmarks/java25/README.md b/benchmarks/java25/README.md new file mode 100644 index 0000000000..80e2928f42 --- /dev/null +++ b/benchmarks/java25/README.md @@ -0,0 +1,44 @@ +# 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`. +- `sun.misc.Unsafe` raw native-address access over the same 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. + +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 +``` + +The benchmark class adds fork JVM options only for the explicit Unsafe baseline: + +```text +--add-opens=java.base/java.nio=ALL-UNNAMED +--sun-misc-unsafe-memory-access=allow +``` + +These flags are not part of Fory's JDK25 zero-Unsafe deployment contract. They are present here so +the benchmark can compare supported `ByteBuffer`/FFM access with the old raw-address Unsafe +baseline in the same process shape. + +If you run with `-f 0`, pass those options to the outer `java` command because JMH will not fork a +child JVM. 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..da251382cf --- /dev/null +++ b/benchmarks/java25/src/main/java/org/apache/fory/benchmark/java25/DirectMemoryAccessBenchmark.java @@ -0,0 +1,216 @@ +/* + * 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.lang.reflect.Field; +import java.nio.Buffer; +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; +import sun.misc.Unsafe; + +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 1) +@Fork( + value = 1, + jvmArgsAppend = { + "--add-opens=java.base/java.nio=ALL-UNNAMED", + "--sun-misc-unsafe-memory-access=allow" + }) +@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 Unsafe UNSAFE = loadUnsafe(); + private static final long BUFFER_ADDRESS_OFFSET = bufferAddressOffset(); + 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; + long address; + int intValue; + int intCursor; + long longValue; + int longCursor; + + @Setup + public void setup() { + buffer = ByteBuffer.allocateDirect(BUFFER_BYTES).order(NATIVE_ORDER); + segment = MemorySegment.ofBuffer(buffer); + address = UNSAFE.getLong(buffer, BUFFER_ADDRESS_OFFSET); + intValue = 0x12345678; + longValue = 0x123456789abcdef0L; + for (int i = 0; i < INT_SLOTS; i++) { + int intOffset = i << 2; + int intPattern = intValue + i; + UNSAFE.putInt(null, address + intOffset, intPattern); + } + for (int i = 0; i < LONG_SLOTS; i++) { + int longOffset = i << 3; + long longPattern = longValue + i; + UNSAFE.putLong(null, address + 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 unsafePutInt(DirectState state) { + int offset = state.nextIntOffset(); + int value = state.nextIntValue(); + UNSAFE.putInt(null, state.address + 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 int unsafeGetInt(DirectState state) { + return UNSAFE.getInt(null, state.address + 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 unsafePutLong(DirectState state) { + int offset = state.nextLongOffset(); + long value = state.nextLongValue(); + UNSAFE.putLong(null, state.address + 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()); + } + + @Benchmark + public long unsafeGetLong(DirectState state) { + return UNSAFE.getLong(null, state.address + state.nextLongOffset()); + } + + private static Unsafe loadUnsafe() { + try { + Field field = Unsafe.class.getDeclaredField("theUnsafe"); + field.setAccessible(true); + return (Unsafe) field.get(null); + } catch (ReflectiveOperationException e) { + throw new ExceptionInInitializerError(e); + } + } + + private static long bufferAddressOffset() { + try { + return UNSAFE.objectFieldOffset(Buffer.class.getDeclaredField("address")); + } catch (NoSuchFieldException e) { + throw new ExceptionInInitializerError(e); + } + } +} 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..f3c3658471 --- /dev/null +++ b/benchmarks/java25/src/main/java/org/apache/fory/benchmark/java25/DirectToHeapCopyBenchmark.java @@ -0,0 +1,123 @@ +/* + * 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.reflect.Field; +import java.nio.Buffer; +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; +import sun.misc.Unsafe; + +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 1) +@Fork( + value = 1, + jvmArgsAppend = { + "--add-opens=java.base/java.nio=ALL-UNNAMED", + "--sun-misc-unsafe-memory-access=allow" + }) +@Threads(1) +public class DirectToHeapCopyBenchmark { + private static final int BUFFER_BYTES = 64 * 1024; + private static final Unsafe UNSAFE = loadUnsafe(); + private static final long BUFFER_ADDRESS_OFFSET = bufferAddressOffset(); + private static final int BYTE_ARRAY_OFFSET = UNSAFE.arrayBaseOffset(byte[].class); + + @State(Scope.Thread) + public static class CopyState { + @Param({"128", "256", "512", "1024", "2048"}) + int copySize; + + ByteBuffer directBuffer; + MemorySegment directSegment; + byte[] heapBuffer; + MemorySegment heapSegment; + long directAddress; + + @Setup + public void setup() { + directBuffer = ByteBuffer.allocateDirect(BUFFER_BYTES); + directSegment = MemorySegment.ofBuffer(directBuffer); + heapBuffer = new byte[BUFFER_BYTES]; + heapSegment = MemorySegment.ofArray(heapBuffer); + directAddress = UNSAFE.getLong(directBuffer, BUFFER_ADDRESS_OFFSET); + for (int i = 0; i < BUFFER_BYTES; i++) { + UNSAFE.putByte(null, directAddress + 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]; + } + + @Benchmark + public int unsafeCopyMemory(CopyState state) { + int copySize = state.copySize; + byte[] heap = state.heapBuffer; + UNSAFE.copyMemory(null, state.directAddress, heap, BYTE_ARRAY_OFFSET, copySize); + return heap[copySize - 1]; + } + + private static Unsafe loadUnsafe() { + try { + Field field = Unsafe.class.getDeclaredField("theUnsafe"); + field.setAccessible(true); + return (Unsafe) field.get(null); + } catch (ReflectiveOperationException e) { + throw new ExceptionInInitializerError(e); + } + } + + private static long bufferAddressOffset() { + try { + return UNSAFE.objectFieldOffset(Buffer.class.getDeclaredField("address")); + } catch (NoSuchFieldException e) { + throw new ExceptionInInitializerError(e); + } + } +} 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.sh b/ci/run_ci.sh index 45e096a45e..34434a13d8 100755 --- a/ci/run_ci.sh +++ b/ci/run_ci.sh @@ -61,6 +61,7 @@ install_pyfory() { } JDKS=( +"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 +79,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_deny_options) $(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,27 +101,96 @@ graalvm_test() { echo "Execute graalvm tests succeed!" } -integration_tests() { +jdk25_deny_options() { + local fory_open_targets="ALL-UNNAMED,org.apache.fory.core,org.apache.fory.format" + printf "%s" "--sun-misc-unsafe-memory-access=deny" + printf " %s" "--add-opens=java.base/java.lang=${fory_open_targets}" + printf " %s" "--add-opens=java.base/java.lang.invoke=${fory_open_targets}" + printf " %s" "--add-opens=java.base/java.lang.reflect=${fory_open_targets}" + printf " %s" "--add-opens=java.base/jdk.internal.reflect=${fory_open_targets}" + printf " %s" "--add-opens=java.base/java.util=${fory_open_targets}" + printf " %s" "--add-opens=java.base/java.util.concurrent=${fory_open_targets}" + printf " %s" "--add-opens=java.base/java.util.concurrent.atomic=${fory_open_targets}" + printf " %s" "--add-opens=java.base/java.io=${fory_open_targets}" + printf " %s" "--add-opens=java.base/java.net=${fory_open_targets}" + printf " %s" "--add-opens=java.base/java.math=${fory_open_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" == zulu25* ]]; then + export JDK_JAVA_OPTIONS="$(jdk25_deny_options) $(jdk25_javac_options)" + 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" cd "$ROOT"/java - mvn -T10 -B --no-transfer-progress clean install -DskipTests + mvn -T10 -B --no-transfer-progress clean install -DskipTests -Dmaven.compiler.parameters=true -pl '!:fory-testsuite' + echo "Verify JDK25 benchmark multi-release jar" + cd "$ROOT"/benchmarks/java + mvn -T10 -B --no-transfer-progress -Pjmh -DskipTests install + 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 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 + 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 + use_jdk "$jdk" echo "Second round for compatibility: ${jdk}" mvn -T10 --no-transfer-progress clean test -Dtest=org.apache.fory.integration_tests.JDKCompatibilityTest done @@ -117,11 +198,25 @@ 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_deny_options) $(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 + jdk25_test_classpath=() + if [[ "$java_major" -ge 25 ]]; then + jdk25_test_classpath=(-Dfory.jdk25.test.classpath=true -Dmaven.compiler.parameters=true) + fi + mvn -T10 --batch-mode --no-transfer-progress install "${jdk25_test_classpath[@]}" testcode=$? if [[ $testcode -ne 0 ]]; then exit $testcode diff --git a/ci/tasks/java.py b/ci/tasks/java.py index c42cd39627..2aca7ad071 100644 --- a/ci/tasks/java.py +++ b/ci/tasks/java.py @@ -76,6 +76,90 @@ def install_jdks(): logging.info("JDKs downloaded and installed successfully") +def jdk25_deny_options(): + fory_open_targets = "ALL-UNNAMED,org.apache.fory.core,org.apache.fory.format" + return [ + "--sun-misc-unsafe-memory-access=deny", + f"--add-opens=java.base/java.lang={fory_open_targets}", + f"--add-opens=java.base/java.lang.invoke={fory_open_targets}", + f"--add-opens=java.base/java.lang.reflect={fory_open_targets}", + f"--add-opens=java.base/jdk.internal.reflect={fory_open_targets}", + f"--add-opens=java.base/java.util={fory_open_targets}", + f"--add-opens=java.base/java.util.concurrent={fory_open_targets}", + f"--add-opens=java.base/java.util.concurrent.atomic={fory_open_targets}", + f"--add-opens=java.base/java.io={fory_open_targets}", + f"--add-opens=java.base/java.net={fory_open_targets}", + f"--add-opens=java.base/java.math={fory_open_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 java_version == "25": + os.environ["JDK_JAVA_OPTIONS"] = " ".join( + jdk25_deny_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") + common.cd_project_subdir("java") + common.exec_cmd( + "mvn -T10 -B --no-transfer-progress clean install -DskipTests " + "-Dmaven.compiler.parameters=true -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") + 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 @@ -167,12 +251,25 @@ 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"] = ( - "--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED" - ) + jdk_options = [] + if java_version == "25": + jdk_options.extend(jdk25_deny_options()) + jdk_options.extend(jdk25_javac_options()) + else: + jdk_options.append( + "--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED" + ) + os.environ["JDK_JAVA_OPTIONS"] = " ".join(jdk_options) common.cd_project_subdir("java") - common.exec_cmd("mvn -T10 --batch-mode --no-transfer-progress install") + jdk25_test_classpath = "" + if java_version == "25": + jdk25_test_classpath = ( + " -Dfory.jdk25.test.classpath=true -Dmaven.compiler.parameters=true" + ) + common.exec_cmd( + f"mvn -T10 --batch-mode --no-transfer-progress install{jdk25_test_classpath}" + ) logging.info("Executing fory java tests succeeds") @@ -203,10 +300,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 +308,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 +321,8 @@ 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(): + use_jdk(java_version) logging.info(f"Generating data with JDK: {jdk}") common.exec_cmd( @@ -239,10 +331,8 @@ 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(): + use_jdk(java_version) logging.info(f"Testing compatibility with JDK: {jdk}") common.exec_cmd( @@ -255,6 +345,13 @@ 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_deny_options() + jdk25_javac_options() + ) + else: + os.environ.pop("JDK_JAVA_OPTIONS", None) common.cd_project_subdir("java") common.exec_cmd( @@ -275,6 +372,11 @@ 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 diff --git a/docs/guide/java/troubleshooting.md b/docs/guide/java/troubleshooting.md index e04a325185..9c35b3eb70 100644 --- a/docs/guide/java/troubleshooting.md +++ b/docs/guide/java/troubleshooting.md @@ -148,6 +148,60 @@ Fory fory = Fory.builder() fory.registerSerializer(MyClass.class, new MyClassSerializer(fory.getTypeResolver())); ``` +### JDK25+ zero-Unsafe mode and module opens + +When running on JDK25+ with Unsafe memory access denied, or on a later JDK where denied Unsafe +memory access becomes the default, start the JVM with: + +```bash +--sun-misc-unsafe-memory-access=deny +``` + +If Fory needs private fields in your named module, open the target package to both Java modules. +When any Fory artifact is on the classpath instead of the module path, also include `ALL-UNNAMED`: + +```bash +--add-opens=/=ALL-UNNAMED,org.apache.fory.core,org.apache.fory.format +``` + +Some optimized serializers also need JDK-private packages. For each package in the table, open the +owning JDK module/package to `org.apache.fory.core` and `org.apache.fory.format`; include +`ALL-UNNAMED` too when any Fory artifact is on the classpath. Add only the opens needed by the paths +used in your process: + +| Path | Required opens | +| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | +| String fast paths and throwable fields | `java.base/java.lang` | +| Serialized lambdas | `java.base/java.lang.invoke` | +| Reflection-based object construction | `java.base/java.lang.reflect`, `java.base/jdk.internal.reflect` | +| Collection wrappers, sublists, `EnumMap`, and `StringTokenizer` | `java.base/java.util` | +| Blocking queue capacity serializers | `java.base/java.util.concurrent`, `java.base/java.util.concurrent.atomic` | +| `ByteArrayInputStream`, `ByteArrayOutputStream`, and Java object-stream metadata | `java.base/java.io` | +| URL and networking serializers | `java.base/java.net` | +| Proxy serializers | `java.base/java.lang.reflect` | +| Big number internals | `java.base/java.math` | + +Normal classes with final instance fields require final-field mutation to be enabled for the module +that contains Fory's mutating code when Unsafe allocation is denied. Use the Fory module name on the +module path: + +```bash +--enable-final-field-mutation=org.apache.fory.core +``` + +Use `ALL-UNNAMED` when running Fory on the classpath: + +```bash +--enable-final-field-mutation=ALL-UNNAMED +``` + +Fory restores those final fields through method-handle based access. Non-final fields can still be +restored through generated direct field assignment where available. + +The vectorized Arrow APIs in `fory-format` depend on Apache Arrow's memory layer. With the current +Arrow dependency, those APIs are unavailable when `--sun-misc-unsafe-memory-access=deny` is set +because Arrow initializes its own `sun.misc.Unsafe` memory access internally. + ## Performance Issues ### Slow Initial Serialization diff --git a/docs/guide/kotlin/configuration.md b/docs/guide/kotlin/configuration.md index fc44d872ea..8f338e529e 100644 --- a/docs/guide/kotlin/configuration.md +++ b/docs/guide/kotlin/configuration.md @@ -97,6 +97,29 @@ val fory: ThreadSafeFory = ForyKotlin.builder() All configuration options from Fory Java are available. See [Java Configuration](../java/configuration.md) for the complete list. +## JDK25+ Zero-Unsafe Mode + +On JDK25+ with Unsafe memory access denied, Kotlin classes with final constructor properties need +bindable constructor metadata so Fory can call the primary constructor instead of allocating an +uninitialized instance. Enable Java parameter metadata for Kotlin compilation: + +```kotlin +kotlin { + compilerOptions { + javaParameters = true + } +} +``` + +For Maven builds, configure the Kotlin Maven plugin with: + +```xml +true +``` + +The JVM also needs the module opens and final-field mutation option listed in +[Java Troubleshooting](../java/troubleshooting.md#jdk25-zero-unsafe-mode-and-module-opens). + Common options for Kotlin native-mode payloads: ```kotlin 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/pom.xml b/integration_tests/graalvm_tests/pom.xml index 38d3e5b50f..906347fdfb 100644 --- a/integration_tests/graalvm_tests/pom.xml +++ b/integration_tests/graalvm_tests/pom.xml @@ -176,6 +176,16 @@ -H:+UnlockExperimentalVMOptions + -J--add-opens=java.base/java.lang=ALL-UNNAMED + -J--add-opens=java.base/java.lang.invoke=ALL-UNNAMED + -J--add-opens=java.base/java.lang.reflect=ALL-UNNAMED + -J--add-opens=java.base/jdk.internal.reflect=ALL-UNNAMED + -J--add-opens=java.base/java.util=ALL-UNNAMED + -J--add-opens=java.base/java.util.concurrent=ALL-UNNAMED + -J--add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED + -J--add-opens=java.base/java.io=ALL-UNNAMED + -J--add-opens=java.base/java.net=ALL-UNNAMED + -J--add-opens=java.base/java.math=ALL-UNNAMED @@ -187,13 +197,26 @@ java-agent - java + exec test - ${mainClass} - true - false + ${java.home}/bin/java + + --add-opens=java.base/java.lang=ALL-UNNAMED + --add-opens=java.base/java.lang.invoke=ALL-UNNAMED + --add-opens=java.base/java.lang.reflect=ALL-UNNAMED + --add-opens=java.base/jdk.internal.reflect=ALL-UNNAMED + --add-opens=java.base/java.util=ALL-UNNAMED + --add-opens=java.base/java.util.concurrent=ALL-UNNAMED + --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED + --add-opens=java.base/java.io=ALL-UNNAMED + --add-opens=java.base/java.net=ALL-UNNAMED + --add-opens=java.base/java.math=ALL-UNNAMED + -classpath + + ${mainClass} + @@ -201,5 +224,27 @@ + + jdk25-native + + [25,) + + + + + org.graalvm.buildtools + native-maven-plugin + + + --sun-misc-unsafe-memory-access=deny + + + -J--sun-misc-unsafe-memory-access=deny + + + + + + 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..d81ac3125d 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,6 +19,7 @@ package org.apache.fory.graalvm; +import java.beans.ConstructorProperties; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; @@ -50,6 +51,7 @@ public interface TestInterface { public static class TestInvocationHandler implements InvocationHandler { private final String value; + @ConstructorProperties("value") public TestInvocationHandler(String value) { this.value = value; } diff --git a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/Foo.java b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/Foo.java index 3aa1362faf..e5d4a0065f 100644 --- a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/Foo.java +++ b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/Foo.java @@ -19,6 +19,7 @@ package org.apache.fory.graalvm; +import java.beans.ConstructorProperties; import java.io.Serializable; import java.util.List; import java.util.Map; @@ -30,6 +31,7 @@ public class Foo implements Serializable { List f3; Map f4; + @ConstructorProperties({"f1", "f2", "f3", "f4"}) public Foo(int f1, String f2, List f3, Map f4) { this.f1 = f1; this.f2 = f2; 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..68dca1b20b 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 @@ -19,6 +19,7 @@ package org.apache.fory.graalvm; +import java.beans.ConstructorProperties; import java.io.Serializable; import java.util.AbstractMap; import java.util.Arrays; @@ -66,7 +67,16 @@ public class ObjectStreamExample extends AbstractMap { FORY.ensureSerializersCompiled(); } - final int[] ints = new int[10]; + final int[] ints; + + public ObjectStreamExample() { + this(new int[10]); + } + + @ConstructorProperties("ints") + 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 b0d4c50797..53a40d1b56 100644 --- a/integration_tests/jdk_compatibility_tests/pom.xml +++ b/integration_tests/jdk_compatibility_tests/pom.xml @@ -81,4 +81,36 @@ + + + jdk25-and-higher + + [25,) + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + --sun-misc-unsafe-memory-access=deny + --add-opens=java.base/java.lang=ALL-UNNAMED + --add-opens=java.base/java.lang.invoke=ALL-UNNAMED + --add-opens=java.base/java.lang.reflect=ALL-UNNAMED + --add-opens=java.base/jdk.internal.reflect=ALL-UNNAMED + --add-opens=java.base/java.util=ALL-UNNAMED + --add-opens=java.base/java.util.concurrent=ALL-UNNAMED + --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED + --add-opens=java.base/java.io=ALL-UNNAMED + --add-opens=java.base/java.net=ALL-UNNAMED + --add-opens=java.base/java.math=ALL-UNNAMED + + + + + + + + diff --git a/integration_tests/jpms_tests/pom.xml b/integration_tests/jpms_tests/pom.xml index 1d672155f7..f74ff38671 100644 --- a/integration_tests/jpms_tests/pom.xml +++ b/integration_tests/jpms_tests/pom.xml @@ -76,4 +76,36 @@ + + + jdk25-and-higher + + [25,) + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + --sun-misc-unsafe-memory-access=deny + --add-opens=java.base/java.lang=org.apache.fory.core,org.apache.fory.format + --add-opens=java.base/java.lang.invoke=org.apache.fory.core,org.apache.fory.format + --add-opens=java.base/java.lang.reflect=org.apache.fory.core,org.apache.fory.format + --add-opens=java.base/jdk.internal.reflect=org.apache.fory.core,org.apache.fory.format + --add-opens=java.base/java.util=org.apache.fory.core,org.apache.fory.format + --add-opens=java.base/java.util.concurrent=org.apache.fory.core,org.apache.fory.format + --add-opens=java.base/java.util.concurrent.atomic=org.apache.fory.core,org.apache.fory.format + --add-opens=java.base/java.io=org.apache.fory.core,org.apache.fory.format + --add-opens=java.base/java.net=org.apache.fory.core,org.apache.fory.format + --add-opens=java.base/java.math=org.apache.fory.core,org.apache.fory.format + + + + + + + + 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..02eb446d24 100644 --- a/integration_tests/jpms_tests/src/main/java/module-info.java +++ b/integration_tests/jpms_tests/src/main/java/module-info.java @@ -26,4 +26,8 @@ // 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; + opens org.apache.fory.integration_tests.model to org.apache.fory.core, org.apache.fory.format; } 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..8b0c9e326d --- /dev/null +++ b/integration_tests/jpms_tests/src/main/java/org/apache/fory/integration_tests/model/PrivateFieldBean.java @@ -0,0 +1,32 @@ +/* + * 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; + +public final class PrivateFieldBean { + private 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..25b2aa2e43 --- /dev/null +++ b/integration_tests/jpms_tests/src/main/java/org/apache/fory/integration_tests/publicserializer/PublicSerializerValueSerializer.java @@ -0,0 +1,41 @@ +/* + * 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.serializer.Serializer; +import org.apache.fory.resolver.TypeResolver; + +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..fdfbe0d49e --- /dev/null +++ b/integration_tests/jpms_tests/src/test/java/org/apache/fory/integration_tests/JpmsFieldAccessorTest.java @@ -0,0 +1,50 @@ +/* + * 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 org.apache.fory.Fory; +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.testng.Assert; +import org.testng.annotations.Test; + +public class JpmsFieldAccessorTest { + @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 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); + } +} diff --git a/java/fory-core/pom.xml b/java/fory-core/pom.xml index 4176d74184..7aabd48d97 100644 --- a/java/fory-core/pom.xml +++ b/java/fory-core/pom.xml @@ -77,6 +77,7 @@ + true org.apache.fory.core @@ -92,6 +93,7 @@ shade + false true @@ -118,6 +120,7 @@ + true org.apache.fory.core @@ -135,6 +138,243 @@ + + jdk25-multi-release + + [25,] + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + package + + jar + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 3.1.0 + + + compile-jdk25-multi-release + process-classes + + run + + + + + + + + + + + + + + + + + + prepare-jdk25-test-classes + process-test-classes + + run + + + + + + + + + + + + + + + + + + + verify-jdk25-multi-release-jar + verify + + run + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + patch-jdk25-source-jar + package + + run + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + ${project.build.directory}/jdk25-test-classes + + + + + xlang-parallel 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..3ad373043b 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 @@ -52,7 +52,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; @@ -564,7 +563,8 @@ public T copy(T obj) { private void serializeToStream(OutputStream outputStream, Consumer function) { MemoryBuffer buf = getBuffer(); - if (!AndroidSupport.IS_ANDROID && outputStream.getClass() == ByteArrayOutputStream.class) { + if (MemoryUtils.JDK_BYTE_ARRAY_STREAM_FIELD_ACCESS + && outputStream.getClass() == ByteArrayOutputStream.class) { byte[] oldBytes = buf.getHeapMemory(); // Note: This should not be null. assert oldBytes != null; MemoryUtils.wrap((ByteArrayOutputStream) outputStream, buf); 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..bdd81edaf9 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.platform.internal._JDKAccess; import org.apache.fory.reflect.ReflectionUtils; import org.apache.fory.reflect.TypeRef; import org.apache.fory.resolver.ClassResolver; @@ -153,6 +153,7 @@ import org.apache.fory.type.Types; import org.apache.fory.util.Preconditions; import org.apache.fory.util.StringUtils; +import sun.misc.Unsafe; /** * Generate sequential read/write code for java serialization to speed up performance. It also @@ -420,7 +421,12 @@ protected void registerJITNotifyCallback() { */ protected void addCommonImports() { ctx.addImports( - Fory.class, MemoryBuffer.class, WriteContext.class, ReadContext.class, UnsafeOps.class); + Fory.class, + MemoryBuffer.class, + WriteContext.class, + ReadContext.class, + _JDKAccess.class, + Unsafe.class); ctx.addImports(TypeInfo.class, TypeInfoHolder.class, ClassResolver.class); ctx.addImport(Generated.class); ctx.addImports(LazyInitBeanSerializer.class, EnumSerializer.class); 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..dca762854b 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 @@ -57,7 +57,8 @@ 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.platform.internal._JDKAccess; +import org.apache.fory.reflect.FieldAccessor; import org.apache.fory.reflect.ObjectCreator; import org.apache.fory.reflect.ObjectCreators; import org.apache.fory.reflect.ReflectionUtils; @@ -70,6 +71,7 @@ import org.apache.fory.util.function.Functions; import org.apache.fory.util.record.RecordComponent; import org.apache.fory.util.record.RecordUtils; +import sun.misc.Unsafe; /** * Base builder for generating code to serialize java bean in row-format or object stream format. @@ -198,8 +200,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. */ @@ -310,30 +312,38 @@ 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 new Invoke(getUnsafe(), 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 = + new Invoke( + getUnsafe(), "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,18 +355,45 @@ 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) + Expression reflectFieldRef = getReflectField(field.getDeclaringClass(), field, false); + return new Invoke( + getUnsafe(), "objectFieldOffset", PRIMITIVE_LONG_TYPE, reflectFieldRef) .inline(); }); } else { - long fieldOffset = ReflectionUtils.getFieldOffset(field); - Preconditions.checkArgument(fieldOffset != -1); - return Literal.ofLong(fieldOffset); + return Literal.ofLong(_JDKAccess.UNSAFE.objectFieldOffset(field)); } } + private Reference getUnsafe() { + return getOrCreateField( + true, + Unsafe.class, + "_unsafe_", + () -> new StaticInvoke(_JDKAccess.class, "unsafe", TypeRef.of(Unsafe.class))); + } + + private Reference getFieldAccessor(Descriptor descriptor) { + Field field = descriptor.getField(); + String fieldName = descriptor.getName(); + String fieldAccessorName = + (duplicatedFields.contains(fieldName) + ? field.getDeclaringClass().getName().replaceAll("\\.|\\$", "_") + "_" + : "") + + fieldName + + "_accessor_"; + return getOrCreateField( + true, + FieldAccessor.class, + fieldAccessorName, + () -> + new StaticInvoke( + FieldAccessor.class, + "createAccessor", + TypeRef.of(FieldAccessor.class), + getReflectField(field.getDeclaringClass(), field, false))); + } + /** * Returns an expression that deserialize data as a java bean of type {@link * CodecBuilder#beanClass}. @@ -425,14 +462,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 new Invoke(getUnsafe(), funcName, bean, fieldOffsetExpr, value); } else { - return new StaticInvoke(UnsafeOps.class, "putObject", bean, fieldOffsetExpr, value); + return new Invoke(getUnsafe(), "putObject", bean, fieldOffsetExpr, value); } } @@ -444,7 +491,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 +505,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,14 +543,19 @@ 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) { + if (JdkVersion.MAJOR_VERSION >= 25) { ObjectCreators.getObjectCreator(beanClass); // trigger cache - return new Invoke(getObjectCreator(beanClass), "newInstance", OBJECT_TYPE); + Invoke newInstance = new Invoke(getObjectCreator(beanClass), "newInstance", OBJECT_TYPE); + return sourcePublicAccessible(beanClass) ? new Cast(newInstance, beanType) : newInstance; } - return new StaticInvoke(UnsafeOps.class, "newInstance", OBJECT_TYPE, beanClassExpr()); + Invoke newInstance = + new Invoke(getUnsafe(), "allocateInstance", OBJECT_TYPE, beanClassExpr()); + return sourcePublicAccessible(beanClass) ? new Cast(newInstance, beanType) : newInstance; } } @@ -594,50 +654,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 new Invoke(getUnsafe(), "putByte", base, pos, value); } protected Expression unsafePutBoolean(Expression base, Expression pos, Expression value) { - return new StaticInvoke(UnsafeOps.class, "putBoolean", base, pos, value); + return new Invoke(getUnsafe(), "putBoolean", base, pos, value); } protected Expression unsafePutChar(Expression base, Expression pos, Expression value) { - return new StaticInvoke(UnsafeOps.class, "putChar", base, pos, value); + return new Invoke(getUnsafe(), "putChar", base, pos, value); } protected Expression unsafePutShort(Expression base, Expression pos, Expression value) { - return new StaticInvoke(UnsafeOps.class, "putShort", base, pos, value); + return new Invoke(getUnsafe(), "putShort", base, pos, value); } protected Expression unsafePutInt(Expression base, Expression pos, Expression value) { - return new StaticInvoke(UnsafeOps.class, "putInt", base, pos, value); + return new Invoke(getUnsafe(), "putInt", base, pos, value); } protected Expression unsafePutLong(Expression base, Expression pos, Expression value) { - return new StaticInvoke(UnsafeOps.class, "putLong", base, pos, value); + return new Invoke(getUnsafe(), "putLong", base, pos, value); } protected Expression unsafePutFloat(Expression base, Expression pos, Expression value) { - return new StaticInvoke(UnsafeOps.class, "putFloat", base, pos, value); + return new Invoke(getUnsafe(), "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 new Invoke(getUnsafe(), "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 new Invoke(getUnsafe(), "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 new Invoke(getUnsafe(), "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 = new Invoke(getUnsafe(), "getChar", PRIMITIVE_CHAR_TYPE, base, pos); if (!NativeByteOrder.IS_LITTLE_ENDIAN) { expr = new StaticInvoke(Character.class, "reverseBytes", PRIMITIVE_CHAR_TYPE, expr.inline()); } @@ -645,8 +704,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 = new Invoke(getUnsafe(), "getShort", PRIMITIVE_SHORT_TYPE, base, pos); if (!NativeByteOrder.IS_LITTLE_ENDIAN) { expr = new StaticInvoke(Short.class, "reverseBytes", PRIMITIVE_SHORT_TYPE, expr.inline()); } @@ -654,7 +712,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 = new Invoke(getUnsafe(), "getInt", PRIMITIVE_INT_TYPE, base, pos); if (!NativeByteOrder.IS_LITTLE_ENDIAN) { expr = new StaticInvoke(Integer.class, "reverseBytes", PRIMITIVE_INT_TYPE, expr.inline()); } @@ -662,8 +720,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 = new Invoke(getUnsafe(), "getLong", PRIMITIVE_LONG_TYPE, base, pos); if (!NativeByteOrder.IS_LITTLE_ENDIAN) { expr = new StaticInvoke(Long.class, "reverseBytes", PRIMITIVE_LONG_TYPE, expr.inline()); } @@ -671,7 +728,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 = new Invoke(getUnsafe(), "getInt", PRIMITIVE_INT_TYPE, base, pos); if (!NativeByteOrder.IS_LITTLE_ENDIAN) { expr = new StaticInvoke(Integer.class, "reverseBytes", PRIMITIVE_INT_TYPE, expr.inline()); } @@ -679,8 +736,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 = new Invoke(getUnsafe(), "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/CompatibleCodecBuilder.java b/java/fory-core/src/main/java/org/apache/fory/builder/CompatibleCodecBuilder.java index 3b44448db5..ffeaab8100 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 @@ -42,6 +42,8 @@ import org.apache.fory.memory.MemoryBuffer; import org.apache.fory.meta.TypeDef; import org.apache.fory.platform.GraalvmSupport; +import org.apache.fory.reflect.ObjectCreator; +import org.apache.fory.reflect.ObjectCreators; import org.apache.fory.reflect.TypeRef; import org.apache.fory.resolver.TypeResolver; import org.apache.fory.serializer.CodegenSerializer; @@ -130,6 +132,29 @@ public CompatibleCodecBuilder(TypeRef beanType, Fory fory, TypeDef typeDef) { } this.defaultValueLanguage = defaultValueLanguage; this.defaultValueFields = defaultValueFields; + if (!isRecord) { + initConstructorFields( + sortedDescriptors, + true, + defaultFieldNames(defaultValueFields), + defaultDeclaringClasses(defaultValueFields)); + } + } + + private static String[] defaultFieldNames(DefaultValueUtils.DefaultValueField[] fields) { + String[] names = new String[fields.length]; + for (int i = 0; i < fields.length; i++) { + names[i] = fields[i].getFieldName(); + } + return names; + } + + private static Class[] defaultDeclaringClasses(DefaultValueUtils.DefaultValueField[] fields) { + Class[] declaringClasses = new Class[fields.length]; + for (int i = 0; i < fields.length; i++) { + declaringClasses[i] = fields[i].getDeclaringClass(); + } + return declaringClasses; } // Must be static to be shared across the whole process life. @@ -295,10 +320,25 @@ protected Expression newBean() { Expression.ListExpression setDefaultsExpr = new Expression.ListExpression(); setDefaultsExpr.add(bean); + addDefaultValueSetters(setDefaultsExpr, bean); + setDefaultsExpr.add(bean); + return setDefaultsExpr; + } + + @Override + protected void postCreateConstructorObject( + Expression.ListExpression expressions, Expression bean) { + addDefaultValueSetters(expressions, bean); + } + + private void addDefaultValueSetters(Expression.ListExpression expressions, Expression bean) { Map descriptors = Descriptor.getAllDescriptorsMap(beanClass); for (DefaultValueUtils.DefaultValueField defaultField : defaultValueFields) { Object defaultValue = defaultField.getDefaultValue(); Member member = defaultField.getFieldAccessor().getField(); + if (constructorOwnsField(member)) { + continue; + } Descriptor descriptor = descriptors.get(member); TypeRef typeRef = descriptor.getTypeRef(); Expression defaultValueExpr; @@ -322,9 +362,54 @@ 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; + } + + private boolean constructorOwnsField(Member member) { + if (constructorFieldIndexes == null) { + return false; + } + ObjectCreator objectCreator = ObjectCreators.getObjectCreator(beanClass); + String[] names = objectCreator.getConstructorFieldNames(); + Class[] declaringClasses = objectCreator.getConstructorFieldDeclaringClasses(); + for (int i = 0; i < names.length; i++) { + Class declaringClass = declaringClasses == null ? null : declaringClasses[i]; + if (names[i].equals(member.getName()) + && (declaringClass == null || declaringClass == member.getDeclaringClass())) { + return true; + } + } + return false; + } + + @Override + protected Expression defaultConstructorValue(int constructorParameterIndex) { + ObjectCreator objectCreator = ObjectCreators.getObjectCreator(beanClass); + String fieldName = objectCreator.getConstructorFieldNames()[constructorParameterIndex]; + Class[] declaringClasses = objectCreator.getConstructorFieldDeclaringClasses(); + Class declaringClass = + declaringClasses == null ? null : declaringClasses[constructorParameterIndex]; + for (DefaultValueUtils.DefaultValueField defaultField : defaultValueFields) { + if (!defaultField.getFieldName().equals(fieldName) + || (declaringClass != null && defaultField.getDeclaringClass() != declaringClass)) { + continue; + } + Object defaultValue = defaultField.getDefaultValue(); + TypeRef typeRef = TypeRef.of(constructorFieldTypes[constructorParameterIndex]); + if (typeRef.unwrap().isPrimitive() || typeRef.equals(STRING_TYPE)) { + return new Literal(defaultValue, typeRef); + } + String funcName = "get" + defaultValueLanguage + "DefaultValue"; + return new Expression.Cast( + new StaticInvoke( + DefaultValueUtils.class, + funcName, + OBJECT_TYPE, + staticBeanClassExpr(), + Literal.ofString(fieldName)), + typeRef); + } + return super.defaultConstructorValue(constructorParameterIndex); } } 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..b1d25fcbff 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; @@ -37,6 +41,7 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -46,6 +51,7 @@ 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,9 +64,11 @@ 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.platform.JdkVersion; +import org.apache.fory.reflect.ObjectCreator; import org.apache.fory.reflect.ObjectCreators; import org.apache.fory.reflect.TypeRef; +import org.apache.fory.serializer.AbstractObjectSerializer; import org.apache.fory.serializer.ObjectSerializer; import org.apache.fory.type.BFloat16; import org.apache.fory.type.Descriptor; @@ -93,6 +101,10 @@ public class ObjectCodecBuilder extends BaseObjectCodecBuilder { private final Literal classVersionHash; protected ObjectCodecOptimizer objectCodecOptimizer; protected Map recordReversedMapping; + protected Map fieldIndexes; + protected int[] constructorFieldIndexes; + protected boolean[] constructorFieldMask; + protected Class[] constructorFieldTypes; public ObjectCodecBuilder(Class beanClass, Fory fory) { super(TypeRef.of(beanClass), fory, Generated.GeneratedObjectSerializer.class); @@ -133,6 +145,8 @@ public ObjectCodecBuilder(Class beanClass, Fory fory) { buildRecordComponentDefaultValues(); } recordReversedMapping = RecordUtils.buildFieldToComponentMapping(beanClass); + } else { + initConstructorFields(grouper.getSortedDescriptors(), true); } } @@ -147,6 +161,122 @@ protected ObjectCodecBuilder(TypeRef beanType, Fory fory, Class superSeria } } + protected final void initConstructorFields( + List sortedDescriptors, boolean allowMissingNonFinal) { + initConstructorFields(sortedDescriptors, allowMissingNonFinal, null); + } + + protected final void initConstructorFields( + List sortedDescriptors, boolean allowMissingNonFinal, String[] defaultFields) { + initConstructorFields(sortedDescriptors, allowMissingNonFinal, defaultFields, null); + } + + protected final void initConstructorFields( + List sortedDescriptors, + boolean allowMissingNonFinal, + String[] defaultFields, + Class[] defaultDeclaringClasses) { + ObjectCreator objectCreator = ObjectCreators.getObjectCreator(beanClass); + if (!objectCreator.hasConstructorFields()) { + return; + } + fieldIndexes = buildFieldIndexes(sortedDescriptors); + constructorFieldTypes = objectCreator.getConstructorFieldTypes(); + constructorFieldIndexes = + buildConstructorFieldIndexes( + sortedDescriptors, + objectCreator, + allowMissingNonFinal, + defaultFields, + defaultDeclaringClasses); + constructorFieldMask = buildConstructorFieldMask(sortedDescriptors.size()); + } + + private static Map buildFieldIndexes(List descriptors) { + Map indexes = new IdentityHashMap<>(); + for (int i = 0; i < descriptors.size(); i++) { + indexes.put(descriptors.get(i), i); + } + return indexes; + } + + private int[] buildConstructorFieldIndexes( + List descriptors, + ObjectCreator objectCreator, + boolean allowMissingNonFinal, + String[] defaultFields, + Class[] defaultDeclaringClasses) { + String[] names = objectCreator.getConstructorFieldNames(); + Class[] declaringClasses = objectCreator.getConstructorFieldDeclaringClasses(); + boolean[] finalFields = objectCreator.getConstructorFieldFinal(); + int[] indexes = new int[names.length]; + for (int i = 0; i < names.length; i++) { + Class declaringClass = declaringClasses == null ? null : declaringClasses[i]; + boolean allowMissing = + (allowMissingNonFinal && !finalFields[i]) + || contains(defaultFields, defaultDeclaringClasses, names[i], declaringClass); + indexes[i] = constructorFieldIndex(descriptors, declaringClass, names[i], allowMissing); + } + return indexes; + } + + private static boolean contains( + String[] values, Class[] declaringClasses, String value, Class declaringClass) { + if (values == null) { + return false; + } + for (int i = 0; i < values.length; i++) { + if (values[i].equals(value) + && (declaringClasses == null + || i >= declaringClasses.length + || declaringClasses[i] == null + || declaringClasses[i] == declaringClass)) { + return true; + } + } + return false; + } + + private int constructorFieldIndex( + List descriptors, + Class declaringClass, + String fieldName, + boolean allowMissing) { + int index = -1; + for (int i = 0; i < descriptors.size(); i++) { + Descriptor descriptor = descriptors.get(i); + if (!descriptor.getName().equals(fieldName) + || (declaringClass != null + && (descriptor.getField() == null + || descriptor.getField().getDeclaringClass() != declaringClass))) { + continue; + } + if (index >= 0) { + throw new IllegalStateException( + "Constructor field " + fieldName + " is ambiguous for " + beanClass); + } + index = i; + } + if (index < 0) { + if (allowMissing) { + return -1; + } + throw new IllegalStateException( + "Constructor field " + fieldName + " is not serialized for " + beanClass); + } + return index; + } + + private boolean[] buildConstructorFieldMask(int size) { + boolean[] mask = new boolean[size]; + for (int index : constructorFieldIndexes) { + if (index >= 0) { + mask[index] = true; + } + } + return mask; + } + @Override protected String codecSuffix() { return ""; @@ -250,6 +380,9 @@ protected int getNumPrimitiveFields(List> primitiveGroups) { private List serializePrimitivesUnCompressed( Expression bean, Expression buffer, List> primitiveGroups, int totalSize) { + if (JdkVersion.MAJOR_VERSION >= 25) { + return serializeRawPrimitivesIndexed(bean, buffer, primitiveGroups, totalSize); + } List expressions = new ArrayList<>(); int numPrimitiveFields = getNumPrimitiveFields(primitiveGroups); Literal totalSizeLiteral = new Literal(totalSize, PRIMITIVE_INT_TYPE); @@ -343,6 +476,9 @@ private List serializePrimitivesUnCompressed( private List serializePrimitivesCompressed( Expression bean, Expression buffer, List> primitiveGroups, int totalSize) { + if (JdkVersion.MAJOR_VERSION >= 25) { + return serializeCompressedIndexed(bean, buffer, primitiveGroups, totalSize); + } List expressions = new ArrayList<>(); // int/long may need extra one-byte for writing. int extraSize = 0; @@ -491,6 +627,324 @@ private List serializePrimitivesCompressed( return expressions; } + private List serializeRawPrimitivesIndexed( + Expression bean, Expression buffer, List> primitiveGroups, int totalSize) { + List expressions = new ArrayList<>(); + int numPrimitiveFields = getNumPrimitiveFields(primitiveGroups); + Literal totalSizeLiteral = new Literal(totalSize, PRIMITIVE_INT_TYPE); + expressions.add(new Invoke(buffer, "grow", totalSizeLiteral)); + Expression writerIndex = new Invoke(buffer, "writerIndex", "writerIndex", PRIMITIVE_INT_TYPE); + expressions.add(writerIndex); + int acc = 0; + for (List group : primitiveGroups) { + ListExpression groupExpressions = new ListExpression(); + for (Descriptor descriptor : group) { + int dispatchId = getNumericDescriptorDispatchId(descriptor); + Expression fieldValue = getFieldValue(bean, descriptor); + if (fieldValue instanceof Inlineable) { + ((Inlineable) fieldValue).inline(); + } + if (dispatchId == DispatchId.BOOL) { + groupExpressions.add( + bufferPutBoolean(buffer, getBufferIndex(writerIndex, acc), fieldValue)); + acc += 1; + } else if (dispatchId == DispatchId.INT8) { + groupExpressions.add(bufferPutByte(buffer, getBufferIndex(writerIndex, acc), fieldValue)); + acc += 1; + } else if (dispatchId == DispatchId.UINT8) { + groupExpressions.add( + bufferPutByte( + buffer, + getBufferIndex(writerIndex, acc), + primitiveByteValue(fieldValue, descriptor))); + acc += 1; + } else if (dispatchId == DispatchId.CHAR) { + groupExpressions.add(bufferPutChar(buffer, getBufferIndex(writerIndex, acc), fieldValue)); + acc += 2; + } else if (dispatchId == DispatchId.INT16) { + groupExpressions.add( + bufferPutInt16(buffer, getBufferIndex(writerIndex, acc), fieldValue)); + acc += 2; + } else if (dispatchId == DispatchId.UINT16) { + groupExpressions.add( + bufferPutInt16( + buffer, + getBufferIndex(writerIndex, acc), + primitiveShortValue(fieldValue, descriptor))); + acc += 2; + } else if (dispatchId == DispatchId.FLOAT16 || dispatchId == DispatchId.BFLOAT16) { + groupExpressions.add( + bufferPutInt16( + buffer, + getBufferIndex(writerIndex, acc), + new Invoke(fieldValue, "toBits", SHORT_TYPE))); + acc += 2; + } else if (dispatchId == DispatchId.INT32) { + groupExpressions.add( + bufferPutInt32(buffer, getBufferIndex(writerIndex, acc), fieldValue)); + acc += 4; + } else if (dispatchId == DispatchId.UINT32) { + groupExpressions.add( + bufferPutInt32( + buffer, + getBufferIndex(writerIndex, acc), + primitiveIntValue(fieldValue, descriptor))); + acc += 4; + } else if (dispatchId == DispatchId.INT64 || dispatchId == DispatchId.UINT64) { + groupExpressions.add( + bufferPutInt64(buffer, getBufferIndex(writerIndex, acc), fieldValue)); + acc += 8; + } else if (dispatchId == DispatchId.FLOAT32) { + groupExpressions.add( + bufferPutFloat32(buffer, getBufferIndex(writerIndex, acc), fieldValue)); + acc += 4; + } else if (dispatchId == DispatchId.FLOAT64) { + groupExpressions.add( + bufferPutFloat64(buffer, getBufferIndex(writerIndex, acc), fieldValue)); + acc += 8; + } else { + throw new IllegalStateException("Unsupported dispatchId: " + dispatchId); + } + } + if (hasFewFields() || numPrimitiveFields < 4) { + expressions.add(groupExpressions); + } else { + expressions.add( + objectCodecOptimizer.invokeGenerated( + ofHashSet(bean, buffer, writerIndex), groupExpressions, "writeFields")); + } + } + Expression increaseWriterIndex = + new Invoke( + buffer, + "_increaseWriterIndexUnsafe", + new Literal(totalSizeLiteral, PRIMITIVE_INT_TYPE)); + expressions.add(increaseWriterIndex); + return expressions; + } + + private List serializeCompressedIndexed( + Expression bean, Expression buffer, List> primitiveGroups, int totalSize) { + List expressions = new ArrayList<>(); + int extraSize = 0; + for (List group : primitiveGroups) { + for (Descriptor d : group) { + int id = getNumericDescriptorDispatchId(d); + if (id == DispatchId.INT32 + || id == DispatchId.VARINT32 + || id == DispatchId.VAR_UINT32 + || id == DispatchId.UINT32) { + extraSize += 4; + } else if (id == DispatchId.INT64 + || id == DispatchId.VARINT64 + || id == DispatchId.TAGGED_INT64 + || id == DispatchId.VAR_UINT64 + || id == DispatchId.TAGGED_UINT64 + || id == DispatchId.UINT64) { + extraSize += 1; + } + } + } + int growSize = totalSize + extraSize; + expressions.add(new Invoke(buffer, "grow", Literal.ofInt(growSize))); + int numPrimitiveFields = getNumPrimitiveFields(primitiveGroups); + for (List group : primitiveGroups) { + ListExpression groupExpressions = new ListExpression(); + Expression writerIndex = new Invoke(buffer, "writerIndex", "writerIndex", PRIMITIVE_INT_TYPE); + int acc = 0; + boolean compressStarted = false; + for (Descriptor descriptor : group) { + int dispatchId = getNumericDescriptorDispatchId(descriptor); + Expression fieldValue = getFieldValue(bean, descriptor); + if (fieldValue instanceof Inlineable) { + ((Inlineable) fieldValue).inline(); + } + if (dispatchId == DispatchId.BOOL) { + groupExpressions.add( + bufferPutBoolean(buffer, getBufferIndex(writerIndex, acc), fieldValue)); + acc += 1; + } else if (dispatchId == DispatchId.INT8) { + groupExpressions.add(bufferPutByte(buffer, getBufferIndex(writerIndex, acc), fieldValue)); + acc += 1; + } else if (dispatchId == DispatchId.UINT8) { + groupExpressions.add( + bufferPutByte( + buffer, + getBufferIndex(writerIndex, acc), + primitiveByteValue(fieldValue, descriptor))); + acc += 1; + } else if (dispatchId == DispatchId.CHAR) { + groupExpressions.add(bufferPutChar(buffer, getBufferIndex(writerIndex, acc), fieldValue)); + acc += 2; + } else if (dispatchId == DispatchId.INT16) { + groupExpressions.add( + bufferPutInt16(buffer, getBufferIndex(writerIndex, acc), fieldValue)); + acc += 2; + } else if (dispatchId == DispatchId.UINT16) { + groupExpressions.add( + bufferPutInt16( + buffer, + getBufferIndex(writerIndex, acc), + primitiveShortValue(fieldValue, descriptor))); + acc += 2; + } else if (dispatchId == DispatchId.FLOAT16 || dispatchId == DispatchId.BFLOAT16) { + groupExpressions.add( + bufferPutInt16( + buffer, + getBufferIndex(writerIndex, acc), + new Invoke(fieldValue, "toBits", SHORT_TYPE))); + acc += 2; + } else if (dispatchId == DispatchId.FLOAT32) { + groupExpressions.add( + bufferPutFloat32(buffer, getBufferIndex(writerIndex, acc), fieldValue)); + acc += 4; + } else if (dispatchId == DispatchId.FLOAT64) { + groupExpressions.add( + bufferPutFloat64(buffer, getBufferIndex(writerIndex, acc), fieldValue)); + acc += 8; + } else if (dispatchId == DispatchId.INT32) { + groupExpressions.add( + bufferPutInt32(buffer, getBufferIndex(writerIndex, acc), fieldValue)); + acc += 4; + } else if (dispatchId == DispatchId.UINT32) { + groupExpressions.add( + bufferPutInt32( + buffer, + getBufferIndex(writerIndex, acc), + primitiveIntValue(fieldValue, descriptor))); + acc += 4; + } else if (dispatchId == DispatchId.INT64 || dispatchId == DispatchId.UINT64) { + groupExpressions.add( + bufferPutInt64(buffer, getBufferIndex(writerIndex, 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) { + addIncWriterIndexExpr(groupExpressions, buffer, acc); + } + if (hasFewFields() || numPrimitiveFields < 4) { + expressions.add(groupExpressions); + } else { + expressions.add( + objectCodecOptimizer.invokeGenerated( + ofHashSet(bean, buffer, writerIndex), groupExpressions, "writeFields")); + } + } + return expressions; + } + + 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) { return fieldValue.type().isPrimitive() ? cast(fieldValue, PRIMITIVE_BYTE_TYPE) @@ -548,12 +1002,20 @@ public Expression buildDecodeExpression() { if (typeResolver.checkClassVersion()) { expressions.add(checkClassVersion(buffer)); } + if (!isRecord && constructorFieldIndexes != null) { + return buildConstructorDecodeExpression(buffer, expressions); + } Expression bean; if (!isRecord) { - bean = newBean(); - Expression referenceObject = invokeReadContext("reference", bean); - expressions.add(bean); - expressions.add(referenceObject); + if (constructorFieldIndexes == null) { + bean = newBean(); + Expression referenceObject = invokeReadContext("reference", bean); + expressions.add(bean); + expressions.add(referenceObject); + } else { + bean = new FieldsArray(fieldIndexes.size()); + expressions.add(bean); + } } else { if (recordCtrAccessible) { bean = new FieldsCollector(); @@ -578,33 +1040,258 @@ public Expression buildDecodeExpression() { new Invoke(getObjectCreator(beanClass), "newInstanceWithArguments", OBJECT_TYPE, bean); } } - expressions.add(new Expression.Return(bean)); - return expressions; + expressions.add(new Expression.Return(bean)); + return expressions; + } + + private Expression buildConstructorDecodeExpression( + Reference buffer, ListExpression expressions) { + FieldsArray fieldsArray = new FieldsArray(fieldIndexes.size()); + expressions.add(fieldsArray); + expressions.add( + new StaticInvoke( + AbstractObjectSerializer.class, + "beginConstructorRef", + PRIMITIVE_VOID_TYPE, + readContextRef())); + List bufferedNonConstructorFields = new ArrayList<>(); + int remainingConstructorFields = countConstructorFields(); + Expression bean = null; + if (remainingConstructorFields == 0) { + bean = createCtorBean(expressions, fieldsArray); + } + for (Descriptor descriptor : protocolDescriptors()) { + int index = fieldIndexes.get(descriptor); + walkPath.add(descriptor.getDeclaringClass() + descriptor.getName()); + if (constructorFieldMask[index]) { + expressions.add(deserializeToFieldsArray(fieldsArray, buffer, descriptor, true)); + remainingConstructorFields--; + if (remainingConstructorFields == 0) { + bean = createCtorBean(expressions, fieldsArray); + addBufferedFieldSetters(expressions, bean, fieldsArray, bufferedNonConstructorFields); + } + } else if (bean == null) { + expressions.add(deserializeToFieldsArray(fieldsArray, buffer, descriptor, false)); + bufferedNonConstructorFields.add(descriptor); + } else { + expressions.add(deserializeToBean(bean, buffer, descriptor)); + } + walkPath.removeLast(); + } + expressions.add( + new StaticInvoke( + AbstractObjectSerializer.class, + "endConstructorRef", + PRIMITIVE_VOID_TYPE, + readContextRef())); + expressions.add(new Expression.Return(bean)); + return expressions; + } + + private int countConstructorFields() { + int count = 0; + for (boolean constructorField : constructorFieldMask) { + if (constructorField) { + count++; + } + } + return count; + } + + private List protocolDescriptors() { + List descriptors = new ArrayList<>(); + addDescriptors(descriptors, objectCodecOptimizer.primitiveGroups); + addDescriptors(descriptors, objectCodecOptimizer.boxedReadGroups); + addDescriptors(descriptors, objectCodecOptimizer.nonPrimitiveReadGroups); + return descriptors; + } + + private void addDescriptors(List descriptors, List> groups) { + for (List group : groups) { + descriptors.addAll(group); + } + } + + private Expression createCtorBean(ListExpression expressions, FieldsArray fieldsArray) { + Expression bean = createConstructorObject(fieldsArray); + expressions.add( + new StaticInvoke( + AbstractObjectSerializer.class, + "checkNoUnresolvedReadRef", + PRIMITIVE_VOID_TYPE, + readContextRef(), + staticBeanClassExpr())); + expressions.add(bean); + expressions.add( + new StaticInvoke( + AbstractObjectSerializer.class, + "referenceConstructorRef", + PRIMITIVE_VOID_TYPE, + readContextRef(), + bean)); + postCreateConstructorObject(expressions, bean); + return bean; + } + + private Expression deserializeToFieldsArray( + FieldsArray fieldsArray, Reference buffer, Descriptor descriptor, boolean constructorField) { + TypeRef castTypeRef = + hasCompatibleCollectionArrayRead(descriptor) + ? compatibleReadTargetTypeRef(descriptor) + : descriptor.getTypeRef(); + return deserializeField( + buffer, + descriptor, + expr -> { + Expression value = + constructorField ? tryInlineCast(expr, castTypeRef) : new Cast(expr, OBJECT_TYPE); + value = + new StaticInvoke( + AbstractObjectSerializer.class, + constructorField ? "ctorFieldValue" : "bufferFieldValue", + OBJECT_TYPE, + readContextRef(), + value, + staticBeanClassExpr()); + return setFieldValue(fieldsArray, descriptor, value); + }); + } + + private Expression deserializeToBean(Expression bean, Reference buffer, Descriptor descriptor) { + TypeRef castTypeRef = + hasCompatibleCollectionArrayRead(descriptor) + ? compatibleReadTargetTypeRef(descriptor) + : descriptor.getTypeRef(); + return deserializeField( + buffer, + descriptor, + expr -> setFieldValue(bean, descriptor, tryInlineCast(expr, castTypeRef))); + } + + protected void postCreateConstructorObject(ListExpression expressions, Expression bean) {} + + protected void deserializeReadGroup( + List> readGroups, + int numGroups, + ListExpression expressions, + Expression bean, + Reference buffer) { + for (List group : readGroups) { + if (group.isEmpty()) { + continue; + } + boolean inline = hasFewFields() || (group.size() == 1 && numGroups < 10); + expressions.add(deserializeGroup(group, bean, buffer, inline)); + } + } + + protected Expression buildComponentsArray() { + return new Cast( + new Invoke(recordComponentDefaultValues, "clone", OBJECT_TYPE), OBJECT_ARRAY_TYPE); + } + + protected Expression createRecord(SortedMap recordComponents) { + Expression[] params = recordComponents.values().toArray(new Expression[0]); + return new NewInstance(beanType, params); + } + + protected Expression createConstructorObject(FieldsArray fieldValues) { + Expression[] params = new Expression[constructorFieldIndexes.length]; + Expression[] directParams = new Expression[constructorFieldIndexes.length]; + for (int i = 0; i < constructorFieldIndexes.length; i++) { + int index = constructorFieldIndexes[i]; + if (index < 0) { + params[i] = defaultConstructorValue(i); + } else { + params[i] = fieldValue(fieldValues, index); + } + directParams[i] = tryInlineCast(params[i], TypeRef.of(constructorFieldTypes[i])); + } + ObjectCreator objectCreator = ObjectCreators.getObjectCreator(beanClass); + if (JdkVersion.MAJOR_VERSION >= 25 + && objectCreator.isOnlyPublicConstructor() + && sourcePublicAccessible(beanClass) + && constructorParamsAccessible()) { + return new NewInstance(beanType, directParams); + } + Expression args = new Expression.NewArray(OBJECT_ARRAY_TYPE, params); + Expression newInstance = + new Invoke(getObjectCreator(beanClass), "newInstanceWithArguments", OBJECT_TYPE, args); + return sourcePublicAccessible(beanClass) ? new Cast(newInstance, beanType) : newInstance; + } + + protected Expression defaultConstructorValue(int constructorParameterIndex) { + return new StaticInvoke( + AbstractObjectSerializer.class, + "defaultConstructorValue", + OBJECT_TYPE, + staticClassFieldExpr( + constructorFieldTypes[constructorParameterIndex], + "constructorFieldClass" + constructorParameterIndex + "_")); + } + + private boolean constructorParamsAccessible() { + for (Class constructorFieldType : constructorFieldTypes) { + if (!sourcePublicAccessible(constructorFieldType)) { + return false; + } + } + return true; } - protected void deserializeReadGroup( - List> readGroups, - int numGroups, - ListExpression expressions, - Expression bean, - Reference buffer) { - for (List group : readGroups) { - if (group.isEmpty()) { + private void addNonConstructorFieldSetters( + ListExpression expressions, Expression bean, FieldsArray fieldValues) { + for (Descriptor descriptor : objectCodecOptimizer.descriptorGrouper.getSortedDescriptors()) { + int index = fieldIndexes.get(descriptor); + if (constructorFieldMask[index]) { continue; } - boolean inline = hasFewFields() || (group.size() == 1 && numGroups < 10); - expressions.add(deserializeGroup(group, bean, buffer, inline)); + TypeRef castTypeRef = + hasCompatibleCollectionArrayRead(descriptor) + ? compatibleReadTargetTypeRef(descriptor) + : descriptor.getTypeRef(); + Expression value = + new StaticInvoke( + AbstractObjectSerializer.class, + "resolveBufferedValue", + OBJECT_TYPE, + fieldValue(fieldValues, index), + bean); + value = tryInlineCast(value, castTypeRef); + expressions.add(setFieldValue(bean, descriptor, value)); } } - protected Expression buildComponentsArray() { - return new StaticInvoke( - UnsafeOps.class, "copyObjectArray", OBJECT_ARRAY_TYPE, recordComponentDefaultValues); + private void addBufferedFieldSetters( + ListExpression expressions, + Expression bean, + FieldsArray fieldValues, + List descriptors) { + for (Descriptor descriptor : descriptors) { + int index = fieldIndexes.get(descriptor); + TypeRef castTypeRef = + hasCompatibleCollectionArrayRead(descriptor) + ? compatibleReadTargetTypeRef(descriptor) + : descriptor.getTypeRef(); + Expression value = + new StaticInvoke( + AbstractObjectSerializer.class, + "resolveBufferedValue", + OBJECT_TYPE, + fieldValue(fieldValues, index), + bean); + value = tryInlineCast(value, castTypeRef); + expressions.add(setFieldValue(bean, descriptor, value)); + } } - protected Expression createRecord(SortedMap recordComponents) { - Expression[] params = recordComponents.values().toArray(new Expression[0]); - return new NewInstance(beanType, params); + private Expression fieldValue(Expression fieldValues, int index) { + return new StaticInvoke( + AbstractObjectSerializer.class, + "fieldValue", + OBJECT_TYPE, + fieldValues, + Literal.ofInt(index)); } private class FieldsCollector extends Expression.AbstractExpression { @@ -625,8 +1312,38 @@ public Code.ExprCode doGenCode(CodegenContext ctx) { } } + protected class FieldsArray extends Expression.AbstractExpression { + private final int size; + private final String name; + + protected FieldsArray(int size) { + super(new Expression[0]); + this.size = size; + name = ctx.newName("fieldValues"); + } + + @Override + public TypeRef type() { + return OBJECT_ARRAY_TYPE; + } + + @Override + public Code.ExprCode doGenCode(CodegenContext ctx) { + String code = ctx.type(Object[].class) + " " + name + " = new Object[" + size + "];"; + return new Code.ExprCode(code, FalseLiteral, Code.variable(Object[].class, name)); + } + + int fieldIndex(Descriptor descriptor) { + return fieldIndexes.get(descriptor); + } + } + @Override protected Expression setFieldValue(Expression bean, Descriptor d, Expression value) { + if (bean instanceof FieldsArray) { + return new Expression.AssignArrayElem( + bean, value, Literal.ofInt(((FieldsArray) bean).fieldIndex(d))); + } if (isRecord) { if (recordCtrAccessible) { if (value instanceof Inlineable) { @@ -737,6 +1454,9 @@ protected List deserializePrimitives( private List deserializeUnCompressedPrimitives( Expression bean, Expression buffer, List> primitiveGroups, int totalSize) { + if (JdkVersion.MAJOR_VERSION >= 25) { + return deserializeRawIndexed(bean, buffer, primitiveGroups, totalSize); + } List expressions = new ArrayList<>(); int numPrimitiveFields = getNumPrimitiveFields(primitiveGroups); Literal totalSizeLiteral = Literal.ofInt(totalSize); @@ -841,6 +1561,9 @@ private List deserializeUnCompressedPrimitives( private List deserializeCompressedPrimitives( Expression bean, Expression buffer, List> primitiveGroups) { + if (JdkVersion.MAJOR_VERSION >= 25) { + return deserializeCompressedIndexed(bean, buffer, primitiveGroups); + } List expressions = new ArrayList<>(); int numPrimitiveFields = getNumPrimitiveFields(primitiveGroups); for (List group : primitiveGroups) { @@ -988,6 +1711,247 @@ private List deserializeCompressedPrimitives( return expressions; } + private List deserializeRawIndexed( + Expression bean, Expression buffer, List> primitiveGroups, int totalSize) { + List expressions = new ArrayList<>(); + int numPrimitiveFields = getNumPrimitiveFields(primitiveGroups); + Literal totalSizeLiteral = Literal.ofInt(totalSize); + expressions.add(new Invoke(buffer, "checkReadableBytes", totalSizeLiteral)); + Expression readerIndex = new Invoke(buffer, "readerIndex", "readerIndex", PRIMITIVE_INT_TYPE); + expressions.add(readerIndex); + 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 = bufferGetBoolean(buffer, getBufferIndex(readerIndex, acc)); + acc += 1; + } else if (dispatchId == DispatchId.INT8) { + fieldValue = bufferGetByte(buffer, getBufferIndex(readerIndex, acc)); + acc += 1; + } else if (dispatchId == DispatchId.UINT8) { + fieldValue = + new StaticInvoke( + Byte.class, + "toUnsignedInt", + descriptor.getTypeRef(), + bufferGetByte(buffer, getBufferIndex(readerIndex, acc))); + acc += 1; + } else if (dispatchId == DispatchId.CHAR) { + fieldValue = bufferGetChar(buffer, getBufferIndex(readerIndex, acc)); + acc += 2; + } else if (dispatchId == DispatchId.INT16) { + fieldValue = bufferGetInt16(buffer, getBufferIndex(readerIndex, acc)); + acc += 2; + } else if (dispatchId == DispatchId.UINT16) { + fieldValue = + new StaticInvoke( + Short.class, + "toUnsignedInt", + descriptor.getTypeRef(), + bufferGetInt16(buffer, getBufferIndex(readerIndex, acc))); + acc += 2; + } else if (dispatchId == DispatchId.FLOAT16) { + fieldValue = + new StaticInvoke( + Float16.class, + "fromBits", + TypeRef.of(Float16.class), + bufferGetInt16(buffer, getBufferIndex(readerIndex, acc))); + acc += 2; + } else if (dispatchId == DispatchId.BFLOAT16) { + fieldValue = + new StaticInvoke( + BFloat16.class, + "fromBits", + TypeRef.of(BFloat16.class), + bufferGetInt16(buffer, getBufferIndex(readerIndex, acc))); + acc += 2; + } else if (dispatchId == DispatchId.INT32) { + fieldValue = bufferGetInt32(buffer, getBufferIndex(readerIndex, acc)); + acc += 4; + } else if (dispatchId == DispatchId.UINT32) { + fieldValue = + new StaticInvoke( + Integer.class, + "toUnsignedLong", + descriptor.getTypeRef(), + bufferGetInt32(buffer, getBufferIndex(readerIndex, acc))); + acc += 4; + } else if (dispatchId == DispatchId.INT64 || dispatchId == DispatchId.UINT64) { + fieldValue = bufferGetInt64(buffer, getBufferIndex(readerIndex, acc)); + acc += 8; + } else if (dispatchId == DispatchId.FLOAT32) { + fieldValue = bufferGetFloat32(buffer, getBufferIndex(readerIndex, acc)); + acc += 4; + } else if (dispatchId == DispatchId.FLOAT64) { + fieldValue = bufferGetFloat64(buffer, getBufferIndex(readerIndex, acc)); + acc += 8; + } else { + throw new IllegalStateException("Unsupported dispatchId: " + dispatchId); + } + groupExpressions.add(setFieldValue(bean, descriptor, fieldValue)); + } + if (hasFewFields() || numPrimitiveFields < 4 || isRecord) { + expressions.add(groupExpressions); + } else { + expressions.add( + objectCodecOptimizer.invokeGenerated( + ofHashSet(bean, buffer, readerIndex), groupExpressions, "readFields")); + } + } + Expression increaseReaderIndex = + new Invoke( + buffer, "increaseReaderIndex", new Literal(totalSizeLiteral, PRIMITIVE_INT_TYPE)); + expressions.add(increaseReaderIndex); + return expressions; + } + + private List deserializeCompressedIndexed( + Expression bean, Expression buffer, List> primitiveGroups) { + List expressions = new ArrayList<>(); + int numPrimitiveFields = getNumPrimitiveFields(primitiveGroups); + for (List group : primitiveGroups) { + ReplaceStub checkReadableBytesStub = new ReplaceStub(); + expressions.add(checkReadableBytesStub); + Expression readerIndex = new Invoke(buffer, "readerIndex", "readerIndex", PRIMITIVE_INT_TYPE); + expressions.add(readerIndex); + ListExpression groupExpressions = new ListExpression(); + int acc = 0; + boolean compressStarted = false; + for (Descriptor descriptor : group) { + int dispatchId = getNumericDescriptorDispatchId(descriptor); + Expression fieldValue; + if (dispatchId == DispatchId.BOOL) { + fieldValue = bufferGetBoolean(buffer, getBufferIndex(readerIndex, acc)); + acc += 1; + } else if (dispatchId == DispatchId.INT8) { + fieldValue = bufferGetByte(buffer, getBufferIndex(readerIndex, acc)); + acc += 1; + } else if (dispatchId == DispatchId.UINT8) { + fieldValue = + new StaticInvoke( + Byte.class, + "toUnsignedInt", + descriptor.getTypeRef(), + bufferGetByte(buffer, getBufferIndex(readerIndex, acc))); + acc += 1; + } else if (dispatchId == DispatchId.CHAR) { + fieldValue = bufferGetChar(buffer, getBufferIndex(readerIndex, acc)); + acc += 2; + } else if (dispatchId == DispatchId.INT16) { + fieldValue = bufferGetInt16(buffer, getBufferIndex(readerIndex, acc)); + acc += 2; + } else if (dispatchId == DispatchId.UINT16) { + fieldValue = + new StaticInvoke( + Short.class, + "toUnsignedInt", + descriptor.getTypeRef(), + bufferGetInt16(buffer, getBufferIndex(readerIndex, acc))); + acc += 2; + } else if (dispatchId == DispatchId.FLOAT16) { + fieldValue = + new StaticInvoke( + Float16.class, + "fromBits", + TypeRef.of(Float16.class), + bufferGetInt16(buffer, getBufferIndex(readerIndex, acc))); + acc += 2; + } else if (dispatchId == DispatchId.BFLOAT16) { + fieldValue = + new StaticInvoke( + BFloat16.class, + "fromBits", + TypeRef.of(BFloat16.class), + bufferGetInt16(buffer, getBufferIndex(readerIndex, acc))); + acc += 2; + } else if (dispatchId == DispatchId.FLOAT32) { + fieldValue = bufferGetFloat32(buffer, getBufferIndex(readerIndex, acc)); + acc += 4; + } else if (dispatchId == DispatchId.FLOAT64) { + fieldValue = bufferGetFloat64(buffer, getBufferIndex(readerIndex, acc)); + acc += 8; + } else if (dispatchId == DispatchId.INT32) { + fieldValue = bufferGetInt32(buffer, getBufferIndex(readerIndex, acc)); + acc += 4; + } else if (dispatchId == DispatchId.UINT32) { + fieldValue = + new StaticInvoke( + Integer.class, + "toUnsignedLong", + descriptor.getTypeRef(), + bufferGetInt32(buffer, getBufferIndex(readerIndex, acc))); + acc += 4; + } else if (dispatchId == DispatchId.INT64 || dispatchId == DispatchId.UINT64) { + fieldValue = bufferGetInt64(buffer, getBufferIndex(readerIndex, 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); + } + 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 (hasFewFields() || numPrimitiveFields < 4 || isRecord) { + expressions.add(groupExpressions); + } else { + expressions.add( + objectCodecOptimizer.invokeGenerated( + ofHashSet(bean, buffer, readerIndex), groupExpressions, "readFields")); + } + } + return expressions; + } + private void addIncReaderIndexExpr(ListExpression expressions, Expression buffer, int diff) { if (diff != 0) { expressions.add(new Invoke(buffer, "increaseReaderIndex", Literal.ofInt(diff))); @@ -1000,4 +1964,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/codegen/Expression.java b/java/fory-core/src/main/java/org/apache/fory/codegen/Expression.java index d7cdfcb91a..db3439023a 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.platform.JdkVersion; +import org.apache.fory.platform.internal._JDKAccess; +import org.apache.fory.reflect.ObjectCreator; +import org.apache.fory.reflect.ObjectCreators; 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 creator = ctx.newName("objectCreator"); + codeBuilder + .append( + ExpressionUtils.callFunc( + ctx.type(ObjectCreator.class), + creator, + ctx.type(ObjectCreators.class), + "getObjectCreator", + clzName + ".class", + false)) + .append('\n'); + target = creator; + functionName = "newInstance"; + args = ""; + } else { + target = ctx.type(_JDKAccess.class) + ".unsafe()"; + 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/context/CopyContext.java b/java/fory-core/src/main/java/org/apache/fory/context/CopyContext.java index 5eea194405..f17c170ca7 100644 --- a/java/fory-core/src/main/java/org/apache/fory/context/CopyContext.java +++ b/java/fory-core/src/main/java/org/apache/fory/context/CopyContext.java @@ -21,6 +21,7 @@ import java.util.Arrays; import org.apache.fory.collection.IdentityMap; +import org.apache.fory.exception.ForyException; import org.apache.fory.resolver.ClassResolver; import org.apache.fory.resolver.TypeInfo; import org.apache.fory.resolver.TypeResolver; @@ -41,6 +42,7 @@ public final class CopyContext { private final TypeResolver typeResolver; private final boolean copyRefTracking; private final IdentityMap originToCopyMap; + private static final Object COPY_IN_PROGRESS = new Object(); /** * Creates a copy context for one runtime. @@ -88,7 +90,26 @@ public void reference(T origin, T copied) { /** Returns the previously registered copy for {@code origin}, or {@code null} if absent. */ public T getCopyObject(T origin) { - return (T) originToCopyMap.get(origin); + Object copied = originToCopyMap.get(origin); + if (copied == COPY_IN_PROGRESS) { + throw new ForyException( + "Cyclic references to constructor-bound objects cannot be copied before construction."); + } + return (T) copied; + } + + /** Marks {@code origin} as being copied before a constructor-bound copy can be registered. */ + public void markCopying(Object origin) { + if (copyRefTracking && origin != null) { + originToCopyMap.put(origin, COPY_IN_PROGRESS); + } + } + + /** Clears an in-progress constructor-bound copy marker after a failed copy. */ + public void cancelCopy(Object origin) { + if (copyRefTracking && origin != null && originToCopyMap.get(origin) == COPY_IN_PROGRESS) { + originToCopyMap.remove(origin); + } } /** diff --git a/java/fory-core/src/main/java/org/apache/fory/context/MapRefReader.java b/java/fory-core/src/main/java/org/apache/fory/context/MapRefReader.java index 59101b61ac..328acbf78d 100644 --- a/java/fory-core/src/main/java/org/apache/fory/context/MapRefReader.java +++ b/java/fory-core/src/main/java/org/apache/fory/context/MapRefReader.java @@ -38,6 +38,8 @@ public final class MapRefReader implements RefReader { private long readTotalObjectSize = 0; private final ObjectArray readObjects = new ObjectArray(DEFAULT_ARRAY_CAPACITY); private final IntArray readRefIds = new IntArray(DEFAULT_ARRAY_CAPACITY); + private final IntArray trackedRefIds = new IntArray(DEFAULT_ARRAY_CAPACITY); + private final IntArray unresolvedRefIds = new IntArray(DEFAULT_ARRAY_CAPACITY); private Object readObject; /** Reads a ref-or-null header and resolves cached references immediately when present. */ @@ -45,7 +47,7 @@ public final class MapRefReader implements RefReader { public byte readRefOrNull(MemoryBuffer buffer) { byte headFlag = buffer.readByte(); if (headFlag == Fory.REF_FLAG) { - readObject = getReadRef(buffer.readVarUInt32Small14()); + readObject = readRef(buffer.readVarUInt32Small14()); } else { readObject = null; } @@ -73,7 +75,7 @@ public int preserveRefId(int refId) { public int tryPreserveRefId(MemoryBuffer buffer) { byte headFlag = buffer.readByte(); if (headFlag == Fory.REF_FLAG) { - readObject = getReadRef(buffer.readVarUInt32Small14()); + readObject = readRef(buffer.readVarUInt32Small14()); } else { readObject = null; if (headFlag == Fory.REF_VALUE_FLAG) { @@ -104,6 +106,26 @@ public void reference(Object object) { setReadRef(refId, object); } + /** Binds a reserved ref id that may no longer be the top of the pending-id stack. */ + @Override + public void reference(int refId, Object object) { + removePreservedRefId(refId); + setReadRef(refId, object); + } + + private void removePreservedRefId(int refId) { + for (int i = readRefIds.size - 1; i >= 0; i--) { + if (readRefIds.elementData[i] == refId) { + int numMoved = readRefIds.size - i - 1; + if (numMoved > 0) { + System.arraycopy(readRefIds.elementData, i + 1, readRefIds.elementData, i, numMoved); + } + readRefIds.size--; + return; + } + } + } + /** Returns the previously materialized object stored at {@code id}. */ @Override public Object getReadRef(int id) { @@ -116,6 +138,26 @@ public Object getReadRef() { return readObject; } + private Object readRef(int id) { + if (trackedRefIds.size == 0) { + return readObjects.get(id); + } + Object object = readObjects.get(id); + if (object == null && isTrackedRef(id)) { + unresolvedRefIds.add(id); + } + return object; + } + + private boolean isTrackedRef(int id) { + for (int i = trackedRefIds.size - 1; i >= 0; i--) { + if (trackedRefIds.get(i) == id) { + return true; + } + } + return false; + } + /** Stores {@code object} under an already reserved read ref id. */ @Override public void setReadRef(int id, Object object) { @@ -124,6 +166,44 @@ public void setReadRef(int id, Object object) { } } + @Override + public void trackUnresolvedRef(int id) { + trackedRefIds.add(id); + } + + @Override + public boolean hasTrackedRef() { + return trackedRefIds.size > 0; + } + + @Override + public int currentTrackedRefId() { + return trackedRefIds.get(trackedRefIds.size - 1); + } + + @Override + public void untrackUnresolvedRef() { + if (trackedRefIds.size > 0) { + trackedRefIds.pop(); + } + } + + @Override + public boolean consumeUnresolvedRef(int id) { + boolean found = false; + int newSize = 0; + for (int i = 0; i < unresolvedRefIds.size; i++) { + int unresolvedRefId = unresolvedRefIds.get(i); + if (unresolvedRefId == id) { + found = true; + } else { + unresolvedRefIds.elementData[newSize++] = unresolvedRefId; + } + } + unresolvedRefIds.size = newSize; + return found; + } + /** Exposes the resolved read-reference table for debugging and focused tests. */ public ObjectArray getReadRefs() { return readObjects; @@ -146,6 +226,8 @@ public void reset() { } readObjects.clearApproximate(avg); readRefIds.clear(); + trackedRefIds.clear(); + unresolvedRefIds.clear(); readObject = null; } } diff --git a/java/fory-core/src/main/java/org/apache/fory/context/ReadContext.java b/java/fory-core/src/main/java/org/apache/fory/context/ReadContext.java index 0b03800dd6..6771912454 100644 --- a/java/fory-core/src/main/java/org/apache/fory/context/ReadContext.java +++ b/java/fory-core/src/main/java/org/apache/fory/context/ReadContext.java @@ -360,6 +360,11 @@ public void reference(Object object) { refReader.reference(object); } + /** Binds a specific preserved read ref id to {@code object}. */ + public void reference(int refId, Object object) { + refReader.reference(refId, object); + } + /** Returns a previously read object by ref id. */ public Object getReadRef(int id) { return refReader.getReadRef(id); @@ -375,6 +380,31 @@ public void setReadRef(int id, Object object) { refReader.setReadRef(id, object); } + /** Starts tracking unresolved reads of {@code id} while a constructor-bound object is read. */ + public void trackUnresolvedRef(int id) { + refReader.trackUnresolvedRef(id); + } + + /** Returns whether a constructor-bound object ref id is currently tracked. */ + public boolean hasTrackedRef() { + return refReader.hasTrackedRef(); + } + + /** Returns the active constructor-bound object ref id. */ + public int currentTrackedRefId() { + return refReader.currentTrackedRefId(); + } + + /** Stops tracking unresolved reads for the most recent constructor-bound object. */ + public void untrackUnresolvedRef() { + refReader.untrackUnresolvedRef(); + } + + /** Returns and clears whether {@code id} was read before it was bound to an object. */ + public boolean consumeUnresolvedRef(int id) { + return refReader.consumeUnresolvedRef(id); + } + /** Returns the read-side meta-string state for the current runtime. */ public MetaStringReader getMetaStringReader() { return metaStringReader; diff --git a/java/fory-core/src/main/java/org/apache/fory/context/RefReader.java b/java/fory-core/src/main/java/org/apache/fory/context/RefReader.java index c295bedc38..f18b52973b 100644 --- a/java/fory-core/src/main/java/org/apache/fory/context/RefReader.java +++ b/java/fory-core/src/main/java/org/apache/fory/context/RefReader.java @@ -49,6 +49,11 @@ public interface RefReader { /** Binds the most recently preserved reference id to {@code object}. */ void reference(Object object); + /** Binds a specific preserved reference id to {@code object}. */ + default void reference(int refId, Object object) { + reference(object); + } + /** Returns the previously materialized object for a specific ref id. */ Object getReadRef(int id); @@ -58,6 +63,27 @@ public interface RefReader { /** Replaces the object stored for a previously preserved ref id. */ void setReadRef(int id, Object object); + /** Starts tracking unresolved reads of the currently constructed object's ref id. */ + default void trackUnresolvedRef(int id) {} + + /** Returns whether a constructor-bound object ref id is currently tracked. */ + default boolean hasTrackedRef() { + return false; + } + + /** Returns the most recently tracked constructor-bound object ref id. */ + default int currentTrackedRefId() { + return -1; + } + + /** Stops tracking unresolved reads for the most recent constructor-bound object. */ + default void untrackUnresolvedRef() {} + + /** Returns and clears whether {@code id} was read before it was bound to an object. */ + default boolean consumeUnresolvedRef(int id) { + return false; + } + /** Clears all per-operation ref-tracking state. */ void reset(); @@ -96,6 +122,9 @@ public boolean hasPreservedRefId() { @Override public void reference(Object object) {} + @Override + public void reference(int refId, Object object) {} + @Override public Object getReadRef(int id) { return null; 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..d3facaee67 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._JDKAccess; +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 : _JDKAccess.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,11 @@ 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); + long v = UNSAFE.getLong(o, BYTE_ARRAY_OFFSET + index); return NativeByteOrder.IS_LITTLE_ENDIAN ? v : Long.reverseBytes(v); } @@ -91,22 +87,6 @@ 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); + UNSAFE.putLong(o, 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..5b0e73617d 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._JDKAccess; 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 : _JDKAccess.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; + // Global allocator instance that can be customized private static volatile MemoryAllocator globalAllocator = new DefaultMemoryAllocator(); + 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 = UNSAFE.objectFieldOffset(addressField); + checkArgument(BUFFER_ADDRESS_FIELD_OFFSET != 0); + } 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, DirectBufferAccess.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..41db2cfe46 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 @@ -23,11 +23,42 @@ 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.GraalvmSupport; +import org.apache.fory.platform.internal._JDKAccess; /** Memory utils for fory. */ public class MemoryUtils { + // JDK25+ internal-field access must be backed by supported access in the multi-release classes. + // When a JDK25+ path needs JDK private fields, open the needed java.base package to both + // org.apache.fory.core and org.apache.fory.format. + public static final boolean JDK_INTERNAL_FIELD_ACCESS = + !AndroidSupport.IS_ANDROID + && !GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE + && _JDKAccess.JDK_INTERNAL_FIELD_ACCESS; + public static final boolean JDK_LANG_FIELD_ACCESS = + !AndroidSupport.IS_ANDROID + && !GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE + && _JDKAccess.JDK_LANG_FIELD_ACCESS; + public static final boolean JDK_BYTE_ARRAY_STREAM_FIELD_ACCESS = + !AndroidSupport.IS_ANDROID + && !GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE + && _JDKAccess.JDK_BYTE_ARRAY_STREAM_FIELD_ACCESS; + public static final boolean JDK_OBJECT_STREAM_FIELD_ACCESS = + !AndroidSupport.IS_ANDROID + && !GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE + && _JDKAccess.JDK_OBJECT_STREAM_FIELD_ACCESS; + public static final boolean JDK_COLLECTION_FIELD_ACCESS = + !AndroidSupport.IS_ANDROID + && !GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE + && _JDKAccess.JDK_COLLECTION_FIELD_ACCESS; + public static final boolean JDK_CONCURRENT_FIELD_ACCESS = + !AndroidSupport.IS_ANDROID + && !GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE + && _JDKAccess.JDK_CONCURRENT_FIELD_ACCESS; + public static final boolean JDK_PROXY_FIELD_ACCESS = + !AndroidSupport.IS_ANDROID + && !GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE + && _JDKAccess.JDK_PROXY_FIELD_ACCESS; public static MemoryBuffer buffer(int size) { return wrap(new byte[size]); @@ -71,46 +102,13 @@ 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); + checkByteArrayStreamWrap("ByteArrayOutputStream"); + _JDKAccess.wrap(stream, buffer); } /** @@ -118,15 +116,8 @@ public static void wrap(ByteArrayOutputStream stream, MemoryBuffer buffer) { * 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()); + checkByteArrayStreamWrap("ByteArrayOutputStream"); + _JDKAccess.wrap(buffer, stream); } /** @@ -134,16 +125,17 @@ public static void wrap(MemoryBuffer buffer, ByteArrayOutputStream stream) { * be the pos of stream. */ public static void wrap(ByteArrayInputStream stream, MemoryBuffer buffer) { - if (AndroidSupport.IS_ANDROID) { + checkByteArrayStreamWrap("ByteArrayInputStream"); + _JDKAccess.wrap(stream, buffer); + } + + private static void checkByteArrayStreamWrap(String streamType) { + if (!JDK_BYTE_ARRAY_STREAM_FIELD_ACCESS) { throw new UnsupportedOperationException( - "ByteArrayInputStream direct wrapping is not supported on Android"); + streamType + + " direct wrapping requires JDK internal field access. On JDK25+, open " + + "java.base/java.io to org.apache.fory.core,org.apache.fory.format."); } - 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) { 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/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/util/unsafe/DefineClass.java b/java/fory-core/src/main/java/org/apache/fory/platform/internal/DefineClass.java similarity index 91% rename from java/fory-core/src/main/java/org/apache/fory/util/unsafe/DefineClass.java rename to java/fory-core/src/main/java/org/apache/fory/platform/internal/DefineClass.java index eca71ea811..c340e673a7 100644 --- a/java/fory-core/src/main/java/org/apache/fory/util/unsafe/DefineClass.java +++ b/java/fory-core/src/main/java/org/apache/fory/platform/internal/DefineClass.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; @@ -78,4 +78,9 @@ public static Class defineClass( throw new RuntimeException(e); } } + + public static Class defineHiddenNestmate(Class neighbor, byte[] bytecodes) { + throw new UnsupportedOperationException( + "Hidden nestmate class definition requires the JDK25 multi-release DefineClass"); + } } diff --git a/java/fory-core/src/main/java/org/apache/fory/platform/internal/_JDKAccess.java b/java/fory-core/src/main/java/org/apache/fory/platform/internal/_JDKAccess.java new file mode 100644 index 0000000000..9efba19593 --- /dev/null +++ b/java/fory-core/src/main/java/org/apache/fory/platform/internal/_JDKAccess.java @@ -0,0 +1,701 @@ +/* + * 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.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectStreamClass; +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.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Consumer; +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.memory.MemoryBuffer; +import org.apache.fory.platform.AndroidSupport; +import org.apache.fory.platform.GraalvmSupport; +import org.apache.fory.platform.JdkVersion; +import org.apache.fory.type.TypeUtils; +import org.apache.fory.util.ExceptionUtils; +import org.apache.fory.util.Preconditions; +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 sun.misc.Unsafe; + +/** Unsafe JDK utils. */ +// CHECKSTYLE.OFF:TypeName +public class _JDKAccess { + // CHECKSTYLE.ON:TypeName + public static final boolean IS_OPEN_J9; + public static final Unsafe UNSAFE; + // Root classes use Unsafe for JDK internal fields. A JDK25 multi-release _JDKAccess must keep + // this API surface and implement supported cases with VarHandle, or set this false so callers + // choose public fallbacks. + public static final boolean JDK_INTERNAL_FIELD_ACCESS; + public static final boolean JDK_LANG_FIELD_ACCESS; + public static final boolean JDK_STRING_FIELD_ACCESS; + public static final boolean JDK_BYTE_ARRAY_STREAM_FIELD_ACCESS; + public static final boolean JDK_OBJECT_STREAM_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; + public static final Class _INNER_UNSAFE_CLASS; + public static final Object _INNER_UNSAFE; + + 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 (AndroidSupport.IS_ANDROID) { + JDK_INTERNAL_FIELD_ACCESS = false; + JDK_LANG_FIELD_ACCESS = false; + JDK_STRING_FIELD_ACCESS = false; + JDK_BYTE_ARRAY_STREAM_FIELD_ACCESS = false; + JDK_OBJECT_STREAM_FIELD_ACCESS = false; + JDK_COLLECTION_FIELD_ACCESS = false; + JDK_CONCURRENT_FIELD_ACCESS = false; + JDK_PROXY_FIELD_ACCESS = false; + } else { + JDK_INTERNAL_FIELD_ACCESS = true; + JDK_LANG_FIELD_ACCESS = true; + JDK_STRING_FIELD_ACCESS = true; + JDK_BYTE_ARRAY_STREAM_FIELD_ACCESS = true; + JDK_OBJECT_STREAM_FIELD_ACCESS = true; + JDK_COLLECTION_FIELD_ACCESS = true; + JDK_CONCURRENT_FIELD_ACCESS = true; + JDK_PROXY_FIELD_ACCESS = true; + } + Object innerUnsafe = null; + Class innerUnsafeClass = null; + if (!AndroidSupport.IS_ANDROID && JdkVersion.MAJOR_VERSION >= 11) { + try { + Field theInternalUnsafeField = Unsafe.class.getDeclaredField("theInternalUnsafe"); + theInternalUnsafeField.setAccessible(true); + innerUnsafe = theInternalUnsafeField.get(null); + innerUnsafeClass = innerUnsafe.getClass(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + _INNER_UNSAFE = innerUnsafe; + _INNER_UNSAFE_CLASS = innerUnsafeClass; + } + + public static Unsafe unsafe() { + return UNSAFE; + } + + private static final ClassValueCache lookupCache = ClassValueCache.newClassKeyCache(32); + + public static final boolean STRING_VALUE_FIELD_IS_CHARS; + public static final boolean STRING_VALUE_FIELD_IS_BYTES; + public static final boolean STRING_HAS_COUNT_OFFSET; + public static final long STRING_VALUE_FIELD_OFFSET; + public static final long STRING_COUNT_FIELD_OFFSET; + public static final long STRING_OFFSET_FIELD_OFFSET; + + 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 { + try { + Field valueField = String.class.getDeclaredField("value"); + STRING_VALUE_FIELD_IS_CHARS = valueField.getType() == char[].class; + STRING_VALUE_FIELD_IS_BYTES = valueField.getType() == byte[].class; + STRING_VALUE_FIELD_OFFSET = UNSAFE.objectFieldOffset(valueField); + Field countField = getStringFieldNullable("count"); + Field offsetField = getStringFieldNullable("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 = UNSAFE.objectFieldOffset(countField); + STRING_OFFSET_FIELD_OFFSET = UNSAFE.objectFieldOffset(offsetField); + } else { + STRING_HAS_COUNT_OFFSET = false; + STRING_COUNT_FIELD_OFFSET = -1; + STRING_OFFSET_FIELD_OFFSET = -1; + } + } 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 class StringCoderField { + private static final long OFFSET; + + static { + try { + OFFSET = UNSAFE.objectFieldOffset(String.class.getDeclaredField("coder")); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } + } + } + + public static final long STRING_CODER_FIELD_OFFSET = + STRING_VALUE_FIELD_IS_BYTES ? StringCoderField.OFFSET : -1; + + public static Object getStringValue(String value) { + return UNSAFE.getObject(value, STRING_VALUE_FIELD_OFFSET); + } + + public static byte getStringCoder(String value) { + return UNSAFE.getByte(value, STRING_CODER_FIELD_OFFSET); + } + + public static int getStringOffset(String value) { + return UNSAFE.getInt(value, STRING_OFFSET_FIELD_OFFSET); + } + + public static int getStringCount(String value) { + return UNSAFE.getInt(value, STRING_COUNT_FIELD_OFFSET); + } + + // CHECKSTYLE.OFF:MethodName + + public static Lookup _trustedLookup(Class objectClass) { + // CHECKSTYLE.ON:MethodName + if (GraalvmSupport.isGraalBuildTime()) { + // Lookup will init `java.io.FilePermission`,which is not allowed at graalvm build time + // as a reachable object. + return _Lookup._trustedLookup(objectClass); + } + return lookupCache.get(objectClass, () -> _Lookup._trustedLookup(objectClass)); + } + + public static MethodHandle readResolveHandle(Class cls, Method method) + throws IllegalAccessException { + return _trustedLookup(cls).unreflect(method); + } + + private static final byte LATIN1 = 0; + private static final Byte LATIN1_BOXED = LATIN1; + private static final byte UTF16 = 1; + private static final Byte UTF16_BOXED = UTF16; + private static final MethodHandles.Lookup STRING_LOOK_UP = + JDK_INTERNAL_FIELD_ACCESS ? _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; + + public static String newCharsStringZeroCopy(char[] 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"); + } + return CHARS_STRING_ZERO_COPY_CTR.apply(data, Boolean.TRUE); + } + + public static String newBytesStringZeroCopy(byte coder, byte[] data) { + if (!JDK_INTERNAL_FIELD_ACCESS) { + return newBytesStringSlow(coder, data); + } + if (coder == LATIN1) { + if (LATIN_BYTES_STRING_ZERO_COPY_CTR != null) { + return LATIN_BYTES_STRING_ZERO_COPY_CTR.apply(data); + } + return BYTES_STRING_ZERO_COPY_CTR.apply(data, LATIN1_BOXED); + } else if (coder == UTF16) { + return BYTES_STRING_ZERO_COPY_CTR.apply(data, UTF16_BOXED); + } else { + return BYTES_STRING_ZERO_COPY_CTR.apply(data, coder); + } + } + + private static String newBytesStringSlow(byte coder, byte[] data) { + if (coder == LATIN1) { + return new String(data, StandardCharsets.ISO_8859_1); + } else if (coder == UTF16) { + char[] chars = new char[data.length >> 1]; + for (int i = 0, j = 0; i < data.length; i += 2) { + chars[j++] = (char) ((data[i] & 0xff) | ((data[i + 1] & 0xff) << 8)); + } + return new String(chars); + } else { + return new String(data, StandardCharsets.UTF_8); + } + } + + private static BiFunction getCharsStringZeroCopyCtr() { + if (!STRING_VALUE_FIELD_IS_CHARS) { + return null; + } + MethodHandle handle = getJavaStringZeroCopyCtrHandle(); + if (handle == null) { + return null; + } + try { + CallSite callSite = + LambdaMetafactory.metafactory( + STRING_LOOK_UP, + "apply", + MethodType.methodType(BiFunction.class), + handle.type().generic(), + handle, + handle.type()); + return (BiFunction) callSite.getTarget().invokeExact(); + } catch (Throwable e) { + return null; + } + } + + private static BiFunction getBytesStringZeroCopyCtr() { + if (!STRING_VALUE_FIELD_IS_BYTES) { + return null; + } + MethodHandle handle = getJavaStringZeroCopyCtrHandle(); + if (handle == null) { + return null; + } + try { + MethodType instantiatedMethodType = + MethodType.methodType(handle.type().returnType(), new Class[] {byte[].class, Byte.class}); + CallSite callSite = + LambdaMetafactory.metafactory( + STRING_LOOK_UP, + "apply", + MethodType.methodType(BiFunction.class), + handle.type().generic(), + handle, + instantiatedMethodType); + return (BiFunction) callSite.getTarget().invokeExact(); + } catch (Throwable e) { + return null; + } + } + + private static Function getLatinBytesStringZeroCopyCtr() { + if (!STRING_VALUE_FIELD_IS_BYTES || STRING_LOOK_UP == null) { + return null; + } + try { + Class clazz = Class.forName("java.lang.StringCoding"); + Lookup caller = STRING_LOOK_UP.in(clazz); + MethodHandle handle = + caller.findStatic( + clazz, "newStringLatin1", MethodType.methodType(String.class, byte[].class)); + return makeFunction(caller, handle, Function.class); + } catch (Throwable e) { + return null; + } + } + + private static MethodHandle getJavaStringZeroCopyCtrHandle() { + Preconditions.checkArgument(JdkVersion.MAJOR_VERSION >= 8); + if (STRING_LOOK_UP == null) { + return null; + } + try { + if (STRING_VALUE_FIELD_IS_CHARS) { + return STRING_LOOK_UP.findConstructor( + String.class, MethodType.methodType(void.class, char[].class, boolean.class)); + } else { + return STRING_LOOK_UP.findConstructor( + String.class, MethodType.methodType(void.class, byte[].class, byte.class)); + } + } catch (Exception e) { + return null; + } + } + + // Lazy load offsets and keep the access shape in one class so the JDK25 multi-release + // replacement can change these methods without touching MemoryUtils callers. + private static class ByteArrayStreamFields { + 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 = UNSAFE.objectFieldOffset(ByteArrayOutputStream.class.getDeclaredField("buf")); + BAS_BUF_COUNT = + UNSAFE.objectFieldOffset(ByteArrayOutputStream.class.getDeclaredField("count")); + BIS_BUF_BUF = UNSAFE.objectFieldOffset(ByteArrayInputStream.class.getDeclaredField("buf")); + BIS_BUF_POS = UNSAFE.objectFieldOffset(ByteArrayInputStream.class.getDeclaredField("pos")); + BIS_BUF_COUNT = + UNSAFE.objectFieldOffset(ByteArrayInputStream.class.getDeclaredField("count")); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } + } + } + + public static void wrap(ByteArrayOutputStream stream, MemoryBuffer buffer) { + Preconditions.checkNotNull(stream); + byte[] buf = (byte[]) UNSAFE.getObject(stream, ByteArrayStreamFields.BAS_BUF_BUF); + int count = UNSAFE.getInt(stream, ByteArrayStreamFields.BAS_BUF_COUNT); + buffer.pointTo(buf, 0, buf.length); + buffer.writerIndex(count); + } + + public static void wrap(MemoryBuffer buffer, ByteArrayOutputStream stream) { + Preconditions.checkNotNull(stream); + byte[] bytes = buffer.getHeapMemory(); + Preconditions.checkNotNull(bytes); + UNSAFE.putObject(stream, ByteArrayStreamFields.BAS_BUF_BUF, bytes); + UNSAFE.putInt(stream, ByteArrayStreamFields.BAS_BUF_COUNT, buffer.writerIndex()); + } + + public static void wrap(ByteArrayInputStream stream, MemoryBuffer buffer) { + Preconditions.checkNotNull(stream); + byte[] buf = (byte[]) UNSAFE.getObject(stream, ByteArrayStreamFields.BIS_BUF_BUF); + int count = UNSAFE.getInt(stream, ByteArrayStreamFields.BIS_BUF_COUNT); + int pos = UNSAFE.getInt(stream, ByteArrayStreamFields.BIS_BUF_POS); + buffer.pointTo(buf, 0, count); + buffer.readerIndex(pos); + } + + private static class ObjectStreamClassFields { + private static final long WRITE_OBJECT_METHOD; + private static final long READ_OBJECT_METHOD; + private static final long READ_OBJECT_NO_DATA_METHOD; + private static final long WRITE_REPLACE_METHOD; + private static final long READ_RESOLVE_METHOD; + + static { + try { + WRITE_OBJECT_METHOD = + UNSAFE.objectFieldOffset(ObjectStreamClass.class.getDeclaredField("writeObjectMethod")); + READ_OBJECT_METHOD = + UNSAFE.objectFieldOffset(ObjectStreamClass.class.getDeclaredField("readObjectMethod")); + READ_OBJECT_NO_DATA_METHOD = + UNSAFE.objectFieldOffset( + ObjectStreamClass.class.getDeclaredField("readObjectNoDataMethod")); + WRITE_REPLACE_METHOD = + UNSAFE.objectFieldOffset( + ObjectStreamClass.class.getDeclaredField("writeReplaceMethod")); + READ_RESOLVE_METHOD = + UNSAFE.objectFieldOffset(ObjectStreamClass.class.getDeclaredField("readResolveMethod")); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } + } + } + + public static Method getObjectStreamClassWriteObjectMethod(ObjectStreamClass objectStreamClass) { + return (Method) + UNSAFE.getObject(objectStreamClass, ObjectStreamClassFields.WRITE_OBJECT_METHOD); + } + + public static Method getObjectStreamClassReadObjectMethod(ObjectStreamClass objectStreamClass) { + return (Method) UNSAFE.getObject(objectStreamClass, ObjectStreamClassFields.READ_OBJECT_METHOD); + } + + public static Method getObjectStreamClassReadObjectNoDataMethod( + ObjectStreamClass objectStreamClass) { + return (Method) + UNSAFE.getObject(objectStreamClass, ObjectStreamClassFields.READ_OBJECT_NO_DATA_METHOD); + } + + public static Method getObjectStreamClassWriteReplaceMethod(ObjectStreamClass objectStreamClass) { + return (Method) + UNSAFE.getObject(objectStreamClass, ObjectStreamClassFields.WRITE_REPLACE_METHOD); + } + + public static Method getObjectStreamClassReadResolveMethod(ObjectStreamClass objectStreamClass) { + return (Method) + UNSAFE.getObject(objectStreamClass, ObjectStreamClassFields.READ_RESOLVE_METHOD); + } + + public static T tryMakeFunction( + Lookup lookup, MethodHandle handle, Class functionInterface) { + try { + return makeFunction(lookup, handle, functionInterface); + } catch (Throwable e) { + ExceptionUtils.ignore(e); + throw new IllegalStateException(); + } + } + + private static final MethodType jdkFunctionMethodType = + MethodType.methodType(Object.class, Object.class); + + @SuppressWarnings("unchecked") + public static Function makeJDKFunction(Lookup lookup, MethodHandle handle) { + return makeJDKFunction(lookup, handle, jdkFunctionMethodType); + } + + @SuppressWarnings("unchecked") + public static Function makeJDKFunction( + Lookup lookup, MethodHandle handle, MethodType methodType) { + try { + CallSite callSite = + LambdaMetafactory.metafactory( + lookup, + "apply", + MethodType.methodType(Function.class), + methodType, + handle, + boxedMethodType(handle.type())); + return (Function) callSite.getTarget().invokeExact(); + } catch (Throwable e) { + throw ExceptionUtils.throwException(e); + } + } + + private static final MethodType jdkConsumerMethodType = + MethodType.methodType(void.class, Object.class); + + @SuppressWarnings("unchecked") + public static Consumer makeJDKConsumer(Lookup lookup, MethodHandle handle) { + try { + CallSite callSite = + LambdaMetafactory.metafactory( + lookup, + "accept", + MethodType.methodType(Consumer.class), + jdkConsumerMethodType, + handle, + boxedMethodType(handle.type())); + return (Consumer) callSite.getTarget().invokeExact(); + } catch (Throwable e) { + throw ExceptionUtils.throwException(e); + } + } + + private static final MethodType jdkBiConsumerMethodType = + MethodType.methodType(void.class, Object.class, Object.class); + + @SuppressWarnings("unchecked") + public static BiConsumer makeJDKBiConsumer(Lookup lookup, MethodHandle handle) { + try { + CallSite callSite = + LambdaMetafactory.metafactory( + lookup, + "accept", + MethodType.methodType(BiConsumer.class), + jdkBiConsumerMethodType, + handle, + boxedMethodType(handle.type())); + return (BiConsumer) callSite.getTarget().invokeExact(); + } catch (Throwable e) { + throw ExceptionUtils.throwException(e); + } + } + + private static MethodType boxedMethodType(MethodType methodType) { + Class[] paramTypes = new Class[methodType.parameterCount()]; + for (int i = 0; i < paramTypes.length; i++) { + Class t = methodType.parameterType(i); + if (t.isPrimitive()) { + t = TypeUtils.wrap(t); + } + paramTypes[i] = t; + } + return MethodType.methodType(methodType.returnType(), paramTypes); + } + + @SuppressWarnings("unchecked") + public static T makeFunction(Lookup lookup, MethodHandle handle, Method methodToImpl) { + MethodType instantiatedMethodType = boxedMethodType(handle.type()); + MethodType methodToImplType = + MethodType.methodType(methodToImpl.getReturnType(), methodToImpl.getParameterTypes()); + try { + // Faster than handle.invokeExact. + CallSite callSite = + LambdaMetafactory.metafactory( + lookup, + methodToImpl.getName(), + MethodType.methodType(methodToImpl.getDeclaringClass()), + methodToImplType, + handle, + instantiatedMethodType); + return (T) callSite.getTarget().invokeExact(); + } catch (Throwable e) { + throw ExceptionUtils.throwException(e); + } + } + + public static T makeFunction(Lookup lookup, MethodHandle handle, Class functionInterface) { + String invokedName = "apply"; + try { + Method method = null; + Method[] methods = functionInterface.getMethods(); + for (Method interfaceMethod : methods) { + if (interfaceMethod.getName().equals(invokedName)) { + method = interfaceMethod; + break; + } + } + if (method == null) { + Preconditions.checkArgument(methods.length == 1); + method = methods[0]; + invokedName = method.getName(); + } + MethodType interfaceType = + MethodType.methodType(method.getReturnType(), method.getParameterTypes()); + // Faster than handle.invokeExact. + CallSite callSite = + LambdaMetafactory.metafactory( + lookup, + invokedName, + MethodType.methodType(functionInterface), + interfaceType, + handle, + interfaceType); + // FIXME(chaokunyang) why use invokeExact will fail. + return (T) callSite.getTarget().invoke(); + } catch (Throwable e) { + throw ExceptionUtils.throwException(e); + } + } + + private static final Map, Tuple2, String>> methodMap = new HashMap<>(); + + static { + methodMap.put(boolean.class, Tuple2.of(Predicate.class, "test")); + methodMap.put(byte.class, Tuple2.of(ToByteFunction.class, "applyAsByte")); + methodMap.put(char.class, Tuple2.of(ToCharFunction.class, "applyAsChar")); + methodMap.put(short.class, Tuple2.of(ToShortFunction.class, "applyAsShort")); + methodMap.put(int.class, Tuple2.of(ToIntFunction.class, "applyAsInt")); + methodMap.put(long.class, Tuple2.of(ToLongFunction.class, "applyAsLong")); + methodMap.put(float.class, Tuple2.of(ToFloatFunction.class, "applyAsFloat")); + methodMap.put(double.class, Tuple2.of(ToDoubleFunction.class, "applyAsDouble")); + } + + public static Tuple2, String> getterMethodInfo(Class type) { + Tuple2, String> info = methodMap.get(type); + if (info == null) { + return Tuple2.of(Function.class, "apply"); + } + return info; + } + + public static Object makeGetterFunction( + MethodHandles.Lookup lookup, MethodHandle handle, Class returnType) { + Tuple2, String> methodInfo = methodMap.get(returnType); + MethodType factoryType; + if (methodInfo == null) { + methodInfo = Tuple2.of(Function.class, "apply"); + factoryType = jdkFunctionMethodType; + } else { + factoryType = MethodType.methodType(returnType, Object.class); + } + try { + CallSite callSite = + LambdaMetafactory.metafactory( + lookup, + methodInfo.f1, + MethodType.methodType(methodInfo.f0), + factoryType, + handle, + handle.type()); + // Can't use invokeExact, since we can't specify exact target type for return variable. + return callSite.getTarget().invoke(); + } catch (ClassNotFoundException | NoClassDefFoundError e) { + // ToByteFunction/ToBoolFunction/.. are not defined in jdk, if the classloader of + // fory functions `ToByteFunction/..` isn't parent classloader of classloader for getter + // represented by handle, then exception will be thrown. + return makeGetterFunction(lookup, handle, Object.class); + } catch (Throwable e) { + throw ExceptionUtils.throwException(e); + } + } + + private static volatile Method getModuleMethod; + + public static Object getModule(Class cls) { + Preconditions.checkArgument(JdkVersion.MAJOR_VERSION >= 9); + if (getModuleMethod == null) { + try { + getModuleMethod = Class.class.getDeclaredMethod("getModule"); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + try { + return getModuleMethod.invoke(cls); + } catch (IllegalAccessException | InvocationTargetException e) { + throw ExceptionUtils.throwException(e); + } + } + + // caller sensitive, must use MethodHandle to walk around the check. + private static volatile MethodHandle addReadsHandle; + + public static Object addReads(Object thisModule, Object otherModule) { + Preconditions.checkArgument(JdkVersion.MAJOR_VERSION >= 9); + try { + if (addReadsHandle == null) { + Class cls = Class.forName("java.lang.Module"); + MethodHandles.Lookup lookup = _JDKAccess._trustedLookup(cls); + addReadsHandle = lookup.findVirtual(cls, "addReads", MethodType.methodType(cls, cls)); + } + return addReadsHandle.invoke(thisModule, otherModule); + } catch (Throwable 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 99% 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..2ece8bb9ce 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; 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..5aa6ebee5a 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 @@ -24,6 +24,7 @@ import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.function.Function; @@ -36,7 +37,7 @@ 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.type.TypeUtils; import org.apache.fory.util.Preconditions; import org.apache.fory.util.function.Functions; @@ -45,33 +46,74 @@ 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; +import sun.misc.Unsafe; -/** - * Field accessor for primitive types and object types. - * - *

Note for primitive types, there will be box/unbox overhead. - */ +/** Field accessor for primitive types and object types. */ @SuppressWarnings({"unchecked", "rawtypes"}) public abstract class FieldAccessor { + private static final Unsafe UNSAFE = AndroidSupport.IS_ANDROID ? null : _JDKAccess.UNSAFE; + private static final int REFLECTIVE_ACCESS = 0; + 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(field); + this.accessKind = accessKind(field, fieldOffset); + } + + private static long fieldOffset(Field field) { + if (AndroidSupport.IS_ANDROID) { + return -1; } - this.fieldOffset = fieldOffset; + if (GraalvmSupport.isGraalBuildTime()) { + // Field offsets are rewritten by GraalVM and are not stable during native-image build time. + return -1; + } + return UNSAFE.objectFieldOffset(field); } protected FieldAccessor(Field field, long fieldOffset) { this.field = field; this.fieldOffset = fieldOffset; + this.accessKind = accessKind(field, fieldOffset); + } + + private static int accessKind(Field field, long fieldOffset) { + if (fieldOffset == -1) { + return REFLECTIVE_ACCESS; + } + 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,35 +122,137 @@ public void set(Object obj, Object value) { throw new UnsupportedOperationException("Unsupported for field " + field); } + public final void copy(Object sourceObject, Object 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: + putObject(targetObject, getObject(sourceObject)); + } + } + + public final void copyObject(Object sourceObject, Object targetObject) { + if (accessKind == OBJECT_ACCESS) { + UNSAFE.putObject(targetObject, fieldOffset, UNSAFE.getObject(sourceObject, fieldOffset)); + } else { + putObject(targetObject, getObject(sourceObject)); + } + } + public Field getField() { return field; } + public boolean getBoolean(Object targetObject) { + return (Boolean) get(targetObject); + } + + public void putBoolean(Object targetObject, boolean value) { + set(targetObject, value); + } + + public byte getByte(Object targetObject) { + return (Byte) get(targetObject); + } + + public void putByte(Object targetObject, byte value) { + set(targetObject, value); + } + + public char getChar(Object targetObject) { + return (Character) get(targetObject); + } + + public void putChar(Object targetObject, char value) { + set(targetObject, value); + } + + public short getShort(Object targetObject) { + return (Short) get(targetObject); + } + + public void putShort(Object targetObject, short value) { + set(targetObject, value); + } + + public int getInt(Object targetObject) { + return (Integer) get(targetObject); + } + + public void putInt(Object targetObject, int value) { + set(targetObject, value); + } + + public long getLong(Object targetObject) { + return (Long) get(targetObject); + } + + public void putLong(Object targetObject, long value) { + set(targetObject, value); + } + + public float getFloat(Object targetObject) { + return (Float) get(targetObject); + } + + public void putFloat(Object targetObject, float value) { + set(targetObject, value); + } + + public double getDouble(Object targetObject) { + return (Double) get(targetObject); + } + + public void putDouble(Object targetObject, double value) { + set(targetObject, value); + } + 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. + // For primitive fields, we must use set() which calls the correct UNSAFE.putXxx method. + // UNSAFE.putObject writes object references, not primitive values. if (fieldOffset != -1 && !field.getType().isPrimitive()) { - UnsafeOps.putObject(targetObject, fieldOffset, object); + UNSAFE.putObject(targetObject, fieldOffset, object); } else { set(targetObject, object); } } 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 + // For primitive fields, we must use get() which calls the correct UNSAFE.getXxx method + // and returns the boxed value. UNSAFE.getObject interprets primitive bytes as object // refs. if (fieldOffset != -1 && !field.getType().isPrimitive()) { - return UnsafeOps.getObject(targetObject, fieldOffset); + return UNSAFE.getObject(targetObject, fieldOffset); } else { return get(targetObject); } } - public long getFieldOffset() { - return fieldOffset; - } - void checkObj(Object obj) { if (!this.field.getDeclaringClass().isAssignableFrom(obj.getClass())) { throw new IllegalArgumentException("Illegal class " + obj.getClass()); @@ -135,6 +279,7 @@ public Object getGetter() { } public static FieldAccessor createAccessor(Field field) { + Preconditions.checkArgument(!Modifier.isStatic(field.getModifiers()), field); if (RecordUtils.isRecord(field.getDeclaringClass())) { if (AndroidSupport.IS_ANDROID || GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { return new ReflectiveRecordFieldAccessor(field); @@ -195,6 +340,15 @@ public static FieldAccessor createAccessor(Field field) { } } + public static FieldAccessor createStaticAccessor(Field field) { + Preconditions.checkArgument(Modifier.isStatic(field.getModifiers()), field); + if (AndroidSupport.IS_ANDROID) { + field.setAccessible(true); + return new ReflectiveStaticFieldAccessor(field); + } + return new StaticObjectAccessor(field); + } + static final class ReflectiveRecordFieldAccessor extends FieldGetter { private final Method accessor; @@ -236,14 +390,24 @@ public BooleanAccessor(Field field) { @Override public Object get(Object obj) { + return getBoolean(obj); + } + + @Override + public boolean getBoolean(Object obj) { checkObj(obj); - return UnsafeOps.getBoolean(obj, fieldOffset); + return UNSAFE.getBoolean(obj, fieldOffset); } @Override public void set(Object obj, Object value) { + putBoolean(obj, (Boolean) value); + } + + @Override + public void putBoolean(Object obj, boolean value) { checkObj(obj); - UnsafeOps.putBoolean(obj, fieldOffset, (Boolean) value); + UNSAFE.putBoolean(obj, fieldOffset, value); } } @@ -258,6 +422,11 @@ public BooleanGetter(Field field, Predicate getter) { @Override public Boolean get(Object obj) { + return getBoolean(obj); + } + + @Override + public boolean getBoolean(Object obj) { checkObj(obj); return getter.test(obj); } @@ -272,14 +441,24 @@ public ByteAccessor(Field field) { @Override public Byte get(Object obj) { + return getByte(obj); + } + + @Override + public byte getByte(Object obj) { checkObj(obj); - return UnsafeOps.getByte(obj, fieldOffset); + return UNSAFE.getByte(obj, fieldOffset); } @Override public void set(Object obj, Object value) { + putByte(obj, (Byte) value); + } + + @Override + public void putByte(Object obj, byte value) { checkObj(obj); - UnsafeOps.putByte(obj, fieldOffset, (Byte) value); + UNSAFE.putByte(obj, fieldOffset, value); } } @@ -295,6 +474,11 @@ public ByteGetter(Field field, ToByteFunction getter) { @Override public Byte get(Object obj) { + return getByte(obj); + } + + @Override + public byte getByte(Object obj) { return getter.applyAsByte(obj); } } @@ -308,14 +492,24 @@ public CharAccessor(Field field) { @Override public Character get(Object obj) { + return getChar(obj); + } + + @Override + public char getChar(Object obj) { checkObj(obj); - return UnsafeOps.getChar(obj, fieldOffset); + return UNSAFE.getChar(obj, fieldOffset); } @Override public void set(Object obj, Object value) { + putChar(obj, (Character) value); + } + + @Override + public void putChar(Object obj, char value) { checkObj(obj); - UnsafeOps.putChar(obj, fieldOffset, (Character) value); + UNSAFE.putChar(obj, fieldOffset, value); } } @@ -330,6 +524,11 @@ public CharGetter(Field field, ToCharFunction getter) { @Override public Character get(Object obj) { + return getChar(obj); + } + + @Override + public char getChar(Object obj) { return getter.applyAsChar(obj); } } @@ -343,14 +542,24 @@ public ShortAccessor(Field field) { @Override public Short get(Object obj) { + return getShort(obj); + } + + @Override + public short getShort(Object obj) { checkObj(obj); - return UnsafeOps.getShort(obj, fieldOffset); + return UNSAFE.getShort(obj, fieldOffset); } @Override public void set(Object obj, Object value) { + putShort(obj, (Short) value); + } + + @Override + public void putShort(Object obj, short value) { checkObj(obj); - UnsafeOps.putShort(obj, fieldOffset, (Short) value); + UNSAFE.putShort(obj, fieldOffset, value); } } @@ -365,6 +574,11 @@ public ShortGetter(Field field, ToShortFunction getter) { @Override public Short get(Object obj) { + return getShort(obj); + } + + @Override + public short getShort(Object obj) { return getter.applyAsShort(obj); } } @@ -378,14 +592,24 @@ public IntAccessor(Field field) { @Override public Integer get(Object obj) { + return getInt(obj); + } + + @Override + public int getInt(Object obj) { checkObj(obj); - return UnsafeOps.getInt(obj, fieldOffset); + return UNSAFE.getInt(obj, fieldOffset); } @Override public void set(Object obj, Object value) { + putInt(obj, (Integer) value); + } + + @Override + public void putInt(Object obj, int value) { checkObj(obj); - UnsafeOps.putInt(obj, fieldOffset, (Integer) value); + UNSAFE.putInt(obj, fieldOffset, value); } } @@ -400,6 +624,11 @@ public IntGetter(Field field, ToIntFunction getter) { @Override public Integer get(Object obj) { + return getInt(obj); + } + + @Override + public int getInt(Object obj) { return getter.applyAsInt(obj); } } @@ -413,14 +642,24 @@ public LongAccessor(Field field) { @Override public Long get(Object obj) { + return getLong(obj); + } + + @Override + public long getLong(Object obj) { checkObj(obj); - return UnsafeOps.getLong(obj, fieldOffset); + return UNSAFE.getLong(obj, fieldOffset); } @Override public void set(Object obj, Object value) { + putLong(obj, (Long) value); + } + + @Override + public void putLong(Object obj, long value) { checkObj(obj); - UnsafeOps.putLong(obj, fieldOffset, (Long) value); + UNSAFE.putLong(obj, fieldOffset, value); } } @@ -435,6 +674,11 @@ public LongGetter(Field field, ToLongFunction getter) { @Override public Long get(Object obj) { + return getLong(obj); + } + + @Override + public long getLong(Object obj) { return getter.applyAsLong(obj); } } @@ -448,14 +692,24 @@ public FloatAccessor(Field field) { @Override public Object get(Object obj) { + return getFloat(obj); + } + + @Override + public float getFloat(Object obj) { checkObj(obj); - return UnsafeOps.getFloat(obj, fieldOffset); + return UNSAFE.getFloat(obj, fieldOffset); } @Override public void set(Object obj, Object value) { + putFloat(obj, (Float) value); + } + + @Override + public void putFloat(Object obj, float value) { checkObj(obj); - UnsafeOps.putFloat(obj, fieldOffset, (Float) value); + UNSAFE.putFloat(obj, fieldOffset, value); } } @@ -470,6 +724,11 @@ public FloatGetter(Field field, ToFloatFunction getter) { @Override public Float get(Object obj) { + return getFloat(obj); + } + + @Override + public float getFloat(Object obj) { return getter.applyAsFloat(obj); } } @@ -483,14 +742,24 @@ public DoubleAccessor(Field field) { @Override public Object get(Object obj) { + return getDouble(obj); + } + + @Override + public double getDouble(Object obj) { checkObj(obj); - return UnsafeOps.getDouble(obj, fieldOffset); + return UNSAFE.getDouble(obj, fieldOffset); } @Override public void set(Object obj, Object value) { + putDouble(obj, (Double) value); + } + + @Override + public void putDouble(Object obj, double value) { checkObj(obj); - UnsafeOps.putDouble(obj, fieldOffset, (Double) value); + UNSAFE.putDouble(obj, fieldOffset, value); } } @@ -505,6 +774,11 @@ public DoubleGetter(Field field, ToDoubleFunction getter) { @Override public Double get(Object obj) { + return getDouble(obj); + } + + @Override + public double getDouble(Object obj) { return getter.applyAsDouble(obj); } } @@ -519,13 +793,13 @@ public ObjectAccessor(Field field) { @Override public Object get(Object obj) { checkObj(obj); - return UnsafeOps.getObject(obj, fieldOffset); + return UNSAFE.getObject(obj, fieldOffset); } @Override public void set(Object obj, Object value) { checkObj(obj); - UnsafeOps.putObject(obj, fieldOffset, value); + UNSAFE.putObject(obj, fieldOffset, value); } } @@ -544,6 +818,52 @@ public Object get(Object obj) { } } + static final class ReflectiveStaticFieldAccessor extends FieldAccessor { + ReflectiveStaticFieldAccessor(Field field) { + super(field, -1); + } + + @Override + public Object get(Object obj) { + try { + return field.get(null); + } catch (IllegalAccessException | IllegalArgumentException e) { + throw new ForyException("Failed to read static field reflectively: " + field, e); + } + } + + @Override + public void set(Object obj, Object value) { + try { + field.set(null, value); + } catch (IllegalAccessException | IllegalArgumentException e) { + throw new ForyException("Failed to write static field reflectively: " + field, e); + } + } + } + + static final class StaticObjectAccessor extends FieldAccessor { + private final Object base; + private final long offset; + + StaticObjectAccessor(Field field) { + super(field, -1); + Preconditions.checkArgument(!TypeUtils.isPrimitive(field.getType())); + base = UNSAFE.staticFieldBase(field); + offset = UNSAFE.staticFieldOffset(field); + } + + @Override + public Object get(Object obj) { + return UNSAFE.getObject(base, offset); + } + + @Override + public void set(Object obj, Object value) { + UNSAFE.putObject(base, offset, value); + } + } + static final class GeneratedAccessor extends FieldAccessor { private static final ClassValueCache>> cache = ClassValueCache.newClassKeyCache(8); @@ -594,5 +914,149 @@ public void set(Object obj, Object value) { 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/ObjectCreator.java b/java/fory-core/src/main/java/org/apache/fory/reflect/ObjectCreator.java index cc6d0c7d70..1134c21896 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/ObjectCreator.java @@ -31,12 +31,16 @@ * *

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. + * (MethodHandle, Constructor, and supported constructor-bypassing allocation) are all thread-safe. * * @param the type of objects this creator can instantiate */ @ThreadSafe public abstract class ObjectCreator { + private static final String[] NO_FIELDS = new String[0]; + private static final Class[] NO_TYPES = new Class[0]; + private static final boolean[] NO_FINAL_FIELDS = new boolean[0]; + protected final Class type; protected ObjectCreator(Class type) { @@ -52,6 +56,34 @@ protected ObjectCreator(Class type) { */ public abstract T newInstance(); + public boolean hasConstructorFields() { + return false; + } + + public String[] getConstructorFieldNames() { + return NO_FIELDS; + } + + public Class[] getConstructorFieldDeclaringClasses() { + return null; + } + + public Class[] getConstructorFieldTypes() { + return NO_TYPES; + } + + public boolean[] getConstructorFieldFinal() { + return NO_FINAL_FIELDS; + } + + public boolean isConstructorPublic() { + return false; + } + + public boolean isOnlyPublicConstructor() { + return false; + } + /** * Creates a new instance of type T using the provided arguments. * 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 index 00fc668539..d365c4e578 100644 --- 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 @@ -19,19 +19,32 @@ package org.apache.fory.reflect; +import java.lang.annotation.Annotation; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles.Lookup; import java.lang.invoke.MethodType; import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.fory.annotation.ForyField; 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.platform.internal._JDKAccess; +import org.apache.fory.type.Descriptor; +import org.apache.fory.type.TypeUtils; import org.apache.fory.util.record.RecordUtils; -import org.apache.fory.util.unsafe._JDKAccess; +import sun.misc.Unsafe; /** * Factory class for creating and caching {@link ObjectCreator} instances. @@ -45,8 +58,8 @@ * parameterized constructor invocation *

  • Classes with no-arg constructors: Uses {@link * DeclaredNoArgCtrObjectCreator} with MethodHandle for fast invocation - *
  • Classes without accessible constructors: Uses {@link UnsafeObjectCreator} - * with platform-specific unsafe allocation + *
  • Classes without accessible constructors: Uses a private + * constructor-bypassing creator on runtimes where that is still supported *
  • GraalVM native image compatibility: Uses {@link * ParentNoArgCtrObjectCreator} for constructor generate-based creation when needed *
  • Android compatibility: Uses reflection for records and no-arg @@ -61,6 +74,7 @@ */ @SuppressWarnings("unchecked") public class ObjectCreators { + private static final Unsafe UNSAFE = AndroidSupport.IS_ANDROID ? null : _JDKAccess.UNSAFE; private static final ClassValueCache> cache = ClassValueCache.newClassKeySoftCache(8); @@ -81,6 +95,18 @@ public static ObjectCreator getObjectCreator(Class type) { return (ObjectCreator) cache.get(type, () -> creategetObjectCreator(type)); } + private static T allocateInstance(Class type) { + if (UNSAFE == null || JdkVersion.MAJOR_VERSION >= 25) { + throw new ForyException( + "Constructor-bypassing allocation is unsupported in this runtime for " + type); + } + try { + return (T) UNSAFE.allocateInstance(type); + } catch (InstantiationException e) { + throw new ForyException("Failed to allocate instance for " + type, e); + } + } + private static ObjectCreator creategetObjectCreator(Class type) { if (RecordUtils.isRecord(type)) { return new RecordObjectCreator<>(type); @@ -90,7 +116,11 @@ private static ObjectCreator creategetObjectCreator(Class type) { if (noArgConstructor != null) { return new ReflectiveNoArgCtrObjectCreator<>(type, noArgConstructor); } - return new UnsupportedObjectCreator<>(type); + return new UnsupportedObjectCreator<>( + type, "Android cannot create " + type + " without an accessible no-arg constructor"); + } + if (JdkVersion.MAJOR_VERSION >= 25 && noArgConstructor == null) { + return new ConstructorObjectCreator<>(type); } if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { if (noArgConstructor != null) { @@ -105,6 +135,278 @@ private static ObjectCreator creategetObjectCreator(Class type) { return new DeclaredNoArgCtrObjectCreator<>(type); } + public static boolean supportsJdk25Creation(Class type) { + if (JdkVersion.MAJOR_VERSION < 25 || RecordUtils.isRecord(type)) { + return true; + } + try { + ObjectCreator creator = creategetObjectCreator(type); + return !(creator instanceof UnsupportedObjectCreator); + } catch (RuntimeException e) { + return false; + } + } + + private static final class ConstructorMatch { + private final Constructor constructor; + private final String[] fieldNames; + private final Class[] declaringClasses; + private final Class[] fieldTypes; + private final boolean[] finalFields; + private final int score; + + private ConstructorMatch( + Constructor constructor, + String[] fieldNames, + Class[] declaringClasses, + Class[] fieldTypes, + boolean[] finalFields, + int score) { + this.constructor = constructor; + this.fieldNames = fieldNames; + this.declaringClasses = declaringClasses; + this.fieldTypes = fieldTypes; + this.finalFields = finalFields; + this.score = score; + } + } + + private static ConstructorMatch findConstructor(Class type) { + List fields = new ArrayList<>(); + fields.addAll(Descriptor.getFields(type)); + Map fieldsByName = new LinkedHashMap<>(); + Map> fieldsByNameList = new LinkedHashMap<>(); + Map fieldsById = new LinkedHashMap<>(); + Set duplicateNames = new LinkedHashSet<>(); + Set duplicateIds = new LinkedHashSet<>(); + for (Field field : fields) { + fieldsByNameList.computeIfAbsent(field.getName(), name -> new ArrayList<>()).add(field); + Field previous = fieldsByName.put(field.getName(), field); + if (previous != null) { + duplicateNames.add(field.getName()); + } + ForyField foryField = field.getAnnotation(ForyField.class); + if (foryField != null && foryField.id() >= 0) { + previous = fieldsById.put(foryField.id(), field); + if (previous != null) { + duplicateIds.add(foryField.id()); + } + } + } + ConstructorMatch best = null; + for (Constructor constructor : type.getDeclaredConstructors()) { + if (constructor.isSynthetic()) { + continue; + } + ConstructorMatch match = + matchConstructor( + type, + (Constructor) constructor, + fieldsByName, + fieldsByNameList, + fieldsById, + duplicateNames, + duplicateIds); + if (match != null && (best == null || match.score > best.score)) { + best = match; + } + } + if (best == null) { + throw new ForyException( + "JDK25 zero-Unsafe mode requires " + + "a bindable constructor because no no-arg constructor is available" + + " for " + + type + + ". Annotate the constructor with java.beans.ConstructorProperties or compile " + + "the class with -parameters."); + } + return best; + } + + private static ConstructorMatch matchConstructor( + Class type, + Constructor constructor, + Map fieldsByName, + Map> fieldsByNameList, + Map fieldsById, + Set duplicateNames, + Set duplicateIds) { + Field[] fields = + constructorFields( + constructor, fieldsByName, fieldsByNameList, fieldsById, duplicateNames, duplicateIds); + if (fields == null) { + return null; + } + return matchConstructorFields(constructor, fields); + } + + private static ConstructorMatch matchConstructorFields( + Constructor constructor, Field[] fields) { + Class[] parameterTypes = constructor.getParameterTypes(); + String[] names = new String[fields.length]; + Class[] declaringClasses = new Class[fields.length]; + Class[] fieldTypes = new Class[fields.length]; + boolean[] finalFieldFlags = new boolean[fields.length]; + for (int i = 0; i < fields.length; i++) { + Field field = fields[i]; + if (!constructorTypeMatches(parameterTypes[i], field)) { + return null; + } + names[i] = field.getName(); + declaringClasses[i] = field.getDeclaringClass(); + fieldTypes[i] = field.getType(); + finalFieldFlags[i] = Modifier.isFinal(field.getModifiers()); + } + return new ConstructorMatch<>( + constructor, names, declaringClasses, fieldTypes, finalFieldFlags, 300 - fields.length); + } + + private static Field[] constructorFields( + Constructor constructor, + Map fieldsByName, + Map> fieldsByNameList, + Map fieldsById, + Set duplicateNames, + Set duplicateIds) { + Field[] fields = fieldsByForyFieldId(constructor, fieldsById, duplicateIds); + if (fields != null) { + return fields; + } + String[] names = constructorFieldNames(constructor); + if (names != null) { + if (names.length != constructor.getParameterCount()) { + return null; + } + return fieldsByName(constructor, fieldsByName, fieldsByNameList, duplicateNames, names); + } + return null; + } + + private static Field[] fieldsByForyFieldId( + Constructor constructor, Map fieldsById, Set duplicateIds) { + Parameter[] parameters = constructor.getParameters(); + Field[] fields = new Field[parameters.length]; + boolean hasForyFieldId = false; + for (int i = 0; i < parameters.length; i++) { + ForyField foryField = parameters[i].getAnnotation(ForyField.class); + if (foryField == null || foryField.id() < 0) { + continue; + } + hasForyFieldId = true; + int id = foryField.id(); + if (duplicateIds.contains(id)) { + throw new ForyException("Constructor parameter id " + id + " is ambiguous"); + } + fields[i] = fieldsById.get(id); + if (fields[i] == null) { + return null; + } + } + if (!hasForyFieldId) { + return null; + } + for (Field field : fields) { + if (field == null) { + return null; + } + } + return fields; + } + + private static Field[] fieldsByName( + Constructor constructor, + Map fieldsByName, + Map> fieldsByNameList, + Set duplicateNames, + String[] names) { + Field[] fields = new Field[names.length]; + for (int i = 0; i < names.length; i++) { + String name = names[i]; + if (duplicateNames.contains(name)) { + Field field = syntheticField(constructor, fieldsByNameList.get(name)); + if (field == null) { + throw new ForyException( + "Constructor parameter " + + name + + " is ambiguous because " + + constructor.getDeclaringClass() + + " has duplicate field names"); + } + fields[i] = field; + continue; + } + Field field = fieldsByName.get(name); + if (field == null) { + return null; + } + fields[i] = field; + } + return fields; + } + + private static Field syntheticField(Constructor constructor, List fields) { + if (fields == null) { + return null; + } + Class declaringClass = constructor.getDeclaringClass(); + for (Field field : fields) { + if (field.isSynthetic() && field.getDeclaringClass() == declaringClass) { + return field; + } + } + return null; + } + + private static boolean constructorTypeMatches(Class parameterType, Field field) { + Class boxedParameterType = TypeUtils.boxedType(parameterType); + Class boxedFieldType = TypeUtils.boxedType(field.getType()); + return boxedParameterType.isAssignableFrom(boxedFieldType); + } + + private static String[] constructorFieldNames(Constructor constructor) { + String[] names = constructorProperties(constructor); + if (names != null) { + return names; + } + Parameter[] parameters = constructor.getParameters(); + for (Parameter parameter : parameters) { + if (!parameter.isNamePresent()) { + return null; + } + } + names = new String[parameters.length]; + for (int i = 0; i < parameters.length; i++) { + names[i] = parameters[i].getName(); + } + return names; + } + + private static String[] constructorProperties(Constructor constructor) { + for (Annotation annotation : constructor.getDeclaredAnnotations()) { + if ("java.beans.ConstructorProperties".equals(annotation.annotationType().getName())) { + try { + return (String[]) annotation.annotationType().getMethod("value").invoke(annotation); + } catch (ReflectiveOperationException e) { + throw new ForyException("Failed to read ConstructorProperties for " + constructor, e); + } + } + } + return null; + } + + private static MethodHandle constructorHandle(Class type, Constructor constructor) { + Lookup lookup = _JDKAccess._trustedLookup(type); + if (lookup == null) { + return null; + } + try { + return lookup.findConstructor( + type, MethodType.methodType(void.class, constructor.getParameterTypes())); + } catch (NoSuchMethodException | IllegalAccessException e) { + return null; + } + } + private static final class ReflectiveNoArgCtrObjectCreator extends ObjectCreator { private final Constructor constructor; @@ -134,24 +436,113 @@ public T newInstanceWithArguments(Object... arguments) { } private static final class UnsupportedObjectCreator extends ObjectCreator { - private UnsupportedObjectCreator(Class type) { + private final String message; + + private UnsupportedObjectCreator(Class type, String message) { super(type); + this.message = message; } @Override public T newInstance() { - throw new ForyException( - "Android cannot create " + type + " without an accessible no-arg constructor"); + throw new ForyException(message); } @Override public T newInstanceWithArguments(Object... arguments) { + throw new ForyException(message); + } + } + + public static final class ConstructorObjectCreator extends ObjectCreator { + private final Constructor constructor; + private final MethodHandle handle; + private final String[] fieldNames; + private final Class[] declaringClasses; + private final Class[] fieldTypes; + private final boolean[] finalFields; + + private ConstructorObjectCreator(Class type) { + super(type); + ConstructorMatch match = findConstructor(type); + constructor = match.constructor; + handle = constructorHandle(type, constructor); + fieldNames = match.fieldNames; + declaringClasses = match.declaringClasses; + fieldTypes = match.fieldTypes; + finalFields = match.finalFields; + try { + constructor.setAccessible(true); + } catch (RuntimeException e) { + throw new ForyException("Failed to make constructor accessible for " + type, e); + } + } + + @Override + public boolean hasConstructorFields() { + return true; + } + + @Override + public String[] getConstructorFieldNames() { + return fieldNames; + } + + @Override + public Class[] getConstructorFieldDeclaringClasses() { + return declaringClasses; + } + + @Override + public Class[] getConstructorFieldTypes() { + return fieldTypes; + } + + @Override + public boolean[] getConstructorFieldFinal() { + return finalFields; + } + + @Override + public boolean isConstructorPublic() { + return Modifier.isPublic(type.getModifiers()) + && Modifier.isPublic(constructor.getModifiers()); + } + + @Override + public boolean isOnlyPublicConstructor() { + if (!isConstructorPublic()) { + return false; + } + for (Constructor declaredConstructor : type.getDeclaredConstructors()) { + if (Modifier.isPublic(declaredConstructor.getModifiers()) + && declaredConstructor != constructor) { + return false; + } + } + return true; + } + + @Override + public T newInstance() { throw new ForyException( - "Android cannot create " + type + " without a supported constructor path"); + "JDK25 zero-Unsafe mode requires constructor field values to create " + type); + } + + @Override + public T newInstanceWithArguments(Object... arguments) { + try { + if (handle == null) { + return constructor.newInstance(arguments); + } + return (T) handle.invokeWithArguments(arguments); + } catch (Throwable e) { + throw new ForyException("Failed to create instance using constructor: " + type, e); + } } } - public static final class UnsafeObjectCreator extends ObjectCreator { + private static final class UnsafeObjectCreator extends ObjectCreator { public UnsafeObjectCreator(Class type) { super(type); @@ -159,7 +550,7 @@ public UnsafeObjectCreator(Class type) { @Override public T newInstance() { - return UnsafeOps.newInstance(type); + return ObjectCreators.allocateInstance(type); } @Override 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/resolver/ClassResolver.java b/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java index 49c4a6143f..bb2cb1a4cd 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 @@ -31,8 +31,10 @@ import java.io.IOException; import java.io.Serializable; import java.lang.invoke.SerializedLambda; +import java.lang.reflect.Modifier; import java.math.BigDecimal; import java.math.BigInteger; +import java.net.URL; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.time.Duration; @@ -57,6 +59,7 @@ import java.util.Objects; import java.util.Optional; import java.util.OptionalInt; +import java.util.StringTokenizer; import java.util.TimeZone; import java.util.TreeMap; import java.util.TreeSet; @@ -100,6 +103,7 @@ import org.apache.fory.logging.LoggerFactory; import org.apache.fory.memory.ByteBufferUtil; import org.apache.fory.memory.MemoryBuffer; +import org.apache.fory.memory.MemoryUtils; import org.apache.fory.meta.ClassSpec; import org.apache.fory.meta.EncodedMetaString; import org.apache.fory.meta.Encoders; @@ -108,11 +112,13 @@ import org.apache.fory.meta.TypeExtMeta; import org.apache.fory.platform.AndroidSupport; import org.apache.fory.platform.GraalvmSupport; +import org.apache.fory.platform.JdkVersion; 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; @@ -134,6 +140,7 @@ import org.apache.fory.serializer.Shareable; import org.apache.fory.serializer.SqlTimeSerializers; import org.apache.fory.serializer.TimeSerializers; +import org.apache.fory.serializer.URLSerializer; import org.apache.fory.serializer.UnknownClass; import org.apache.fory.serializer.UnknownClass.UnknownEmptyStruct; import org.apache.fory.serializer.UnknownClass.UnknownStruct; @@ -1467,6 +1474,15 @@ public Class getSerializerClass(Class cls, boolean code return TimeSerializers.ZoneIdSerializer.class; } else if (TimeZone.class.isAssignableFrom(cls)) { return TimeSerializers.TimeZoneSerializer.class; + } else if (cls == URL.class) { + return URLSerializer.class; + } else if (cls == StringTokenizer.class) { + if (!MemoryUtils.JDK_COLLECTION_FIELD_ACCESS) { + throw new UnsupportedOperationException( + "StringTokenizer serialization requires JDK internal field access. On JDK25+, open " + + "java.base/java.util to org.apache.fory.core,org.apache.fory.format."); + } + return Serializers.StringTokenizerSerializer.class; } else if (ByteBuffer.class.isAssignableFrom(cls)) { return BufferSerializers.ByteBufferSerializer.class; } @@ -1526,6 +1542,9 @@ public Class getSerializerClass(Class cls, boolean code return MapSerializer.class; } } + if (requiresJdkStream(cls)) { + return getDefaultJDKStreamSerializerType(); + } if (isCrossLanguage()) { LOG.warn("Class {} isn't supported for cross-language serialization.", cls); } @@ -1564,6 +1583,9 @@ public Object id() { public Class getObjectSerializerClass( Class cls, JITContext.SerializerJITCallback> callback) { boolean codegen = config.isCodeGenEnabled() && supportCodegenForJavaSerialization(cls); + if (JdkVersion.MAJOR_VERSION >= 25 && !Modifier.isPublic(cls.getModifiers())) { + codegen = false; + } return getObjectSerializerClass(cls, false, codegen, callback); } @@ -1579,6 +1601,9 @@ public Class getObjectSerializerClass( return serializerClass; } } + if (ReflectionUtils.isJdkProxy(cls)) { + return JdkProxySerializer.class; + } Class staticSerializerClass = getStaticGeneratedStructSerializerClass(cls); if (staticSerializerClass != null && shouldPreferStaticGeneratedSerializer(cls)) { @@ -1610,6 +1635,22 @@ public Class getObjectSerializerClass( } } + private static boolean requiresJdkStream(Class cls) { + return JdkVersion.MAJOR_VERSION >= 25 + && cls.getName().startsWith("java.") + && Serializable.class.isAssignableFrom(cls) + && !hasNoArgConstructor(cls); + } + + private static boolean hasNoArgConstructor(Class cls) { + try { + cls.getDeclaredConstructor(); + return true; + } catch (NoSuchMethodException e) { + return false; + } + } + public Class getJavaSerializer(Class clz) { if (Collection.class.isAssignableFrom(clz)) { return CollectionSerializers.JDKCompatibleCollectionSerializer.class; @@ -1860,7 +1901,21 @@ private void registerGraalvmSerializerClass(Class cls) { RecordUtils.getRecordConstructor(cls); RecordUtils.getRecordComponents(cls); } - ObjectCreators.getObjectCreator(cls); + if (needsGraalvmObjectCreator(cls, serializerClass)) { + ObjectCreators.getObjectCreator(cls); + } + } + + private boolean needsGraalvmObjectCreator( + 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/serializer/AbstractObjectSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/AbstractObjectSerializer.java index 4ac9e2ab5f..8b8dc6376b 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 @@ -32,10 +32,10 @@ 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; @@ -63,6 +63,7 @@ public abstract class AbstractObjectSerializer extends Serializer { private static final Logger LOG = LoggerFactory.getLogger(AbstractObjectSerializer.class); + private static final Object SELF_REFERENCE = new Object(); protected final Config config; protected final TypeResolver typeResolver; protected final boolean isRecord; @@ -243,75 +244,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 +255,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; @@ -770,134 +695,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); @@ -1015,6 +865,9 @@ public T copy(CopyContext copyContext, T originObj) { if (isRecord) { return copyRecord(copyContext, originObj); } + if (objectCreator.hasConstructorFields()) { + return copyConstructorObject(copyContext, originObj); + } T newObj = newBean(); copyContext.reference(originObj, newObj); copyFields(copyContext, originObj, newObj); @@ -1022,7 +875,8 @@ 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); Arrays.fill(copyRecordInfo.getRecordComponents(), null); @@ -1034,7 +888,32 @@ private T copyRecord(CopyContext copyContext, T originObj) { return originObj; } - private Object[] copyFields(CopyContext copyContext, T originObj) { + private T copyConstructorObject(CopyContext copyContext, T originObj) { + SerializationFieldInfo[] fieldInfos = this.fieldInfos; + if (fieldInfos == null) { + fieldInfos = buildFieldsInfo(); + } + int[] constructorFieldIndexes = buildConstructorFieldIndexes(fieldInfos); + boolean[] constructorFieldMask = + buildConstructorFieldMask(fieldInfos.length, constructorFieldIndexes); + copyContext.markCopying(originObj); + try { + Object[] fieldValues = + copyFieldValues(copyContext, originObj, fieldInfos, constructorFieldMask, true); + T newObj = + objectCreator.newInstanceWithArguments( + constructorArgs( + fieldValues, constructorFieldIndexes, objectCreator.getConstructorFieldTypes())); + copyContext.reference(originObj, newObj); + copyFields(copyContext, fieldInfos, originObj, newObj, constructorFieldMask, false); + return newObj; + } catch (Throwable e) { + copyContext.cancelCopy(originObj); + throw e; + } + } + + private Object[] copyFieldValues(CopyContext copyContext, T originObj) { SerializationFieldInfo[] fieldInfos = this.fieldInfos; if (fieldInfos == null) { fieldInfos = buildFieldsInfo(); @@ -1043,21 +922,37 @@ 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 { + fieldValues[i] = + copyNotPrimitiveField(copyContext, originObj, fieldAccessor, fieldInfo.dispatchId); + } + } + return fieldValues; + } + + private Object[] copyFieldValues( + CopyContext copyContext, + T originObj, + SerializationFieldInfo[] fieldInfos, + boolean[] constructorFieldMask, + boolean constructorFields) { + Object[] fieldValues = new Object[fieldInfos.length]; + for (int i = 0; i < fieldInfos.length; i++) { + if (constructorFieldMask[i] != constructorFields) { + continue; + } + SerializationFieldInfo fieldInfo = fieldInfos[i]; + FieldAccessor fieldAccessor = fieldInfo.fieldAccessor; + 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 +970,33 @@ 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)); + if (fieldInfo.isPrimitiveField) { + copySetPrimitiveField(originObj, newObj, fieldAccessor, fieldInfo.dispatchId); + } else { + copySetNotPrimitiveField( + copyContext, originObj, newObj, fieldAccessor, fieldInfo.dispatchId); + } + } + } + + private static void copyFields( + CopyContext copyContext, + SerializationFieldInfo[] fieldInfos, + Object originObj, + Object newObj, + boolean[] constructorFieldMask, + boolean constructorFields) { + for (int i = 0; i < fieldInfos.length; i++) { + if (constructorFieldMask[i] != constructorFields) { continue; } + SerializationFieldInfo fieldInfo = fieldInfos[i]; + FieldAccessor fieldAccessor = fieldInfo.fieldAccessor; 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 +1005,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 +1037,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() { @@ -1328,4 +1149,224 @@ private SerializationFieldInfo[] buildFieldsInfo() { protected T newBean() { return objectCreator.newInstance(); } + + protected final int[] buildConstructorFieldIndexes(SerializationFieldInfo[] fieldInfos) { + return buildConstructorFieldIndexes(fieldInfos, true); + } + + protected final int[] buildConstructorFieldIndexes( + SerializationFieldInfo[] fieldInfos, boolean allowMissingNonFinal) { + return buildConstructorFieldIndexes(fieldInfos, allowMissingNonFinal, null); + } + + protected final int[] buildConstructorFieldIndexes( + SerializationFieldInfo[] fieldInfos, boolean allowMissingNonFinal, String[] defaultFields) { + return buildConstructorFieldIndexes(fieldInfos, allowMissingNonFinal, defaultFields, null); + } + + protected final int[] buildConstructorFieldIndexes( + SerializationFieldInfo[] fieldInfos, + boolean allowMissingNonFinal, + String[] defaultFields, + Class[] defaultDeclaringClasses) { + String[] fieldNames = objectCreator.getConstructorFieldNames(); + if (fieldNames.length == 0) { + return null; + } + Class[] declaringClasses = objectCreator.getConstructorFieldDeclaringClasses(); + boolean[] finalFields = objectCreator.getConstructorFieldFinal(); + int[] indexes = new int[fieldNames.length]; + for (int i = 0; i < fieldNames.length; i++) { + Class declaringClass = declaringClasses == null ? null : declaringClasses[i]; + boolean allowMissing = + (allowMissingNonFinal && !finalFields[i]) + || contains(defaultFields, defaultDeclaringClasses, fieldNames[i], declaringClass); + indexes[i] = constructorFieldIndex(fieldInfos, declaringClass, fieldNames[i], allowMissing); + } + return indexes; + } + + private static boolean contains( + String[] values, Class[] declaringClasses, String value, Class declaringClass) { + if (values == null) { + return false; + } + for (int i = 0; i < values.length; i++) { + if (values[i].equals(value) + && (declaringClasses == null + || i >= declaringClasses.length + || declaringClasses[i] == null + || declaringClasses[i] == declaringClass)) { + return true; + } + } + return false; + } + + protected final boolean[] buildConstructorFieldMask(int size, int[] indexes) { + if (indexes == null) { + return null; + } + boolean[] mask = new boolean[size]; + for (int index : indexes) { + if (index >= 0) { + mask[index] = true; + } + } + return mask; + } + + protected final Object[] constructorArgs(Object[] fieldValues, int[] indexes) { + Object[] args = new Object[indexes.length]; + for (int i = 0; i < indexes.length; i++) { + args[i] = fieldValues[indexes[i]]; + } + return args; + } + + protected final Object[] constructorArgs( + Object[] fieldValues, int[] indexes, Class[] fieldTypes) { + Object[] args = new Object[indexes.length]; + for (int i = 0; i < indexes.length; i++) { + int index = indexes[i]; + args[i] = index < 0 ? defaultConstructorValue(fieldTypes[i]) : fieldValues[index]; + } + return args; + } + + protected final void checkNoUnresolvedReadRef(ReadContext readContext) { + checkNoUnresolvedReadRef(readContext, type); + } + + public static void checkNoUnresolvedReadRef(ReadContext readContext, Class type) { + if (consumeSelfRef(readContext)) { + throwConstructorCycle(type); + } + } + + public static void beginConstructorRef(ReadContext readContext) { + if (readContext.hasPreservedRefId()) { + readContext.trackUnresolvedRef(readContext.lastPreservedRefId()); + } + } + + public static void endConstructorRef(ReadContext readContext) { + readContext.untrackUnresolvedRef(); + } + + public static void referenceConstructorRef(ReadContext readContext, Object object) { + if (readContext.hasTrackedRef()) { + // Constructor-bound objects are registered after some fields may already have reserved and + // resolved their own ids, so bind the tracked id instead of popping the current stack top. + readContext.reference(readContext.currentTrackedRefId(), object); + } else 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) { + if (readContext.hasTrackedRef()) { + return readContext.consumeUnresolvedRef(readContext.currentTrackedRefId()); + } + return readContext.hasPreservedRefId() + && readContext.consumeUnresolvedRef(readContext.lastPreservedRefId()); + } + + private static void throwConstructorCycle(Class type) { + throw new ForyException( + "Cyclic references to constructor-bound type " + + type.getName() + + " cannot be restored before the object is constructed. Use a no-arg constructor " + + "or keep the cycle as a direct non-constructor field."); + } + + public static Object defaultConstructorValue(Class type) { + if (type == boolean.class) { + return false; + } else if (type == byte.class) { + return (byte) 0; + } else if (type == short.class) { + return (short) 0; + } else if (type == char.class) { + return (char) 0; + } else if (type == int.class) { + return 0; + } else if (type == long.class) { + return 0L; + } else if (type == float.class) { + return 0.0f; + } else if (type == double.class) { + return 0.0d; + } + return null; + } + + protected final void setNonConstructorFields( + Object targetObject, + Object[] fieldValues, + SerializationFieldInfo[] fieldInfos, + boolean[] constructorMask) { + for (int i = 0; i < fieldInfos.length; i++) { + if (!constructorMask[i] && fieldInfos[i].fieldAccessor != null) { + fieldInfos[i].fieldAccessor.putObject(targetObject, fieldValues[i]); + } + } + } + + public static Object fieldValue(Object[] fieldValues, int index) { + return fieldValues[index]; + } + + private static int constructorFieldIndex( + SerializationFieldInfo[] fieldInfos, + Class declaringClass, + String fieldName, + boolean allowMissing) { + int index = -1; + for (int i = 0; i < fieldInfos.length; i++) { + FieldAccessor fieldAccessor = fieldInfos[i].fieldAccessor; + if (fieldAccessor == null) { + continue; + } + Field field = fieldAccessor.getField(); + if (!field.getName().equals(fieldName) + || (declaringClass != null && field.getDeclaringClass() != declaringClass)) { + continue; + } + if (index >= 0) { + throw new ForyException( + "Constructor field " + fieldName + " is ambiguous because multiple fields match"); + } + index = i; + } + if (index < 0) { + if (allowMissing) { + return -1; + } + throw new ForyException("Constructor field " + fieldName + " is not serialized"); + } + return index; + } } 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..611dfc0da4 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 @@ -29,6 +29,7 @@ import org.apache.fory.memory.MemoryBuffer; import org.apache.fory.meta.TypeDef; import org.apache.fory.reflect.FieldAccessor; +import org.apache.fory.reflect.ObjectCreator; import org.apache.fory.resolver.TypeResolver; import org.apache.fory.serializer.FieldGroups.SerializationFieldInfo; import org.apache.fory.type.DescriptorGrouper; @@ -41,12 +42,34 @@ */ @SuppressWarnings({"unchecked", "rawtypes"}) public abstract class CompatibleLayerSerializerBase extends AbstractObjectSerializer { + private static final ObjectCreator FIELD_ONLY_CREATOR = new FieldOnlyCreator(); + + private static final class FieldOnlyCreator extends ObjectCreator { + private FieldOnlyCreator() { + super(Object.class); + } + + @Override + public Object newInstance() { + throw new UnsupportedOperationException("Layer serializers do not create objects"); + } + + @Override + public Object newInstanceWithArguments(Object... arguments) { + throw new UnsupportedOperationException("Layer serializers do not create objects"); + } + } + protected TypeDef layerTypeDef; protected Class layerMarkerClass; protected SerializationFieldInfo[] allFields = new SerializationFieldInfo[0]; public CompatibleLayerSerializerBase(TypeResolver typeResolver, Class type) { - super(typeResolver, type); + super(typeResolver, type, fieldOnlyCreator()); + } + + private static ObjectCreator fieldOnlyCreator() { + return (ObjectCreator) FIELD_ONLY_CREATOR; } public final void setLayerSerializerMeta(TypeDef layerTypeDef, Class layerMarkerClass) { 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..01058fbd81 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 @@ -33,8 +33,9 @@ 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.platform.JdkVersion; import org.apache.fory.reflect.FieldAccessor; +import org.apache.fory.reflect.ObjectCreators; import org.apache.fory.resolver.ClassResolver; import org.apache.fory.resolver.RefMode; import org.apache.fory.resolver.TypeResolver; @@ -68,6 +69,8 @@ public class CompatibleSerializer extends AbstractObjectSerializer { private static final Logger LOG = LoggerFactory.getLogger(CompatibleSerializer.class); private final SerializationFieldInfo[] allFields; + private final int[] constructorFieldIndexes; + private final boolean[] constructorFieldMask; private final CompatibleCollectionArrayReader.ReadAction[] allCompatibleReadActions; private final boolean hasCompatibleCollectionArrayRead; private final RecordInfo recordInfo; @@ -139,6 +142,34 @@ public CompatibleSerializer(TypeResolver typeResolver, Class type, TypeDef ty } this.hasDefaultValues = hasDefaultValues; this.defaultValueFields = defaultValueFields; + if (!isRecord && objectCreator.hasConstructorFields()) { + constructorFieldIndexes = + buildConstructorFieldIndexes( + allFields, + true, + defaultFieldNames(defaultValueFields), + defaultDeclaringClasses(defaultValueFields)); + constructorFieldMask = buildConstructorFieldMask(allFields.length, constructorFieldIndexes); + } else { + constructorFieldIndexes = null; + constructorFieldMask = null; + } + } + + private static String[] defaultFieldNames(DefaultValueUtils.DefaultValueField[] fields) { + String[] names = new String[fields.length]; + for (int i = 0; i < fields.length; i++) { + names[i] = fields[i].getFieldName(); + } + return names; + } + + private static Class[] defaultDeclaringClasses(DefaultValueUtils.DefaultValueField[] fields) { + Class[] declaringClasses = new Class[fields.length]; + for (int i = 0; i < fields.length; i++) { + declaringClasses[i] = fields[i].getDeclaringClass(); + } + return declaringClasses; } /** Used by generated compatible serializers for top-level list/array compatible field reads. */ @@ -232,14 +263,44 @@ private T newInstance() { return newBean(); } T obj = - AndroidSupport.IS_ANDROID || GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE + AndroidSupport.IS_ANDROID + || GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE + || JdkVersion.MAJOR_VERSION >= 25 ? newBean() - : UnsafeOps.newInstance(type); + : ObjectCreators.getObjectCreator(type).newInstance(); // Set default values for missing fields in Scala case classes DefaultValueUtils.setDefaultValues(obj, defaultValueFields); return obj; } + private Object[] compatibleConstructorArgs(Object[] fieldValues) { + String[] fieldNames = objectCreator.getConstructorFieldNames(); + Class[] declaringClasses = objectCreator.getConstructorFieldDeclaringClasses(); + Class[] fieldTypes = objectCreator.getConstructorFieldTypes(); + Object[] args = new Object[constructorFieldIndexes.length]; + for (int i = 0; i < constructorFieldIndexes.length; i++) { + int index = constructorFieldIndexes[i]; + if (index >= 0) { + args[i] = fieldValues[index]; + } else { + Class declaringClass = declaringClasses == null ? null : declaringClasses[i]; + args[i] = defaultConstructorValue(fieldNames[i], declaringClass, fieldTypes[i]); + } + } + return args; + } + + private Object defaultConstructorValue( + String fieldName, Class declaringClass, Class fieldType) { + for (DefaultValueUtils.DefaultValueField defaultValueField : defaultValueFields) { + if (defaultValueField.getFieldName().equals(fieldName) + && (declaringClass == null || defaultValueField.getDeclaringClass() == declaringClass)) { + return defaultValueField.getDefaultValue(); + } + } + return AbstractObjectSerializer.defaultConstructorValue(fieldType); + } + @Override public T read(ReadContext readContext) { if (isRecord) { @@ -254,6 +315,10 @@ public T read(ReadContext readContext) { Arrays.fill(recordInfo.getRecordComponents(), null); return t; } + if (objectCreator.hasConstructorFields()) { + Object[] fieldValues = new Object[allFields.length]; + return readConstructorObject(readContext, fieldValues); + } T targetObject = newInstance(); if (readContext.hasPreservedRefId()) { readContext.reference(targetObject); @@ -266,6 +331,96 @@ public T read(ReadContext readContext) { return targetObject; } + private T readConstructorObject(ReadContext readContext, Object[] fieldValues) { + beginConstructorRef(readContext); + try { + boolean[] bufferedNonConstructorFields = new boolean[allFields.length]; + int remainingConstructorFields = countConstructorFields(); + T targetObject = null; + if (remainingConstructorFields == 0) { + targetObject = createConstructorObject(fieldValues); + referenceConstructorRef(readContext, targetObject); + setNonConstructorDefaultValues(targetObject); + } + MemoryBuffer buffer = readContext.getBuffer(); + RefReader refReader = readContext.getRefReader(); + Generics generics = readContext.getGenerics(); + for (int i = 0; i < allFields.length; i++) { + SerializationFieldInfo fieldInfo = allFields[i]; + CompatibleCollectionArrayReader.ReadAction action = + compatibleCollectionArrayReadAction(allCompatibleReadActions, i); + if (constructorFieldMask[i]) { + fieldValues[i] = + ctorFieldValue( + readContext, + readFieldValue(readContext, refReader, generics, fieldInfo, buffer, action), + type); + remainingConstructorFields--; + if (remainingConstructorFields == 0) { + checkNoUnresolvedReadRef(readContext); + targetObject = createConstructorObject(fieldValues); + referenceConstructorRef(readContext, targetObject); + setNonConstructorDefaultValues(targetObject); + setBufferedNonConstructorFields( + targetObject, fieldValues, bufferedNonConstructorFields); + } + } else if (targetObject == null) { + fieldValues[i] = + bufferFieldValue( + readContext, + readFieldValue(readContext, refReader, generics, fieldInfo, buffer, action), + type); + bufferedNonConstructorFields[i] = true; + } else { + readField(readContext, targetObject, refReader, generics, fieldInfo, buffer, action); + } + } + return targetObject; + } finally { + endConstructorRef(readContext); + } + } + + private int countConstructorFields() { + int count = 0; + for (boolean constructorField : constructorFieldMask) { + if (constructorField) { + count++; + } + } + return count; + } + + private T createConstructorObject(Object[] fieldValues) { + return objectCreator.newInstanceWithArguments(compatibleConstructorArgs(fieldValues)); + } + + private void setNonConstructorDefaultValues(T targetObject) { + DefaultValueUtils.setDefaultValues( + targetObject, + defaultValueFields, + objectCreator.getConstructorFieldNames(), + objectCreator.getConstructorFieldDeclaringClasses()); + } + + private void setBufferedNonConstructorFields( + T targetObject, Object[] fieldValues, boolean[] bufferedNonConstructorFields) { + for (int i = 0; i < allFields.length; i++) { + if (bufferedNonConstructorFields[i]) { + setFieldValue( + targetObject, allFields[i], resolveBufferedValue(fieldValues[i], 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(); @@ -275,6 +430,17 @@ private void readFields(ReadContext readContext, T targetObject) { } } + private void readFields(ReadContext readContext, T targetObject, boolean constructorFields) { + MemoryBuffer buffer = readContext.getBuffer(); + RefReader refReader = readContext.getRefReader(); + Generics generics = readContext.getGenerics(); + for (int i = 0; i < allFields.length; i++) { + if (constructorFieldMask[i] == constructorFields) { + readField(readContext, targetObject, refReader, generics, allFields[i], buffer, null); + } + } + } + private void readFields(ReadContext readContext, Object[] fields) { MemoryBuffer buffer = readContext.getBuffer(); int counter = 0; @@ -285,6 +451,17 @@ private void readFields(ReadContext readContext, Object[] fields) { } } + private void readFields(ReadContext readContext, Object[] fields, boolean constructorFields) { + MemoryBuffer buffer = readContext.getBuffer(); + RefReader refReader = readContext.getRefReader(); + Generics generics = readContext.getGenerics(); + for (int i = 0; i < allFields.length; i++) { + if (constructorFieldMask[i] == constructorFields) { + fields[i] = readField(readContext, refReader, generics, allFields[i], buffer, null); + } + } + } + private void compatibleRead( ReadContext readContext, SerializationFieldInfo fieldInfo, Object obj) { MemoryBuffer buffer = readContext.getBuffer(); @@ -309,6 +486,25 @@ private void readFieldsWithCompatibleCollectionArray(ReadContext readContext, T } } + private void readFieldsWithCompatibleCollectionArray( + ReadContext readContext, T targetObject, boolean constructorFields) { + MemoryBuffer buffer = readContext.getBuffer(); + RefReader refReader = readContext.getRefReader(); + Generics generics = readContext.getGenerics(); + for (int i = 0; i < allFields.length; i++) { + if (constructorFieldMask[i] != constructorFields) { + continue; + } + SerializationFieldInfo fieldInfo = allFields[i]; + CompatibleCollectionArrayReader.ReadAction action = + compatibleCollectionArrayReadAction(allCompatibleReadActions, i); + if (Utils.DEBUG_OUTPUT_VERBOSE) { + printFieldDebugInfo(fieldInfo, buffer); + } + readField(readContext, targetObject, refReader, generics, fieldInfo, buffer, action); + } + } + private void readFieldsWithCompatibleCollectionArray(ReadContext readContext, Object[] fields) { MemoryBuffer buffer = readContext.getBuffer(); int counter = 0; @@ -325,6 +521,25 @@ private void readFieldsWithCompatibleCollectionArray(ReadContext readContext, Ob } } + private void readFieldsWithCompatibleCollectionArray( + ReadContext readContext, Object[] fields, boolean constructorFields) { + MemoryBuffer buffer = readContext.getBuffer(); + RefReader refReader = readContext.getRefReader(); + Generics generics = readContext.getGenerics(); + for (int i = 0; i < allFields.length; i++) { + if (constructorFieldMask[i] != constructorFields) { + continue; + } + SerializationFieldInfo fieldInfo = allFields[i]; + CompatibleCollectionArrayReader.ReadAction action = + compatibleCollectionArrayReadAction(allCompatibleReadActions, i); + if (Utils.DEBUG_OUTPUT_ENABLED) { + printFieldDebugInfo(fieldInfo, buffer); + } + fields[i] = readField(readContext, refReader, generics, fieldInfo, buffer, action); + } + } + private void readField( ReadContext readContext, T targetObject, @@ -384,6 +599,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 dc22b47edb..8c4286e852 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,23 +38,41 @@ 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.platform.internal._JDKAccess; +import org.apache.fory.reflect.FieldAccessor; import org.apache.fory.reflect.ObjectCreator; import org.apache.fory.reflect.ObjectCreators; 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"}) public final class ExceptionSerializers { private static final Set> THROWABLE_SUPER_CLASSES = ofHashSet(Throwable.class); + private static final ObjectCreator FIELD_ONLY_CREATOR = new FieldOnlyCreator(); private ExceptionSerializers() {} + private static final class FieldOnlyCreator extends ObjectCreator { + private FieldOnlyCreator() { + super(Object.class); + } + + @Override + public Object newInstance() { + throw new UnsupportedOperationException("Throwable layer serializers do not create objects"); + } + + @Override + public Object newInstanceWithArguments(Object... arguments) { + throw new UnsupportedOperationException("Throwable layer serializers do not create objects"); + } + } + public static final class ExceptionSerializer extends Serializer { private final Config config; private final TypeResolver typeResolver; @@ -69,17 +87,18 @@ public ExceptionSerializer(TypeResolver typeResolver, Class type) { this.typeResolver = typeResolver; messageConstructor = getOptionalMessageConstructor(type); objectCreator = - messageConstructor == null && !AndroidSupport.IS_ANDROID + messageConstructor == null && MemoryUtils.JDK_LANG_FIELD_ACCESS ? createThrowableObjectCreator(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. On JDK25+, open " + + "java.base/java.lang to org.apache.fory.core,org.apache.fory.format."); } // 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 +129,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,7 +139,7 @@ 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); } @@ -136,10 +155,11 @@ 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. " + + "On JDK25+, open java.base/java.lang to " + + "org.apache.fory.core,org.apache.fory.format."); } int refId = readContext.lastPreservedRefId(); if (refId >= 0) { @@ -151,9 +171,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. On JDK25+, open java.base/java.lang " + + "to org.apache.fory.core,org.apache.fory.format."); } T obj = newThrowableWithMessage(detailMessage); readContext.reference(obj); @@ -223,16 +244,17 @@ private void writeNumClassLayers(MemoryBuffer buffer, Serializer[] slotsSerializ 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,8 +371,11 @@ 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); } @@ -360,7 +385,7 @@ private static StackTraceElement newStackTraceElement( private static ObjectCreator createThrowableObjectCreator( Class type) { if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { - return new ObjectCreators.UnsafeObjectCreator<>(type); + return ObjectCreators.getObjectCreator(type); } if (ReflectionUtils.getCtrHandle(type, false) != null) { return ObjectCreators.getObjectCreator(type); @@ -418,7 +443,7 @@ private static Serializer[] buildSlotsSerializers(TypeResolver typeResolver, slotsSerializer = new CompatibleLayerSerializer(typeResolver, type, layerTypeDef, layerMarkerClass); } else { - slotsSerializer = new ObjectSerializer<>(typeResolver, type, false); + slotsSerializer = new ObjectSerializer<>(typeResolver, type, false, fieldOnlyCreator()); } serializers.add(slotsSerializer); type = (Class) type.getSuperclass(); @@ -428,6 +453,10 @@ private static Serializer[] buildSlotsSerializers(TypeResolver typeResolver, return serializers.toArray(new Serializer[0]); } + private static ObjectCreator fieldOnlyCreator() { + return (ObjectCreator) FIELD_ONLY_CREATOR; + } + private static void readAndSetFields( ReadContext readContext, Object target, Serializer[] slotsSerializers, Config config) { readAndCheckNumClassLayers(readContext, target.getClass(), slotsSerializers.length); @@ -532,15 +561,13 @@ private static boolean containsPendingThrowable(Throwable throwable, SetWhen 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 @@ -114,6 +119,24 @@ public Object read(ReadContext readContext) { throw new IllegalStateException("unreachable code"); } + @Override + public Object copy(CopyContext copyContext, Object value) { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try (ObjectOutputStream output = new ObjectOutputStream(bytes)) { + output.writeObject(value); + } + try (ObjectInputStream input = + new ClassLoaderObjectInputStream( + typeResolver.getClassLoader(), 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..af09950c50 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,8 @@ import org.apache.fory.logging.LoggerFactory; import org.apache.fory.memory.MemoryBuffer; import org.apache.fory.meta.TypeDef; +import org.apache.fory.reflect.ObjectCreator; +import org.apache.fory.reflect.ObjectCreators; import org.apache.fory.resolver.TypeResolver; import org.apache.fory.serializer.FieldGroups.SerializationFieldInfo; import org.apache.fory.serializer.struct.Fingerprint; @@ -63,6 +65,8 @@ public final class ObjectSerializer extends AbstractObjectSerializer { private final RecordInfo recordInfo; private final SerializationFieldInfo[] allFields; + private final int[] constructorFieldIndexes; + private final boolean[] constructorFieldMask; private final int classVersionHash; public ObjectSerializer(TypeResolver typeResolver, Class cls) { @@ -70,7 +74,15 @@ public ObjectSerializer(TypeResolver typeResolver, Class cls) { } public ObjectSerializer(TypeResolver typeResolver, Class cls, boolean resolveParent) { - super(typeResolver, cls); + this(typeResolver, cls, resolveParent, ObjectCreators.getObjectCreator(cls)); + } + + public ObjectSerializer( + TypeResolver typeResolver, + Class cls, + boolean resolveParent, + ObjectCreator objectCreator) { + super(typeResolver, cls, objectCreator); // avoid recursive building serializers. // Use `setSerializerIfAbsent` to avoid overwriting existing serializer for class when used // as data serializer. @@ -126,6 +138,13 @@ public ObjectSerializer(TypeResolver typeResolver, Class cls, boolean resolve } FieldGroups fieldGroups = FieldGroups.buildFieldInfos(typeResolver, grouper); allFields = fieldGroups.allFields; + if (!isRecord && objectCreator.hasConstructorFields()) { + constructorFieldIndexes = buildConstructorFieldIndexes(allFields); + constructorFieldMask = buildConstructorFieldMask(allFields.length, constructorFieldIndexes); + } else { + constructorFieldIndexes = null; + constructorFieldMask = null; + } } @Override @@ -137,11 +156,29 @@ 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); } } + private void writeFields( + WriteContext writeContext, + T value, + RefWriter refWriter, + Generics generics, + boolean constructorFields) { + for (int i = 0; i < allFields.length; i++) { + if (constructorFieldMask[i] == constructorFields) { + writeFieldByCodecCategory(writeContext, value, refWriter, generics, allFields[i]); + } + } + } + private void printWriteFieldDebugInfo(SerializationFieldInfo fieldInfo, MemoryBuffer buffer) { LOG.info( "[Java] write field {} of type {}, writer index {}", @@ -202,11 +239,89 @@ public T read(ReadContext readContext) { Arrays.fill(recordInfo.getRecordComponents(), null); return obj; } + if (objectCreator.hasConstructorFields()) { + return readConstructorObject(readContext); + } T obj = newBean(); readContext.reference(obj); return readAndSetFields(readContext, obj); } + private T readConstructorObject(ReadContext readContext) { + beginConstructorRef(readContext); + try { + MemoryBuffer buffer = readContext.getBuffer(); + if (typeResolver.checkClassVersion()) { + int hash = buffer.readInt32(); + checkClassVersion(type, hash, classVersionHash); + } + Object[] fieldValues = new Object[allFields.length]; + boolean[] bufferedNonConstructorFields = new boolean[allFields.length]; + int remainingConstructorFields = countConstructorFields(); + T obj = null; + if (remainingConstructorFields == 0) { + obj = createConstructorObject(fieldValues); + referenceConstructorRef(readContext, obj); + } + RefReader refReader = readContext.getRefReader(); + Generics generics = readContext.getGenerics(); + for (int i = 0; i < allFields.length; i++) { + SerializationFieldInfo fieldInfo = allFields[i]; + if (constructorFieldMask[i]) { + fieldValues[i] = + ctorFieldValue( + readContext, + readFieldByCodecCategory(readContext, refReader, generics, fieldInfo, buffer), + type); + remainingConstructorFields--; + if (remainingConstructorFields == 0) { + checkNoUnresolvedReadRef(readContext); + obj = createConstructorObject(fieldValues); + referenceConstructorRef(readContext, obj); + setBufferedNonConstructorFields(obj, fieldValues, bufferedNonConstructorFields); + } + } else if (obj == null) { + fieldValues[i] = + bufferFieldValue( + readContext, + readFieldByCodecCategory(readContext, refReader, generics, fieldInfo, buffer), + type); + bufferedNonConstructorFields[i] = true; + } else { + readAndSetFieldByCodecCategory(readContext, refReader, generics, fieldInfo, buffer, obj); + } + } + return obj; + } finally { + endConstructorRef(readContext); + } + } + + private int countConstructorFields() { + int count = 0; + for (boolean constructorField : constructorFieldMask) { + if (constructorField) { + count++; + } + } + return count; + } + + private T createConstructorObject(Object[] fieldValues) { + return objectCreator.newInstanceWithArguments( + constructorArgs( + fieldValues, constructorFieldIndexes, objectCreator.getConstructorFieldTypes())); + } + + private void setBufferedNonConstructorFields( + T obj, Object[] fieldValues, boolean[] bufferedNonConstructorFields) { + for (int i = 0; i < allFields.length; i++) { + if (bufferedNonConstructorFields[i]) { + allFields[i].fieldAccessor.putObject(obj, resolveBufferedValue(fieldValues[i], obj)); + } + } + } + public Object[] readFields(ReadContext readContext) { MemoryBuffer buffer = readContext.getBuffer(); RefReader refReader = readContext.getRefReader(); @@ -224,6 +339,19 @@ public Object[] readFields(ReadContext readContext) { return fieldValues; } + private void readFields( + ReadContext readContext, Object[] fieldValues, boolean constructorFields) { + MemoryBuffer buffer = readContext.getBuffer(); + RefReader refReader = readContext.getRefReader(); + Generics generics = readContext.getGenerics(); + for (int i = 0; i < allFields.length; i++) { + if (constructorFieldMask[i] == constructorFields) { + fieldValues[i] = + readFieldByCodecCategory(readContext, refReader, generics, allFields[i], buffer); + } + } + } + public T readAndSetFields(ReadContext readContext, T obj) { MemoryBuffer buffer = readContext.getBuffer(); RefReader refReader = readContext.getRefReader(); @@ -238,6 +366,17 @@ public T readAndSetFields(ReadContext readContext, T obj) { return obj; } + private void readAndSetFields(ReadContext readContext, T obj, boolean constructorFields) { + MemoryBuffer buffer = readContext.getBuffer(); + RefReader refReader = readContext.getRefReader(); + Generics generics = readContext.getGenerics(); + for (int i = 0; i < allFields.length; i++) { + if (constructorFieldMask[i] == constructorFields) { + readAndSetFieldByCodecCategory(readContext, refReader, generics, allFields[i], buffer, obj); + } + } + } + private Object readFieldByCodecCategory( ReadContext readContext, RefReader refReader, 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 fabaa64afd..75e287a940 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 @@ -39,6 +39,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.TreeMap; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -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; @@ -56,25 +58,27 @@ import org.apache.fory.logging.Logger; import org.apache.fory.logging.LoggerFactory; import org.apache.fory.memory.MemoryBuffer; +import org.apache.fory.memory.MemoryUtils; import org.apache.fory.meta.FieldInfo; import org.apache.fory.meta.FieldTypes; import org.apache.fory.meta.NativeTypeDefEncoder; 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.platform.internal._JDKAccess; import org.apache.fory.reflect.ObjectCreator; import org.apache.fory.reflect.ObjectCreators; -import org.apache.fory.reflect.ReflectionUtils; import org.apache.fory.resolver.ClassResolver; import org.apache.fory.resolver.TypeInfo; import org.apache.fory.resolver.TypeResolver; import org.apache.fory.serializer.PrimitiveSerializers.LongSerializer; +import org.apache.fory.serializer.collection.ChildContainerSerializers; import org.apache.fory.type.Descriptor; import org.apache.fory.type.TypeUtils; 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: @@ -94,6 +98,7 @@ public class ObjectStreamSerializer extends AbstractObjectSerializer { private static final int MAX_CACHED_TYPE_DEFS = 8192; private final SlotInfo[] slotsInfos; + private final Serializer fallbackSerializer; // Instance-level cache: TypeDef ID -> TypeInfo (shared across all slots). private final LongMap typeDefIdToTypeInfo = new LongMap<>(4, 0.4f); @@ -184,11 +189,17 @@ private static ObjectStreamClass safeObjectStreamClassLookup(Class type) { } public ObjectStreamSerializer(TypeResolver typeResolver, Class type) { - super(typeResolver, type, createObjectCreatorForGraalVM(type)); + super(typeResolver, type, createObjectStreamCreator(type)); if (!Serializable.class.isAssignableFrom(type)) { throw new IllegalArgumentException( String.format("Class %s should implement %s.", type, Serializable.class)); } + Serializer fallbackSerializer = fallbackSerializer(typeResolver, type); + this.fallbackSerializer = fallbackSerializer; + if (fallbackSerializer != null) { + slotsInfos = new SlotInfo[0]; + return; + } if (!Throwable.class.isAssignableFrom(type)) { LOG.warn( "{} customized jdk serialization, which is inefficient. " @@ -213,22 +224,79 @@ 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 + private static Serializer fallbackSerializer(TypeResolver typeResolver, Class type) { + if (JdkVersion.MAJOR_VERSION < 25) { + return null; + } + Class childSerializerClass = null; + if (Collection.class.isAssignableFrom(type)) { + childSerializerClass = ChildContainerSerializers.getCollectionSerializerClass(type); + } else if (Map.class.isAssignableFrom(type)) { + childSerializerClass = ChildContainerSerializers.getMapSerializerClass(type); + } + if (childSerializerClass != null) { + return Serializers.newSerializer(typeResolver, type, childSerializerClass); + } + if (type.getName().startsWith("java.")) { + return new JavaSerializer(typeResolver, type); + } + return null; + } + + /** Creates an ObjectCreator for Java ObjectStream-compatible reconstruction. */ + private static ObjectCreator createObjectStreamCreator(Class type) { + if (AndroidSupport.IS_ANDROID) { return ObjectCreators.getObjectCreator(type); } + if (JdkVersion.MAJOR_VERSION >= 25) { + if (hasJdk25Fallback(type)) { + return new FallbackOnlyObjectCreator<>(type); + } + // ObjectStreamSerializer must preserve Java serialization construction semantics. On JDK25+ + // this path cannot fall back to Unsafe, including inside GraalVM native images. + return new ObjectCreators.ParentNoArgCtrObjectCreator<>(type); + } + return ObjectCreators.getObjectCreator(type); + } + + private static boolean hasJdk25Fallback(Class type) { + if (JdkVersion.MAJOR_VERSION < 25) { + return false; + } + if (type.getName().startsWith("java.")) { + return true; + } + if (Collection.class.isAssignableFrom(type)) { + return ChildContainerSerializers.getCollectionSerializerClass(type) != null; + } + if (Map.class.isAssignableFrom(type)) { + return ChildContainerSerializers.getMapSerializerClass(type) != null; + } + return false; + } + + private static final class FallbackOnlyObjectCreator extends ObjectCreator { + private FallbackOnlyObjectCreator(Class type) { + super(type); + } + + @Override + public T newInstance() { + throw new ForyException("ObjectStreamSerializer fallback owns construction for " + type); + } + + @Override + public T newInstanceWithArguments(Object... arguments) { + throw new ForyException("ObjectStreamSerializer fallback owns construction for " + type); + } } @Override public void write(WriteContext writeContext, Object value) { + if (fallbackSerializer != null) { + fallbackSerializer.write(writeContext, value); + return; + } MemoryBuffer buffer = writeContext.getBuffer(); buffer.writeInt16((short) slotsInfos.length); try { @@ -281,6 +349,9 @@ public void write(WriteContext writeContext, Object value) { @Override public Object read(ReadContext readContext) { + if (fallbackSerializer != null) { + return fallbackSerializer.read(readContext); + } MemoryBuffer buffer = readContext.getBuffer(); Object obj = objectCreator.newInstance(); readContext.reference(obj); @@ -399,6 +470,14 @@ public Object read(ReadContext readContext) { return obj; } + @Override + public Object copy(CopyContext copyContext, Object value) { + if (fallbackSerializer != null) { + return fallbackSerializer.copy(copyContext, value); + } + return super.copy(copyContext, value); + } + /** * 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. @@ -610,17 +689,14 @@ private StreamTypeInfo(Class type) { Method writeMethod = null; Method readMethod = null; Method noDataMethod = null; - if (AndroidSupport.IS_ANDROID) { + if (AndroidSupport.IS_ANDROID || !MemoryUtils.JDK_OBJECT_STREAM_FIELD_ACCESS) { 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"); + writeMethod = _JDKAccess.getObjectStreamClassWriteObjectMethod(objectStreamClass); + readMethod = _JDKAccess.getObjectStreamClassReadObjectMethod(objectStreamClass); + noDataMethod = _JDKAccess.getObjectStreamClassReadObjectNoDataMethod(objectStreamClass); } this.writeObjectMethod = writeMethod; this.readObjectMethod = readMethod; 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..e2335af192 --- /dev/null +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/PlatformStringUtils.java @@ -0,0 +1,203 @@ +/* + * 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.nio.charset.StandardCharsets; +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 sun.misc.Unsafe; + +/** Platform-owned string internals used by {@link StringSerializer}. */ +final class PlatformStringUtils { + private static final Unsafe UNSAFE = AndroidSupport.IS_ANDROID ? null : _JDKAccess.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); + } + } + + static final boolean JDK_STRING_FIELD_ACCESS = + !AndroidSupport.IS_ANDROID + && !GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE + && _JDKAccess.JDK_STRING_FIELD_ACCESS; + static final boolean STRING_VALUE_FIELD_IS_CHARS = + JDK_STRING_FIELD_ACCESS && _JDKAccess.STRING_VALUE_FIELD_IS_CHARS; + static final boolean STRING_VALUE_FIELD_IS_BYTES = + JDK_STRING_FIELD_ACCESS && _JDKAccess.STRING_VALUE_FIELD_IS_BYTES; + static final boolean STRING_HAS_COUNT_OFFSET = + JDK_STRING_FIELD_ACCESS && _JDKAccess.STRING_HAS_COUNT_OFFSET; + + private static final long STRING_VALUE_FIELD_OFFSET = + JDK_STRING_FIELD_ACCESS ? _JDKAccess.STRING_VALUE_FIELD_OFFSET : -1; + private static final long STRING_CODER_FIELD_OFFSET = + JDK_STRING_FIELD_ACCESS ? _JDKAccess.STRING_CODER_FIELD_OFFSET : -1; + private static final long STRING_COUNT_FIELD_OFFSET = + JDK_STRING_FIELD_ACCESS ? _JDKAccess.STRING_COUNT_FIELD_OFFSET : -1; + private static final long STRING_OFFSET_FIELD_OFFSET = + JDK_STRING_FIELD_ACCESS ? _JDKAccess.STRING_OFFSET_FIELD_OFFSET : -1; + + private static final byte LATIN1 = 0; + private static final byte UTF16 = 1; + + private PlatformStringUtils() {} + + 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 String newCharsStringZeroCopy(char[] data) { + if (!JDK_STRING_FIELD_ACCESS) { + return new String(data); + } + return _JDKAccess.newCharsStringZeroCopy(data); + } + + static String newBytesStringZeroCopy(byte coder, byte[] data) { + if (!JDK_STRING_FIELD_ACCESS) { + return newBytesStringSlow(coder, data); + } + return _JDKAccess.newBytesStringZeroCopy(coder, data); + } + + private static String newBytesStringSlow(byte coder, byte[] data) { + if (coder == LATIN1) { + return new String(data, StandardCharsets.ISO_8859_1); + } else if (coder == UTF16) { + char[] chars = new char[data.length >> 1]; + for (int i = 0, j = 0; i < data.length; i += 2) { + chars[j++] = (char) ((data[i] & 0xff) | ((data[i + 1] & 0xff) << 8)); + } + return new String(chars); + } else { + return new String(data, StandardCharsets.UTF_8); + } + } + + 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); + } + } + return UNSAFE.getLong(bytes, 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, 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..598977c545 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 @@ -36,14 +36,16 @@ import org.apache.fory.logging.Logger; 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.reflect.ReflectionUtils; +import org.apache.fory.platform.JdkVersion; +import org.apache.fory.platform.internal._JDKAccess; +import org.apache.fory.reflect.ObjectCreators; 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 @@ -75,15 +77,13 @@ 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 || !MemoryUtils.JDK_OBJECT_STREAM_FIELD_ACCESS) { 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 = _JDKAccess.getObjectStreamClassWriteReplaceMethod(objectStreamClass); + readResolveMethod = _JDKAccess.getObjectStreamClassReadResolveMethod(objectStreamClass); } else { // FIXME class with `writeReplace` method defined should be Serializable, // but hessian ignores this check and many existing system are using hessian, @@ -113,7 +113,7 @@ private ReplaceResolveInfo(Class cls) { : (readResolveMethod != null ? readResolveMethod.getDeclaringClass() : null); Function writeReplaceFunc = null, readResolveFunc = null; if (declaringClass != null) { - if (AndroidSupport.IS_ANDROID) { + if (AndroidSupport.IS_ANDROID || !MemoryUtils.JDK_OBJECT_STREAM_FIELD_ACCESS) { makeAccessible(writeReplaceMethod); makeAccessible(readResolveMethod); } else { @@ -184,38 +184,88 @@ 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)) { serializerClass = ExternalizableSerializer.class; } else if (JavaSerializer.getReadRefMethod(cls, true) == null && JavaSerializer.getWriteObjectMethod(cls, true) == null) { - serializerClass = - classResolver.getObjectSerializerClass( - cls, - sc -> - methodInfoCache.setObjectSerializer(createDataSerializer(typeResolver, cls, sc))); + if (JdkVersion.MAJOR_VERSION >= 25 + && Serializable.class.isAssignableFrom(cls) + && !ObjectCreators.supportsJdk25Creation(cls)) { + serializerClass = typeResolver.getDefaultJDKStreamSerializerType(); + } else { + serializerClass = + classResolver.getObjectSerializerClass( + cls, + sc -> + methodInfoCache.setObjectSerializer( + createDataSerializer(typeResolver, cls, sc))); + } } else { serializerClass = typeResolver.getDefaultJDKStreamSerializerType(); } - methodInfoCache.setObjectSerializer(createDataSerializer(typeResolver, cls, serializerClass)); - return methodInfoCache; + return serializerClass; } /** @@ -320,7 +370,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 +410,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 +422,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 +430,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/SerializedLambdaSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/SerializedLambdaSerializer.java index e359979a08..c9897b8442 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 @@ -28,9 +28,9 @@ import org.apache.fory.exception.ForyException; import org.apache.fory.memory.MemoryBuffer; 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.unsafe._JDKAccess; /** * Serializer for {@link SerializedLambda}. It writes the JDK lambda payload through the public @@ -51,8 +51,7 @@ public class SerializedLambdaSerializer extends Serializer { Method readResolveMethod = JavaSerializer.getReadResolveMethod(SERIALIZED_LAMBDA); Preconditions.checkNotNull( readResolveMethod, "Missing readResolve for " + SERIALIZED_LAMBDA); - READ_RESOLVE_HANDLE = - _JDKAccess._trustedLookup(SERIALIZED_LAMBDA).unreflect(readResolveMethod); + READ_RESOLVE_HANDLE = _JDKAccess.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 8da69a667b..2e6b762948 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 @@ -31,6 +31,7 @@ import java.net.URI; import java.nio.charset.Charset; import java.util.Currency; +import java.util.StringTokenizer; import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -48,9 +49,12 @@ 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.FieldAccessor; import org.apache.fory.reflect.ReflectionUtils; import org.apache.fory.resolver.ClassResolver; import org.apache.fory.resolver.TypeInfo; @@ -65,8 +69,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 +275,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 +331,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 +343,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 +355,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 +367,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 +379,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 +499,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 +532,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 +551,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()); @@ -501,6 +596,88 @@ public StringBuffer read(ReadContext readContext) { } } + public static final class StringTokenizerSerializer extends Serializer + implements Shareable { + public StringTokenizerSerializer(Config config) { + super(config, StringTokenizer.class); + } + + @Override + public void write(WriteContext writeContext, StringTokenizer value) { + checkStringTokenizerAccess(); + MemoryBuffer buffer = writeContext.getBuffer(); + writeContext.writeRef(Accessors.STR.getObject(value)); + writeContext.writeRef(Accessors.DELIMITERS.getObject(value)); + buffer.writeBoolean(Accessors.RET_DELIMS.getBoolean(value)); + buffer.writeVarInt32(Accessors.CURRENT_POSITION.getInt(value)); + buffer.writeVarInt32(Accessors.NEW_POSITION.getInt(value)); + buffer.writeBoolean(Accessors.DELIMS_CHANGED.getBoolean(value)); + } + + @Override + public StringTokenizer read(ReadContext readContext) { + checkStringTokenizerAccess(); + String str = (String) readContext.readRef(); + String delimiters = (String) readContext.readRef(); + boolean retDelims = readContext.getBuffer().readBoolean(); + StringTokenizer tokenizer = new StringTokenizer(str, delimiters, retDelims); + restoreState(readContext.getBuffer(), tokenizer); + return tokenizer; + } + + @Override + public StringTokenizer copy(CopyContext copyContext, StringTokenizer value) { + checkStringTokenizerAccess(); + StringTokenizer tokenizer = + new StringTokenizer( + (String) Accessors.STR.getObject(value), + (String) Accessors.DELIMITERS.getObject(value), + Accessors.RET_DELIMS.getBoolean(value)); + Accessors.CURRENT_POSITION.putInt(tokenizer, Accessors.CURRENT_POSITION.getInt(value)); + Accessors.NEW_POSITION.putInt(tokenizer, Accessors.NEW_POSITION.getInt(value)); + Accessors.DELIMS_CHANGED.putBoolean(tokenizer, Accessors.DELIMS_CHANGED.getBoolean(value)); + return tokenizer; + } + + private static void restoreState(MemoryBuffer buffer, StringTokenizer tokenizer) { + Accessors.CURRENT_POSITION.putInt(tokenizer, buffer.readVarInt32()); + Accessors.NEW_POSITION.putInt(tokenizer, buffer.readVarInt32()); + Accessors.DELIMS_CHANGED.putBoolean(tokenizer, buffer.readBoolean()); + } + + private static void checkStringTokenizerAccess() { + if (!MemoryUtils.JDK_COLLECTION_FIELD_ACCESS) { + throw stringTokenizerAccessError(); + } + } + + private static UnsupportedOperationException stringTokenizerAccessError() { + return new UnsupportedOperationException( + "StringTokenizer serialization requires JDK internal field access. On JDK25+, open " + + "java.base/java.util to org.apache.fory.core,org.apache.fory.format."); + } + + private static final class Accessors { + private static final FieldAccessor CURRENT_POSITION = + FieldAccessor.createAccessor( + ReflectionUtils.getField(StringTokenizer.class, "currentPosition")); + private static final FieldAccessor NEW_POSITION = + FieldAccessor.createAccessor( + ReflectionUtils.getField(StringTokenizer.class, "newPosition")); + private static final FieldAccessor STR = + FieldAccessor.createAccessor(ReflectionUtils.getField(StringTokenizer.class, "str")); + private static final FieldAccessor DELIMITERS = + FieldAccessor.createAccessor( + ReflectionUtils.getField(StringTokenizer.class, "delimiters")); + private static final FieldAccessor RET_DELIMS = + FieldAccessor.createAccessor( + ReflectionUtils.getField(StringTokenizer.class, "retDelims")); + private static final FieldAccessor DELIMS_CHANGED = + FieldAccessor.createAccessor( + ReflectionUtils.getField(StringTokenizer.class, "delimsChanged")); + } + } + public static final class AtomicBooleanSerializer extends Serializer implements Shareable { @@ -731,6 +908,10 @@ public static void registerDefaultSerializers(TypeResolver resolver) { resolver.registerInternalSerializer(Class.class, new ClassSerializer(config)); resolver.registerInternalSerializer(StringBuilder.class, new StringBuilderSerializer(config)); resolver.registerInternalSerializer(StringBuffer.class, new StringBufferSerializer(config)); + // Keep this internal type id reserved even when JDK collection internals are not open; + // otherwise payloads written with access enabled decode later collection ids incorrectly. + resolver.registerInternalSerializer( + StringTokenizer.class, new StringTokenizerSerializer(config)); resolver.registerInternalSerializer(BigInteger.class, new BigIntegerSerializer(config)); resolver.registerInternalSerializer(BigDecimal.class, new DecimalSerializer(config)); resolver.registerInternalSerializer(AtomicBoolean.class, new AtomicBooleanSerializer(config)); 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/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 bacdc6b29b..9e4f75d3df 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 @@ -23,16 +23,8 @@ 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 java.lang.invoke.CallSite; -import java.lang.invoke.LambdaMetafactory; -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.MethodType; -import java.lang.reflect.Field; import java.nio.charset.StandardCharsets; import java.util.Arrays; -import java.util.function.BiFunction; -import java.util.function.Function; import org.apache.fory.annotation.CodegenInvoke; import org.apache.fory.codegen.Expression; import org.apache.fory.codegen.Expression.Invoke; @@ -44,17 +36,11 @@ 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.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 @@ -62,78 +48,21 @@ */ @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 byte LATIN1 = 0; - private static final Byte LATIN1_BOXED = LATIN1; private static final byte UTF16 = 1; - private static final Byte UTF16_BOXED = UTF16; 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 final boolean STRING_HAS_COUNT_OFFSET = + PlatformStringUtils.STRING_HAS_COUNT_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 boolean jdkInternalFieldAccess() { + return PlatformStringUtils.JDK_STRING_FIELD_ACCESS; } private final boolean compressString; @@ -171,6 +100,9 @@ public String read(ReadContext readContext) { public static Expression writeStringExpr( Expression strSerializer, Expression buffer, Expression str, boolean compressString) { + if (!jdkInternalFieldAccess()) { + return new Invoke(strSerializer, "writeString", buffer, str); + } if (STRING_VALUE_FIELD_IS_BYTES) { if (compressString) { return new Invoke(strSerializer, "writeCompressedBytesString", buffer, str); @@ -199,6 +131,9 @@ public static Expression writeStringExpr( public static Expression readStringExpr( Expression strSerializer, Expression buffer, boolean compressString) { + if (!jdkInternalFieldAccess()) { + return new Invoke(strSerializer, "readString", STRING_TYPE, buffer); + } if (STRING_VALUE_FIELD_IS_BYTES) { if (compressString) { return new Invoke(strSerializer, "readCompressedBytesString", STRING_TYPE, buffer); @@ -363,7 +298,7 @@ public String readCompressedCharsString(MemoryBuffer buffer) { // Invoked by fory JIT public void writeString(MemoryBuffer buffer, String value) { - if (AndroidSupport.IS_ANDROID) { + if (!jdkInternalFieldAccess()) { writeStringSlow(buffer, value); return; } @@ -397,7 +332,7 @@ private void writeJava8String(MemoryBuffer buffer, String value) { // Invoked by fory JIT public String readString(MemoryBuffer buffer) { - if (AndroidSupport.IS_ANDROID) { + if (!jdkInternalFieldAccess()) { return readStringSlow(buffer); } if (STRING_VALUE_FIELD_IS_BYTES) { @@ -418,7 +353,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; } @@ -465,19 +400,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 { @@ -491,7 +433,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); @@ -508,27 +450,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); } @@ -553,14 +496,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); @@ -568,8 +505,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); @@ -578,13 +515,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); } } @@ -751,12 +688,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); @@ -917,160 +849,14 @@ 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 (!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); + return PlatformStringUtils.newCharsStringZeroCopy(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) { - 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); - } - } 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); - } - } - - private static String newBytesStringSlow(byte coder, byte[] data) { - if (coder == LATIN1) { - return new String(data, StandardCharsets.ISO_8859_1); - } else if (coder == UTF16) { - char[] chars = new char[data.length >> 1]; - for (int i = 0, j = 0; i < data.length; i += 2) { - chars[j++] = (char) ((data[i] & 0xff) | ((data[i + 1] & 0xff) << 8)); - } - return new String(chars); - } else { - return new String(data, StandardCharsets.UTF_8); - } - } - - private static BiFunction getCharsStringZeroCopyCtr() { - if (!STRING_VALUE_FIELD_IS_CHARS) { - return null; - } - MethodHandle handle = getJavaStringZeroCopyCtrHandle(); - if (handle == null) { - return null; - } - try { - // Faster than handle.invokeExact(data, boolean) - CallSite callSite = - LambdaMetafactory.metafactory( - STRING_LOOK_UP, - "apply", - MethodType.methodType(BiFunction.class), - handle.type().generic(), - handle, - handle.type()); - return (BiFunction) callSite.getTarget().invokeExact(); - } catch (Throwable e) { - return null; - } - } - - private static BiFunction getBytesStringZeroCopyCtr() { - if (!STRING_VALUE_FIELD_IS_BYTES) { - return null; - } - MethodHandle handle = getJavaStringZeroCopyCtrHandle(); - 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, - "apply", - MethodType.methodType(BiFunction.class), - handle.type().generic(), - handle, - instantiatedMethodType); - return (BiFunction) callSite.getTarget().invokeExact(); - } catch (Throwable e) { - return null; - } - } - - private static Function getLatinBytesStringZeroCopyCtr() { - if (!STRING_VALUE_FIELD_IS_BYTES) { - return null; - } - if (STRING_LOOK_UP == null) { - return null; - } - try { - Class clazz = Class.forName("java.lang.StringCoding"); - MethodHandles.Lookup caller = STRING_LOOK_UP.in(clazz); - // JDK17 removed this method. - 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; - } - } - - private static MethodHandle getJavaStringZeroCopyCtrHandle() { - Preconditions.checkArgument(JdkVersion.MAJOR_VERSION >= 8); - if (STRING_LOOK_UP == null) { - return null; - } - try { - if (STRING_VALUE_FIELD_IS_CHARS) { - return STRING_LOOK_UP.findConstructor( - String.class, MethodType.methodType(void.class, char[].class, boolean.class)); - } else { - return STRING_LOOK_UP.findConstructor( - String.class, MethodType.methodType(void.class, byte[].class, byte.class)); - } - } catch (Exception e) { - return null; - } + return PlatformStringUtils.newBytesStringZeroCopy(coder, data); } private static void writeCharsUTF16BEToHeap( @@ -1189,13 +975,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; @@ -1228,7 +1011,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 @@ -1244,24 +1027,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..3f7670fd47 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 @@ -52,6 +52,7 @@ import org.apache.fory.exception.ForyException; import org.apache.fory.memory.MemoryBuffer; import org.apache.fory.meta.TypeDef; +import org.apache.fory.reflect.ObjectCreator; import org.apache.fory.reflect.ReflectionUtils; import org.apache.fory.resolver.ClassResolver; import org.apache.fory.resolver.TypeResolver; @@ -73,6 +74,23 @@ */ @SuppressWarnings({"unchecked", "rawtypes"}) public class ChildContainerSerializers { + private static final ObjectCreator FIELD_ONLY_CREATOR = new FieldOnlyCreator(); + + private static final class FieldOnlyCreator extends ObjectCreator { + private FieldOnlyCreator() { + super(Object.class); + } + + @Override + public Object newInstance() { + throw new UnsupportedOperationException("Child-container slots do not create objects"); + } + + @Override + public Object newInstanceWithArguments(Object... arguments) { + throw new UnsupportedOperationException("Child-container slots do not create objects"); + } + } public static Class getCollectionSerializerClass(Class cls) { if (ChildCollectionSerializer.superClasses.contains(cls) @@ -626,7 +644,7 @@ private static Serializer[] buildSlotsSerializers( slotsSerializer = new CompatibleLayerSerializer(typeResolver, cls, layerTypeDef, layerMarkerClass); } else { - slotsSerializer = new ObjectSerializer<>(typeResolver, cls, false); + slotsSerializer = new ObjectSerializer<>(typeResolver, cls, false, slotCreator()); } serializers.add(slotsSerializer); cls = (Class) cls.getSuperclass(); @@ -636,6 +654,10 @@ private static Serializer[] buildSlotsSerializers( return serializers.toArray(new Serializer[0]); } + private static ObjectCreator slotCreator() { + return (ObjectCreator) FIELD_ONLY_CREATOR; + } + private static void readAndSetFields( ReadContext readContext, TypeResolver typeResolver, 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..df3e3a14f4 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,10 @@ 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.memory.MemoryUtils; import org.apache.fory.platform.GraalvmSupport; -import org.apache.fory.platform.UnsafeOps; +import org.apache.fory.platform.JdkVersion; +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 +70,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 @@ -129,14 +128,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 +160,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 || GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE ? value.toArray() - : (Object[]) UnsafeOps.getObject(value, ArrayFieldOffset.VALUE); + : (Object[]) ArrayAccess.ACCESSOR.getObject(value); writeContext.writeRef(array); } } @@ -553,18 +551,28 @@ public static final class SetFromMapSerializer extends CollectionSerializer(); 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 FieldAccessor MAP_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(mapField); + } catch (final Exception e) { + throw new RuntimeException(e); + } + } + } + + private static final class LegacySetFromMapAccess { + private static final FieldAccessor M_ACCESSOR; + private static final FieldAccessor S_ACCESSOR; + + static { + try { + Class type = Class.forName("java.util.Collections$SetFromMap"); + M_ACCESSOR = FieldAccessor.createAccessor(type.getDeclaredField("m")); + S_ACCESSOR = FieldAccessor.createAccessor(type.getDeclaredField("s")); } catch (final Exception e) { throw new RuntimeException(e); } @@ -588,18 +596,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 || GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { throw new UnsupportedOperationException( "This runtime cannot read legacy SetFromMap payloads that require hidden JDK field " + "restoration"); } 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<>()); + LegacySetFromMapAccess.M_ACCESSOR.putObject(set, map); + LegacySetFromMapAccess.S_ACCESSOR.putObject(set, map.keySet()); } catch (Throwable e) { - throw new RuntimeException(e); + throw new UnsupportedOperationException( + "This runtime cannot restore legacy SetFromMap payloads through final JDK fields", e); } setNumElements(0); } @@ -610,12 +619,11 @@ 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) { + if (!MemoryUtils.JDK_COLLECTION_FIELD_ACCESS || GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { return Collections.newSetFromMap(new HashMap(originCollection.size())); } Map map = - (Map) - UnsafeOps.getObject(originCollection, JvmSetFromMapAccess.MAP_FIELD_OFFSET); + (Map) JvmSetFromMapAccess.MAP_ACCESSOR.getObject(originCollection); MapLikeSerializer mapSerializer = (MapLikeSerializer) typeResolver.getSerializer(map.getClass()); Map newMap = mapSerializer.newMap(copyContext, map); @@ -625,9 +633,9 @@ public Collection newCollection(CopyContext copyContext, Collection originCollec @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 || GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { HashMap source = new HashMap<>(value.size()); for (Object element : value) { source.put(element, Boolean.TRUE); @@ -635,10 +643,21 @@ 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 = (Map) JvmSetFromMapAccess.MAP_ACCESSOR.getObject(value); typeInfo = typeResolver.getTypeInfo(map.getClass()); } MapLikeSerializer mapSerializer = (MapLikeSerializer) typeInfo.getSerializer(); + // The legacy payload restores Collections$SetFromMap by writing its final JDK fields. + // JDK25 zero-Unsafe mode cannot do that, so emit the public-constructor shape instead. + if (JdkVersion.MAJOR_VERSION >= 25 && !mapSerializer.supportCodegenHook) { + HashMap source = new HashMap<>(value.size()); + for (Object element : value) { + source.put(element, Boolean.TRUE); + } + map = source; + typeInfo = typeResolver.getTypeInfo(HashMap.class); + mapSerializer = (MapLikeSerializer) typeInfo.getSerializer(); + } typeResolver.writeTypeInfo(writeContext, typeInfo); if (mapSerializer.supportCodegenHook) { buffer.writeBoolean(true); @@ -861,13 +880,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 +898,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 +965,10 @@ public LinkedBlockingQueueSerializer( } private static int getCapacity(LinkedBlockingQueue queue) { - if (AndroidSupport.IS_ANDROID || GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { + if (!MemoryUtils.JDK_CONCURRENT_FIELD_ACCESS || GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { return queue.size() + queue.remainingCapacity(); } - 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/GuavaCollectionSerializers.java b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/GuavaCollectionSerializers.java index 4799c7d2ad..91e036088a 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,17 +52,26 @@ 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 int NUM_RESERVED_TYPE_IDS = 13; + 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 = 17; private static final boolean GUAVA_AVAILABLE = isClassAvailable(IMMUTABLE_BI_MAP_CLASS_NAME) && isClassAvailable(IMMUTABLE_LIST_CLASS_NAME) && isClassAvailable(IMMUTABLE_MAP_CLASS_NAME) && isClassAvailable(IMMUTABLE_SET_CLASS_NAME) && isClassAvailable(IMMUTABLE_SORTED_MAP_CLASS_NAME) - && isClassAvailable(IMMUTABLE_SORTED_SET_CLASS_NAME); + && isClassAvailable(IMMUTABLE_SORTED_SET_CLASS_NAME) + && isClassAvailable(HASH_BASED_TABLE_CLASS_NAME) + && isClassAvailable(IMMUTABLE_INT_ARRAY_CLASS_NAME); private interface MapEntryBuilder { void put(Object key, Object value); @@ -353,6 +370,175 @@ protected T xnewInstance(Map map) { } } + public static final 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 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 { @@ -489,6 +675,22 @@ class GuavaEmptySortedMap {} cls = GuavaEmptySortedMap.class; resolver.registerInternalSerializer(cls, new ImmutableSortedMapSerializer(resolver, cls)); } + cls = ImmutableIntArray.class; + resolver.registerInternalSerializer(cls, new ImmutableIntArraySerializer(resolver, cls)); + cls = loadClass(IMMUTABLE_MAP_FORM_CLASS_NAME); + resolver.registerInternalSerializer(cls, new GuavaMapFormSerializer(resolver, cls, false)); + cls = loadClass(IMMUTABLE_BI_MAP_FORM_CLASS_NAME); + resolver.registerInternalSerializer(cls, new GuavaMapFormSerializer(resolver, cls, true)); + cls = HashBasedTable.class; + resolver.registerInternalSerializer(cls, new HashBasedTableSerializer(resolver, cls)); + } + + static Class loadClass(String className) { + try { + return Class.forName(className, false, GuavaCollectionSerializers.class.getClassLoader()); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } } static Class loadClass(String className, Class 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 953314a03d..2adbf77c46 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 @@ -39,8 +39,8 @@ import org.apache.fory.context.ReadContext; import org.apache.fory.context.WriteContext; 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.reflect.ReflectionUtils; import org.apache.fory.resolver.ClassResolver; import org.apache.fory.resolver.TypeInfo; @@ -336,13 +336,12 @@ public static class EnumMapSerializer extends MapSerializer { private static final byte NORMAL_ENUM_MAP = 0; private static final byte JAVA_SERIALIZED_EMPTY_ENUM_MAP = 1; - private static final class KeyTypeFieldOffset { - // Make offset compatible with graalvm native image. - private static final long VALUE; + private static final class KeyTypeAccess { + private static final FieldAccessor ACCESSOR; static { try { - VALUE = UnsafeOps.objectFieldOffset(EnumMap.class.getDeclaredField("keyType")); + ACCESSOR = FieldAccessor.createAccessor(EnumMap.class.getDeclaredField("keyType")); } catch (final Exception e) { throw new RuntimeException(e); } @@ -360,7 +359,7 @@ public EnumMapSerializer(TypeResolver typeResolver) { @Override public Map onMapWrite(WriteContext writeContext, EnumMap value) { MemoryBuffer buffer = writeContext.getBuffer(); - if (AndroidSupport.IS_ANDROID && value.isEmpty()) { + if (!MemoryUtils.JDK_COLLECTION_FIELD_ACCESS && value.isEmpty()) { buffer.writeByte(JAVA_SERIALIZED_EMPTY_ENUM_MAP); getJavaSerializer().write(writeContext, value); return value; @@ -385,9 +384,10 @@ public EnumMap newMap(ReadContext readContext) { if (payloadMode != NORMAL_ENUM_MAP) { throw new IllegalArgumentException("Unknown EnumMap payload mode: " + payloadMode); } - setNumElements(readMapSize(buffer)); + int numElements = readMapSize(buffer); Class keyType = typeResolver.readTypeInfo(readContext).getType(); EnumMap map = new EnumMap(keyType); + setNumElements(numElements); readContext.reference(map); return map; } @@ -402,7 +402,7 @@ private static Class getKeyType(EnumMap value) { Enum key = (Enum) value.keySet().iterator().next(); return key.getDeclaringClass(); } - return (Class) UnsafeOps.getObject(value, KeyTypeFieldOffset.VALUE); + return (Class) KeyTypeAccess.ACCESSOR.getObject(value); } private JavaSerializer getJavaSerializer() { 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..a3e7893a6b 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,7 +123,7 @@ 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) { @@ -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,7 +181,7 @@ 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) { @@ -198,7 +196,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 +223,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..a024391d46 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,7 +116,7 @@ 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()); @@ -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,12 +160,12 @@ 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()); @@ -181,7 +178,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 +203,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..e17e309a02 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 @@ -26,7 +26,7 @@ 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.reflect.FieldAccessor; import org.apache.fory.resolver.TypeResolver; import org.apache.fory.serializer.collection.CollectionLikeSerializer; import org.apache.fory.util.Preconditions; @@ -39,8 +39,7 @@ @SuppressWarnings("rawtypes") public class SingletonCollectionSerializer extends CollectionLikeSerializer { private final Field field; - private Object base = null; - private long offset = -1; + private FieldAccessor accessor; public SingletonCollectionSerializer(TypeResolver typeResolver, Class cls) { super(typeResolver, cls, false); @@ -78,13 +77,12 @@ public Object read(ReadContext readContext) { throw new ForyException("Failed to read Scala singleton field: " + type, e); } } - long offset = this.offset; - if (offset == -1) { + FieldAccessor accessor = this.accessor; + if (accessor == null) { Preconditions.checkArgument(!GraalvmSupport.isGraalBuildTime()); - offset = this.offset = UnsafeOps.UNSAFE.staticFieldOffset(field); - base = UnsafeOps.UNSAFE.staticFieldBase(field); + accessor = this.accessor = FieldAccessor.createStaticAccessor(field); } - return UnsafeOps.getObject(base, offset); + return accessor.getObject(null); } @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..9ef08ac09b 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 @@ -26,7 +26,7 @@ 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.reflect.FieldAccessor; import org.apache.fory.resolver.TypeResolver; import org.apache.fory.serializer.collection.MapLikeSerializer; import org.apache.fory.util.Preconditions; @@ -39,8 +39,7 @@ @SuppressWarnings("rawtypes") public class SingletonMapSerializer extends MapLikeSerializer { private final Field field; - private Object base = null; - private long offset = -1; + private FieldAccessor accessor; public SingletonMapSerializer(TypeResolver typeResolver, Class cls) { super(typeResolver, cls, false); @@ -78,13 +77,12 @@ public Object read(ReadContext readContext) { throw new ForyException("Failed to read Scala singleton field: " + type, e); } } - long offset = this.offset; - if (offset == -1) { + FieldAccessor accessor = this.accessor; + if (accessor == null) { Preconditions.checkArgument(!GraalvmSupport.isGraalBuildTime()); - offset = this.offset = UnsafeOps.UNSAFE.staticFieldOffset(field); - base = UnsafeOps.UNSAFE.staticFieldBase(field); + accessor = this.accessor = FieldAccessor.createStaticAccessor(field); } - return UnsafeOps.getObject(base, offset); + return accessor.getObject(null); } @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..408da2ebc3 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 @@ -25,7 +25,7 @@ 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.reflect.FieldAccessor; import org.apache.fory.resolver.TypeResolver; import org.apache.fory.serializer.Serializer; import org.apache.fory.util.Preconditions; @@ -37,8 +37,7 @@ @SuppressWarnings("rawtypes") public class SingletonObjectSerializer extends Serializer { private final Field field; - private Object base = null; - private long offset = -1; + private FieldAccessor accessor; public SingletonObjectSerializer(TypeResolver typeResolver, Class type) { super(typeResolver.getConfig(), type); @@ -71,12 +70,11 @@ public Object read(ReadContext readContext) { throw new ForyException("Failed to read Scala singleton field: " + type, e); } } - long offset = this.offset; - if (offset == -1) { + FieldAccessor accessor = this.accessor; + if (accessor == null) { Preconditions.checkArgument(!GraalvmSupport.isGraalBuildTime()); - offset = this.offset = UnsafeOps.UNSAFE.staticFieldOffset(field); - base = UnsafeOps.UNSAFE.staticFieldBase(field); + accessor = this.accessor = FieldAccessor.createStaticAccessor(field); } - return UnsafeOps.getObject(base, offset); + return accessor.getObject(null); } } 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/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/_JDKAccess.java b/java/fory-core/src/main/java/org/apache/fory/util/unsafe/_JDKAccess.java deleted file mode 100644 index e9e8774f60..0000000000 --- a/java/fory-core/src/main/java/org/apache/fory/util/unsafe/_JDKAccess.java +++ /dev/null @@ -1,338 +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.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.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.HashMap; -import java.util.Map; -import java.util.function.BiConsumer; -import java.util.function.Consumer; -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.platform.GraalvmSupport; -import org.apache.fory.platform.JdkVersion; -import org.apache.fory.type.TypeUtils; -import org.apache.fory.util.ExceptionUtils; -import org.apache.fory.util.Preconditions; -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 sun.misc.Unsafe; - -/** Unsafe JDK 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; - - 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); - } - } else { - _INNER_UNSAFE_CLASS = null; - _INNER_UNSAFE = null; - } - } - - private static final ClassValueCache lookupCache = ClassValueCache.newClassKeyCache(32); - - // CHECKSTYLE.OFF:MethodName - - public static Lookup _trustedLookup(Class objectClass) { - // CHECKSTYLE.ON:MethodName - if (GraalvmSupport.isGraalBuildTime()) { - // Lookup will init `java.io.FilePermission`,which is not allowed at graalvm build time - // as a reachable object. - return _Lookup._trustedLookup(objectClass); - } - return lookupCache.get(objectClass, () -> _Lookup._trustedLookup(objectClass)); - } - - public static T tryMakeFunction( - Lookup lookup, MethodHandle handle, Class functionInterface) { - try { - return makeFunction(lookup, handle, functionInterface); - } catch (Throwable e) { - ExceptionUtils.ignore(e); - throw new IllegalStateException(); - } - } - - private static final MethodType jdkFunctionMethodType = - MethodType.methodType(Object.class, Object.class); - - @SuppressWarnings("unchecked") - public static Function makeJDKFunction(Lookup lookup, MethodHandle handle) { - return makeJDKFunction(lookup, handle, jdkFunctionMethodType); - } - - @SuppressWarnings("unchecked") - public static Function makeJDKFunction( - Lookup lookup, MethodHandle handle, MethodType methodType) { - try { - CallSite callSite = - LambdaMetafactory.metafactory( - lookup, - "apply", - MethodType.methodType(Function.class), - methodType, - handle, - boxedMethodType(handle.type())); - return (Function) callSite.getTarget().invokeExact(); - } catch (Throwable e) { - UNSAFE.throwException(e); - throw new IllegalStateException(e); - } - } - - private static final MethodType jdkConsumerMethodType = - MethodType.methodType(void.class, Object.class); - - @SuppressWarnings("unchecked") - public static Consumer makeJDKConsumer(Lookup lookup, MethodHandle handle) { - try { - CallSite callSite = - LambdaMetafactory.metafactory( - lookup, - "accept", - MethodType.methodType(Consumer.class), - jdkConsumerMethodType, - handle, - boxedMethodType(handle.type())); - return (Consumer) callSite.getTarget().invokeExact(); - } catch (Throwable e) { - UNSAFE.throwException(e); - throw new IllegalStateException(e); - } - } - - private static final MethodType jdkBiConsumerMethodType = - MethodType.methodType(void.class, Object.class, Object.class); - - @SuppressWarnings("unchecked") - public static BiConsumer makeJDKBiConsumer(Lookup lookup, MethodHandle handle) { - try { - CallSite callSite = - LambdaMetafactory.metafactory( - lookup, - "accept", - MethodType.methodType(BiConsumer.class), - jdkBiConsumerMethodType, - handle, - boxedMethodType(handle.type())); - return (BiConsumer) callSite.getTarget().invokeExact(); - } catch (Throwable e) { - UNSAFE.throwException(e); - throw new IllegalStateException(e); - } - } - - private static MethodType boxedMethodType(MethodType methodType) { - Class[] paramTypes = new Class[methodType.parameterCount()]; - for (int i = 0; i < paramTypes.length; i++) { - Class t = methodType.parameterType(i); - if (t.isPrimitive()) { - t = TypeUtils.wrap(t); - } - paramTypes[i] = t; - } - return MethodType.methodType(methodType.returnType(), paramTypes); - } - - @SuppressWarnings("unchecked") - public static T makeFunction(Lookup lookup, MethodHandle handle, Method methodToImpl) { - MethodType instantiatedMethodType = boxedMethodType(handle.type()); - MethodType methodToImplType = - MethodType.methodType(methodToImpl.getReturnType(), methodToImpl.getParameterTypes()); - try { - // Faster than handle.invokeExact. - CallSite callSite = - LambdaMetafactory.metafactory( - lookup, - methodToImpl.getName(), - MethodType.methodType(methodToImpl.getDeclaringClass()), - methodToImplType, - handle, - instantiatedMethodType); - return (T) callSite.getTarget().invokeExact(); - } catch (Throwable e) { - UNSAFE.throwException(e); - throw new IllegalStateException(e); - } - } - - public static T makeFunction(Lookup lookup, MethodHandle handle, Class functionInterface) { - String invokedName = "apply"; - try { - Method method = null; - Method[] methods = functionInterface.getMethods(); - for (Method interfaceMethod : methods) { - if (interfaceMethod.getName().equals(invokedName)) { - method = interfaceMethod; - break; - } - } - if (method == null) { - Preconditions.checkArgument(methods.length == 1); - method = methods[0]; - invokedName = method.getName(); - } - MethodType interfaceType = - MethodType.methodType(method.getReturnType(), method.getParameterTypes()); - // Faster than handle.invokeExact. - CallSite callSite = - LambdaMetafactory.metafactory( - lookup, - invokedName, - MethodType.methodType(functionInterface), - interfaceType, - handle, - interfaceType); - // FIXME(chaokunyang) why use invokeExact will fail. - return (T) callSite.getTarget().invoke(); - } catch (Throwable e) { - UNSAFE.throwException(e); - throw new IllegalStateException(e); - } - } - - private static final Map, Tuple2, String>> methodMap = new HashMap<>(); - - static { - methodMap.put(boolean.class, Tuple2.of(Predicate.class, "test")); - methodMap.put(byte.class, Tuple2.of(ToByteFunction.class, "applyAsByte")); - methodMap.put(char.class, Tuple2.of(ToCharFunction.class, "applyAsChar")); - methodMap.put(short.class, Tuple2.of(ToShortFunction.class, "applyAsShort")); - methodMap.put(int.class, Tuple2.of(ToIntFunction.class, "applyAsInt")); - methodMap.put(long.class, Tuple2.of(ToLongFunction.class, "applyAsLong")); - methodMap.put(float.class, Tuple2.of(ToFloatFunction.class, "applyAsFloat")); - methodMap.put(double.class, Tuple2.of(ToDoubleFunction.class, "applyAsDouble")); - } - - public static Tuple2, String> getterMethodInfo(Class type) { - Tuple2, String> info = methodMap.get(type); - if (info == null) { - return Tuple2.of(Function.class, "apply"); - } - return info; - } - - public static Object makeGetterFunction( - MethodHandles.Lookup lookup, MethodHandle handle, Class returnType) { - Tuple2, String> methodInfo = methodMap.get(returnType); - MethodType factoryType; - if (methodInfo == null) { - methodInfo = Tuple2.of(Function.class, "apply"); - factoryType = jdkFunctionMethodType; - } else { - factoryType = MethodType.methodType(returnType, Object.class); - } - try { - CallSite callSite = - LambdaMetafactory.metafactory( - lookup, - methodInfo.f1, - MethodType.methodType(methodInfo.f0), - factoryType, - handle, - handle.type()); - // Can't use invokeExact, since we can't specify exact target type for return variable. - return callSite.getTarget().invoke(); - } catch (ClassNotFoundException | NoClassDefFoundError e) { - // ToByteFunction/ToBoolFunction/.. are not defined in jdk, if the classloader of - // fory functions `ToByteFunction/..` isn't parent classloader of classloader for getter - // represented by handle, then exception will be thrown. - return makeGetterFunction(lookup, handle, Object.class); - } catch (Throwable e) { - UNSAFE.throwException(e); - throw new IllegalStateException(e); - } - } - - private static volatile Method getModuleMethod; - - public static Object getModule(Class cls) { - Preconditions.checkArgument(JdkVersion.MAJOR_VERSION >= 9); - if (getModuleMethod == null) { - try { - getModuleMethod = Class.class.getDeclaredMethod("getModule"); - } catch (NoSuchMethodException e) { - throw new RuntimeException(e); - } - } - try { - return getModuleMethod.invoke(cls); - } catch (IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException(e); - } - } - - // caller sensitive, must use MethodHandle to walk around the check. - private static volatile MethodHandle addReadsHandle; - - public static Object addReads(Object thisModule, Object otherModule) { - Preconditions.checkArgument(JdkVersion.MAJOR_VERSION >= 9); - try { - if (addReadsHandle == null) { - Class cls = Class.forName("java.lang.Module"); - MethodHandles.Lookup lookup = _JDKAccess._trustedLookup(cls); - addReadsHandle = lookup.findVirtual(cls, "addReads", MethodType.methodType(cls, cls)); - } - return addReadsHandle.invoke(thisModule, otherModule); - } catch (Throwable e) { - throw new RuntimeException(e); - } - } -} 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..bc7ffd4e9d --- /dev/null +++ b/java/fory-core/src/main/java25/org/apache/fory/memory/LittleEndian.java @@ -0,0 +1,65 @@ +/* + * 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; + +public class LittleEndian { + 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 MemoryOps.getInt64(o, index); + } + + public static void putInt64(byte[] o, int index, long value) { + MemoryOps.putInt64(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..86827aaeee --- /dev/null +++ b/java/fory-core/src/main/java25/org/apache/fory/memory/MemoryBuffer.java @@ -0,0 +1,4439 @@ +/* + * 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; +import org.apache.fory.platform.AndroidSupport; + +/** + * 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: + * + *

      + *
    • read/write data into a chunk of direct memory. + *
    • additional binary compare, swap, and copy methods. + *
    • little-endian access. + *
    • independent read/write index. + *
    • varint int/long encoding. + *
    • aligned int/long encoding. + *
    + * + *

    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 hold on graalvm build time, the heap unsafe + * offset are not correct in runtime since graalvm will change array base offset. + * + *

    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 Unsafe 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()) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.throwDirectByteBufferUnsupported(); + } else { + 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 (AndroidSupport.IS_ANDROID) { + MemoryOps.initHeapBuffer(this, buffer, offset, length); + } else { + 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 + // ------------------------------------------------------------------------ + + private void checkPosition(long index, long pos, long length) { + if (BoundsChecking.BOUNDS_CHECKING_ENABLED) { + if (index < 0 || pos > addressLimit - length) { + throwOOBException(); + } + } + } + + public void get(int index, byte[] dst) { + get(index, dst, 0, dst.length); + } + + public void get(int index, byte[] dst, int offset, int length) { + final byte[] heapMemory = this.heapMemory; + if (heapMemory != null) { + // System.arraycopy faster for some jdk than Unsafe. + System.arraycopy(heapMemory, heapOffset + index, dst, offset, length); + } else { + final long pos = address + index; + if ((index + | offset + | length + | (offset + length) + | (dst.length - (offset + length)) + | addressLimit - length - pos) + < 0) { + throwOOBException(); + } + readBytesToArray(pos, dst, BYTE_ARRAY_OFFSET + offset, length); + } + } + + public void get(int offset, ByteBuffer target, int numBytes) { + if ((offset | numBytes | (offset + numBytes)) < 0) { + throwOOBException(); + } + if (target.remaining() < numBytes) { + throwOOBException(); + } + if (target.isReadOnly()) { + throw new IllegalArgumentException("read only buffer"); + } + final int targetPos = target.position(); + if (AndroidSupport.IS_ANDROID) { + MemoryOps.get(this, offset, target, numBytes); + } else if (target.isDirect()) { + final long sourceAddr = address + offset; + if (sourceAddr <= addressLimit - numBytes) { + ByteBuffer duplicate = target.duplicate(); + ByteBufferUtil.position(duplicate, targetPos); + duplicate.put(sliceAsByteBuffer(offset, numBytes)); + } else { + throwOOBException(); + } + } else { + assert target.hasArray(); + get(offset, target.array(), targetPos + target.arrayOffset(), numBytes); + } + if (target.position() == targetPos) { + ByteBufferUtil.position(target, targetPos + numBytes); + } + } + + public void put(int offset, ByteBuffer source, int numBytes) { + final int remaining = source.remaining(); + if ((offset | numBytes | (offset + numBytes) | (remaining - numBytes)) < 0) { + throwOOBException(); + } + final int sourcePos = source.position(); + if (AndroidSupport.IS_ANDROID) { + MemoryOps.put(this, offset, source, numBytes); + } else if (source.isDirect()) { + final long targetAddr = address + offset; + if (targetAddr <= addressLimit - numBytes) { + ByteBuffer duplicate = source.duplicate(); + ByteBufferUtil.position(duplicate, sourcePos); + duplicate.limit(sourcePos + numBytes); + if (heapMemory != null) { + duplicate.get(heapMemory, heapOffset + offset, numBytes); + } else { + sliceAsByteBuffer(offset, numBytes).put(duplicate.slice()); + } + } else { + throwOOBException(); + } + } else { + assert source.hasArray(); + put(offset, source.array(), sourcePos + source.arrayOffset(), numBytes); + } + if (source.position() == sourcePos) { + ByteBufferUtil.position(source, sourcePos + numBytes); + } + } + + public void put(int index, byte[] src) { + put(index, src, 0, src.length); + } + + public void put(int index, byte[] src, int offset, int length) { + final byte[] heapMemory = this.heapMemory; + if (heapMemory != null) { + // System.arraycopy faster for some jdk than Unsafe. + System.arraycopy(src, offset, heapMemory, heapOffset + index, length); + } else { + final long pos = address + index; + // check the byte array offset and length + if ((index + | offset + | length + | (offset + length) + | (src.length - (offset + length)) + | addressLimit - length - pos) + < 0) { + throwOOBException(); + } + writeBytesFromArray(pos, src, BYTE_ARRAY_OFFSET + offset, length); + } + } + + public byte getByte(int index) { + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.getByte(this, index); + } + final long pos = address + index; + checkPosition(index, pos, 1); + return loadByte(pos); + } + + // CHECKSTYLE.OFF:MethodName + public byte _unsafeGetByte(int index) { + // CHECKSTYLE.ON:MethodName + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.unsafeGetByte(this, index); + } + return loadByte(address + index); + } + + public void putByte(int index, int b) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.putByte(this, index, b); + } else { + final long pos = address + index; + checkPosition(index, pos, 1); + storeByte(pos, (byte) b); + } + } + + public void putByte(int index, byte b) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.putByte(this, index, b); + } else { + final long pos = address + index; + checkPosition(index, pos, 1); + storeByte(pos, b); + } + } + + // CHECKSTYLE.OFF:MethodName + public void _unsafePutByte(int index, byte b) { + // CHECKSTYLE.ON:MethodName + if (AndroidSupport.IS_ANDROID) { + MemoryOps.unsafePutByte(this, index, b); + } else { + storeByte(address + index, b); + } + } + + public boolean getBoolean(int index) { + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.getBoolean(this, index); + } + final long pos = address + index; + checkPosition(index, pos, 1); + return loadByte(pos) != 0; + } + + // CHECKSTYLE.OFF:MethodName + public boolean _unsafeGetBoolean(int index) { + // CHECKSTYLE.ON:MethodName + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.unsafeGetBoolean(this, index); + } + return loadByte(address + index) != 0; + } + + public void putBoolean(int index, boolean value) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.putBoolean(this, index, value); + } else { + storeByte(address + index, (value ? (byte) 1 : (byte) 0)); + } + } + + // CHECKSTYLE.OFF:MethodName + public void _unsafePutBoolean(int index, boolean value) { + // CHECKSTYLE.ON:MethodName + if (AndroidSupport.IS_ANDROID) { + MemoryOps.putBoolean(this, index, value); + } else { + storeByte(address + index, (value ? (byte) 1 : (byte) 0)); + } + } + + public char getChar(int index) { + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.getChar(this, index); + } + final long pos = address + index; + checkPosition(index, pos, 2); + char c = loadChar(pos); + return LITTLE_ENDIAN ? c : Character.reverseBytes(c); + } + + // CHECKSTYLE.OFF:MethodName + public char _unsafeGetChar(int index) { + // CHECKSTYLE.ON:MethodName + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.unsafeGetChar(this, index); + } + char c = loadChar(address + index); + return LITTLE_ENDIAN ? c : Character.reverseBytes(c); + } + + public void putChar(int index, char value) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.putChar(this, index, value); + } else { + final long pos = address + index; + checkPosition(index, pos, 2); + if (!LITTLE_ENDIAN) { + value = Character.reverseBytes(value); + } + storeChar(pos, value); + } + } + + // CHECKSTYLE.OFF:MethodName + public void _unsafePutChar(int index, char value) { + // CHECKSTYLE.ON:MethodName + if (AndroidSupport.IS_ANDROID) { + MemoryOps.unsafePutChar(this, index, value); + } else { + if (!LITTLE_ENDIAN) { + value = Character.reverseBytes(value); + } + storeChar(address + index, value); + } + } + + public short getInt16(int index) { + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.getInt16(this, index); + } + final long pos = address + index; + checkPosition(index, pos, 2); + short v = loadShort(pos); + return LITTLE_ENDIAN ? v : Short.reverseBytes(v); + } + + public void putInt16(int index, short value) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.putInt16(this, index, value); + } else { + final long pos = address + index; + checkPosition(index, pos, 2); + if (!LITTLE_ENDIAN) { + value = Short.reverseBytes(value); + } + storeShort(pos, value); + } + } + + // CHECKSTYLE.OFF:MethodName + public short _unsafeGetInt16(int index) { + // CHECKSTYLE.ON:MethodName + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.unsafeGetInt16(this, index); + } + 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 (AndroidSupport.IS_ANDROID) { + MemoryOps.unsafePutInt16(this, index, value); + } else { + if (!LITTLE_ENDIAN) { + value = Short.reverseBytes(value); + } + storeShort(address + index, value); + } + } + + public int getInt32(int index) { + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.getInt32(this, index); + } + final long pos = address + index; + checkPosition(index, pos, 4); + int v = loadInt(pos); + return LITTLE_ENDIAN ? v : Integer.reverseBytes(v); + } + + public void putInt32(int index, int value) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.putInt32(this, index, value); + } else { + final long pos = address + index; + checkPosition(index, pos, 4); + if (!LITTLE_ENDIAN) { + value = Integer.reverseBytes(value); + } + storeInt(pos, value); + } + } + + // CHECKSTYLE.OFF:MethodName + public int _unsafeGetInt32(int index) { + // CHECKSTYLE.ON:MethodName + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.unsafeGetInt32(this, index); + } + 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 (AndroidSupport.IS_ANDROID) { + MemoryOps.unsafePutInt32(this, index, value); + } else { + if (!LITTLE_ENDIAN) { + value = Integer.reverseBytes(value); + } + storeInt(address + index, value); + } + } + + public long getInt64(int index) { + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.getInt64(this, index); + } + final long pos = address + index; + checkPosition(index, pos, 8); + long v = loadLong(pos); + return LITTLE_ENDIAN ? v : Long.reverseBytes(v); + } + + public void putInt64(int index, long value) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.putInt64(this, index, value); + } else { + final long pos = address + index; + checkPosition(index, pos, 8); + if (!LITTLE_ENDIAN) { + value = Long.reverseBytes(value); + } + storeLong(pos, value); + } + } + + // CHECKSTYLE.OFF:MethodName + public long _unsafeGetInt64(int index) { + // CHECKSTYLE.ON:MethodName + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.unsafeGetInt64(this, index); + } + 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 (AndroidSupport.IS_ANDROID) { + MemoryOps.unsafePutInt64(this, index, value); + } else { + if (!LITTLE_ENDIAN) { + value = Long.reverseBytes(value); + } + storeLong(address + index, value); + } + } + + public float getFloat32(int index) { + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.getFloat32(this, index); + } + final long pos = address + index; + checkPosition(index, pos, 4); + int v = loadInt(pos); + if (!LITTLE_ENDIAN) { + v = Integer.reverseBytes(v); + } + return Float.intBitsToFloat(v); + } + + public void putFloat32(int index, float value) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.putFloat32(this, index, value); + } else { + final long pos = address + index; + checkPosition(index, pos, 4); + int v = Float.floatToRawIntBits(value); + if (!LITTLE_ENDIAN) { + v = Integer.reverseBytes(v); + } + storeInt(pos, v); + } + } + + public double getFloat64(int index) { + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.getFloat64(this, index); + } + final long pos = address + index; + checkPosition(index, pos, 8); + long v = loadLong(pos); + if (!LITTLE_ENDIAN) { + v = Long.reverseBytes(v); + } + return Double.longBitsToDouble(v); + } + + public void putFloat64(int index, double value) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.putFloat64(this, index, value); + } else { + final long pos = address + index; + checkPosition(index, pos, 8); + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.writeBoolean(this, value); + } else { + 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 + if (AndroidSupport.IS_ANDROID) { + MemoryOps.unsafeWriteByte(this, value); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.writeByte(this, value); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.writeChar(this, value); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.writeInt16(this, value); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.writeInt32(this, value); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.writeInt64(this, value); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.writeFloat32(this, value); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.writeFloat64(this, value); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.writeVarInt32(this, 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 + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.unsafeWriteVarInt32(this, v); + } + // 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) { + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.writeVarUInt32(this, 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 + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.unsafeWriteVarUInt32(this, v); + } + 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) { + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.writeVarUInt32Small7(this, 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 + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.unsafePutVarUInt32(this, index, value); + } + 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 + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.unsafePutVarUint36Small(this, index, value); + } + 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) { + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.writeVarUInt32Aligned(this, 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) { + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.writeVarInt64(this, value); + } + ensure(writerIndex + 9); + return _unsafeWriteVarUInt64((value << 1) ^ (value >> 63)); + } + + @CodegenInvoke + // CHECKSTYLE.OFF:MethodName + public int _unsafeWriteVarInt64(long value) { + // CHECKSTYLE.ON:MethodName + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.unsafeWriteVarInt64(this, value); + } + return _unsafeWriteVarUInt64((value << 1) ^ (value >> 63)); + } + + public int writeVarUInt64(long value) { + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.writeVarUInt64(this, 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 + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.unsafeWriteVarUInt64(this, value); + } + 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 + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.unsafeWriteTaggedUInt64(this, value); + } + 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 + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.unsafeWriteTaggedInt64(this, value); + } + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.writeBytesWithSize(this, values); + } else { + writeVarUInt32Small7(values.length); + writeBytes(values, 0, values.length); + } + } + + public void writeBooleansWithSize(boolean[] values) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.writeBooleansWithSize(this, values); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.writeBooleans(this, values, offset, numElements); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.writeCharsWithSize(this, values); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.writeChars(this, values, offset, numElements); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.writeShortsWithSize(this, values); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.writeShorts(this, values, offset, numElements); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.writeIntsWithSize(this, values); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.writeInts(this, values, offset, numElements); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.writeLongsWithSize(this, values); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.writeLongs(this, values, offset, numElements); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.writeFloatsWithSize(this, values); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.writeFloats(this, values, offset, numElements); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.writeDoublesWithSize(this, values); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.writeDoubles(this, values, offset, numElements); + } else { + 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() { + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.readBoolean(this); + } + 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() { + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.readUInt8(this); + } + 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() { + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.readByte(this); + } + int readerIdx = readerIndex; + if (readerIdx > size - 1) { + streamReader.fillBuffer(1); + } + readerIndex = readerIdx + 1; + return loadByte(address + readerIdx); + } + + public char readChar() { + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.readChar(this); + } + 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() { + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.readInt16(this); + } + 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 + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.readInt16(this); + } + 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 + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.readInt16(this); + } + 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() { + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.readInt32(this); + } + 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 + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.readInt32(this); + } + 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 + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.readInt32(this); + } + 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() { + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.readInt64(this); + } + 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 + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.readInt64(this); + } + 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 + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.readInt64(this); + } + 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 + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.readTaggedUInt64(this); + } + 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 + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.readTaggedUInt64(this); + } + 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 + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.readTaggedInt64(this); + } + // 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 + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.readTaggedInt64(this); + } + // 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() { + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.readFloat32(this); + } + // 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 + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.readFloat32(this); + } + 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 + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.readFloat32(this); + } + 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() { + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.readFloat64(this); + } + // 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 + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.readFloat64(this); + } + 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 + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.readFloat64(this); + } + 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 (AndroidSupport.IS_ANDROID) { + return MemoryOps.readVarInt32(this); + } + 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 + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.readVarInt32(this); + } + // 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 + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.readVarInt32(this); + } + // 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() { + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.readVarUint36Small(this); + } + // Android exits above. Keep JVM small-varint bulk reads as raw Unsafe 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() { + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.readVarUInt32(this); + } + 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() { + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.readVarUInt32(this); + } + 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() { + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.readVarUInt32(this); + } + 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() { + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.readVarInt64(this); + } + return LITTLE_ENDIAN ? _readVarInt64OnLE() : _readVarInt64OnBE(); + } + + @CodegenInvoke + // CHECKSTYLE.OFF:MethodName + public long _readVarInt64OnLE() { + // CHECKSTYLE.ON:MethodName + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.readVarInt64(this); + } + // 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 + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.readVarInt64(this); + } + 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() { + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.readVarUInt64(this); + } + 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() { + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.readAlignedVarUInt32(this); + } + 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) { + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.readBytesAsInt64(this, 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() { + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.readBinarySize(this); + } + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.readByteArrayPayload(this, values, numBytes); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.readBooleanArrayPayload(this, values, numBytes); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.readCharArrayPayload(this, values, numBytes); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.readInt16ArrayPayload(this, values, numBytes); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.readInt32ArrayPayload(this, values, numBytes); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.readInt64ArrayPayload(this, values, numBytes); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.readFloat32ArrayPayload(this, values, numBytes); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.readFloat64ArrayPayload(this, values, numBytes); + } else { + 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 (AndroidSupport.IS_ANDROID) { + MemoryOps.readBooleans(this, values, offset, numElements); + } else { + 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 (AndroidSupport.IS_ANDROID) { + MemoryOps.readChars(this, chars, offset, numElements); + } else { + 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 (AndroidSupport.IS_ANDROID) { + MemoryOps.readShorts(this, values, offset, numElements); + } else { + 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 (AndroidSupport.IS_ANDROID) { + MemoryOps.readInts(this, values, offset, numElements); + } else { + 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 (AndroidSupport.IS_ANDROID) { + MemoryOps.readLongs(this, values, offset, numElements); + } else { + 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 (AndroidSupport.IS_ANDROID) { + MemoryOps.readFloats(this, values, offset, numElements); + } else { + 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 (AndroidSupport.IS_ANDROID) { + MemoryOps.readDoubles(this, values, offset, numElements); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.copyTo(this, offset, target, targetOffset, numBytes); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.copyToByteArray(this, offset, target, targetOffset, numBytes); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.copyToBooleanArray(this, offset, target, targetOffset, numBytes); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.copyToCharArray(this, offset, target, targetOffset, numBytes); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.copyToShortArray(this, offset, target, targetOffset, numBytes); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.copyToIntArray(this, offset, target, targetOffset, numBytes); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.copyToLongArray(this, offset, target, targetOffset, numBytes); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.copyToFloatArray(this, offset, target, targetOffset, numBytes); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.copyToDoubleArray(this, offset, target, targetOffset, numBytes); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.copyFromByteArray(this, offset, source, sourceOffset, numBytes); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.copyFromBooleanArray(this, offset, source, sourceOffset, numBytes); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.copyFromCharArray(this, offset, source, sourceOffset, numBytes); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.copyFromShortArray(this, offset, source, sourceOffset, numBytes); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.copyFromIntArray(this, offset, source, sourceOffset, numBytes); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.copyFromLongArray(this, offset, source, sourceOffset, numBytes); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.copyFromFloatArray(this, offset, source, sourceOffset, numBytes); + } else { + 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.copyFromDoubleArray(this, offset, source, sourceOffset, numBytes); + } else { + 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; + } + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.equalTo(this, buf2, offset1, offset2, len); + } + 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; + } + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.equalTo(this, bytes, bytesOffset, offset, len); + } + 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 (AndroidSupport.IS_ANDROID) { + return MemoryOps.fromByteBuffer(buffer); + } else 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) { + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.directByteBufferUnsupported(); + } + 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/DefineClass.java b/java/fory-core/src/main/java25/org/apache/fory/platform/internal/DefineClass.java new file mode 100644 index 0000000000..2c4e62358f --- /dev/null +++ b/java/fory-core/src/main/java25/org/apache/fory/platform/internal/DefineClass.java @@ -0,0 +1,110 @@ +/* + * 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.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); + } + } + + public static Class defineHiddenNestmate(Class neighbor, byte[] bytecodes) { + if (AndroidSupport.IS_ANDROID) { + throw new UnsupportedOperationException( + "Runtime bytecode loading is unsupported on Android."); + } + Preconditions.checkNotNull(neighbor); + Preconditions.checkNotNull(bytecodes); + try { + Lookup lookup = _Lookup.privateLookupIn(neighbor, MethodHandles.lookup()); + return lookup + .defineHiddenClass(bytecodes, true, Lookup.ClassOption.NESTMATE) + .lookupClass(); + } catch (IllegalAccessException | IllegalStateException e) { + Module module = neighbor.getModule(); + Package pkg = neighbor.getPackage(); + String packageName = pkg == null ? "" : pkg.getName(); + throw new IllegalStateException( + "Cannot define hidden nestmate for " + + neighbor.getName() + + " because package " + + packageName + + " in module " + + module.getName() + + " is not open to org.apache.fory.core,org.apache.fory.format", + e); + } + } +} diff --git a/java/fory-core/src/main/java25/org/apache/fory/platform/internal/_JDKAccess.java b/java/fory-core/src/main/java25/org/apache/fory/platform/internal/_JDKAccess.java new file mode 100644 index 0000000000..56caf8e197 --- /dev/null +++ b/java/fory-core/src/main/java25/org/apache/fory/platform/internal/_JDKAccess.java @@ -0,0 +1,913 @@ +/* + * 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.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectStreamClass; +import java.lang.invoke.CallSite; +import java.lang.invoke.LambdaConversionException; +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.invoke.SerializedLambda; +import java.lang.invoke.VarHandle; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Consumer; +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.memory.MemoryBuffer; +import org.apache.fory.platform.GraalvmSupport; +import org.apache.fory.platform.JdkVersion; +import org.apache.fory.type.TypeUtils; +import org.apache.fory.util.ExceptionUtils; +import org.apache.fory.util.Preconditions; +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 sun.misc.Unsafe; + +/** JDK internals access for the JDK25 multi-release runtime. */ +// CHECKSTYLE.OFF:TypeName +public class _JDKAccess { + // CHECKSTYLE.ON:TypeName + public static final boolean IS_OPEN_J9; + public static final Unsafe UNSAFE = null; + public static final boolean JDK_INTERNAL_FIELD_ACCESS; + public static final boolean JDK_LANG_FIELD_ACCESS; + public static final boolean JDK_STRING_FIELD_ACCESS; + public static final boolean JDK_BYTE_ARRAY_STREAM_FIELD_ACCESS; + public static final boolean JDK_OBJECT_STREAM_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; + public static final Class _INNER_UNSAFE_CLASS = null; + public static final Object _INNER_UNSAFE = null; + + public static Unsafe unsafe() { + return UNSAFE; + } + + private static final ClassValueCache lookupCache = ClassValueCache.newClassKeyCache(32); + + public static final boolean STRING_VALUE_FIELD_IS_CHARS; + public static final boolean STRING_VALUE_FIELD_IS_BYTES; + public static final boolean STRING_HAS_COUNT_OFFSET; + public static final long STRING_VALUE_FIELD_OFFSET = -1; + public static final long STRING_COUNT_FIELD_OFFSET = -1; + public static final long STRING_OFFSET_FIELD_OFFSET = -1; + public static final long STRING_CODER_FIELD_OFFSET = -1; + private static final VarHandle STRING_VALUE_HANDLE; + private static final VarHandle STRING_CODER_HANDLE; + private static final VarHandle STRING_COUNT_HANDLE; + private static final VarHandle STRING_OFFSET_HANDLE; + private static final VarHandle BAS_BUF_HANDLE; + private static final VarHandle BAS_COUNT_HANDLE; + private static final VarHandle BIS_BUF_HANDLE; + private static final VarHandle BIS_POS_HANDLE; + private static final VarHandle BIS_COUNT_HANDLE; + private static final VarHandle OSC_WRITE_OBJECT_METHOD_HANDLE; + private static final VarHandle OSC_READ_OBJECT_METHOD_HANDLE; + private static final VarHandle OSC_READ_OBJECT_NO_DATA_METHOD_HANDLE; + private static final VarHandle OSC_WRITE_REPLACE_METHOD_HANDLE; + private static final VarHandle OSC_READ_RESOLVE_METHOD_HANDLE; + + static { + String jmvName = System.getProperty("java.vm.name", ""); + IS_OPEN_J9 = jmvName.contains("OpenJ9"); + } + + static { + try { + Field valueField = String.class.getDeclaredField("value"); + STRING_VALUE_FIELD_IS_CHARS = valueField.getType() == char[].class; + STRING_VALUE_FIELD_IS_BYTES = valueField.getType() == byte[].class; + Field countField = getStringFieldNullable("count"); + Field offsetField = getStringFieldNullable("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; + } else { + STRING_HAS_COUNT_OFFSET = false; + } + + StringHandles stringHandles = initStringHandles(valueField.getType(), countField, offsetField); + StreamHandles streamHandles = initStreamHandles(); + ObjectStreamHandles objectStreamHandles = initObjectStreamHandles(); + + JDK_LANG_FIELD_ACCESS = canOpen(String.class); + JDK_STRING_FIELD_ACCESS = stringHandles != null; + JDK_BYTE_ARRAY_STREAM_FIELD_ACCESS = streamHandles != null; + JDK_OBJECT_STREAM_FIELD_ACCESS = objectStreamHandles != null; + JDK_COLLECTION_FIELD_ACCESS = canOpen("java.util.Collections$SynchronizedCollection"); + JDK_CONCURRENT_FIELD_ACCESS = + canOpen(ArrayBlockingQueue.class) && canOpen(LinkedBlockingQueue.class); + JDK_PROXY_FIELD_ACCESS = canOpen(Proxy.class); + JDK_INTERNAL_FIELD_ACCESS = + JDK_STRING_FIELD_ACCESS + && JDK_BYTE_ARRAY_STREAM_FIELD_ACCESS + && JDK_OBJECT_STREAM_FIELD_ACCESS; + + STRING_VALUE_HANDLE = stringHandles == null ? null : stringHandles.value; + STRING_CODER_HANDLE = stringHandles == null ? null : stringHandles.coder; + STRING_COUNT_HANDLE = stringHandles == null ? null : stringHandles.count; + STRING_OFFSET_HANDLE = stringHandles == null ? null : stringHandles.offset; + BAS_BUF_HANDLE = streamHandles == null ? null : streamHandles.basBuf; + BAS_COUNT_HANDLE = streamHandles == null ? null : streamHandles.basCount; + BIS_BUF_HANDLE = streamHandles == null ? null : streamHandles.bisBuf; + BIS_POS_HANDLE = streamHandles == null ? null : streamHandles.bisPos; + BIS_COUNT_HANDLE = streamHandles == null ? null : streamHandles.bisCount; + OSC_WRITE_OBJECT_METHOD_HANDLE = + objectStreamHandles == null ? null : objectStreamHandles.writeObjectMethod; + OSC_READ_OBJECT_METHOD_HANDLE = + objectStreamHandles == null ? null : objectStreamHandles.readObjectMethod; + OSC_READ_OBJECT_NO_DATA_METHOD_HANDLE = + objectStreamHandles == null ? null : objectStreamHandles.readObjectNoDataMethod; + OSC_WRITE_REPLACE_METHOD_HANDLE = + objectStreamHandles == null ? null : objectStreamHandles.writeReplaceMethod; + OSC_READ_RESOLVE_METHOD_HANDLE = + objectStreamHandles == null ? null : objectStreamHandles.readResolveMethod; + } 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 StringHandles initStringHandles( + Class stringValueType, Field countField, Field offsetField) { + try { + Lookup stringLookup = MethodHandles.privateLookupIn(String.class, MethodHandles.lookup()); + return new StringHandles( + stringLookup.findVarHandle(String.class, "value", stringValueType), + STRING_VALUE_FIELD_IS_BYTES + ? 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 ignored) { + return null; + } + } + + private static StreamHandles initStreamHandles() { + try { + Lookup basLookup = + MethodHandles.privateLookupIn(ByteArrayOutputStream.class, MethodHandles.lookup()); + Lookup bisLookup = + MethodHandles.privateLookupIn(ByteArrayInputStream.class, MethodHandles.lookup()); + return new StreamHandles( + basLookup.findVarHandle(ByteArrayOutputStream.class, "buf", byte[].class), + basLookup.findVarHandle(ByteArrayOutputStream.class, "count", int.class), + bisLookup.findVarHandle(ByteArrayInputStream.class, "buf", byte[].class), + bisLookup.findVarHandle(ByteArrayInputStream.class, "pos", int.class), + bisLookup.findVarHandle(ByteArrayInputStream.class, "count", int.class)); + } catch (Throwable ignored) { + return null; + } + } + + private static ObjectStreamHandles initObjectStreamHandles() { + try { + Lookup oscLookup = + MethodHandles.privateLookupIn(ObjectStreamClass.class, MethodHandles.lookup()); + return new ObjectStreamHandles( + oscLookup.findVarHandle(ObjectStreamClass.class, "writeObjectMethod", Method.class), + oscLookup.findVarHandle(ObjectStreamClass.class, "readObjectMethod", Method.class), + oscLookup.findVarHandle(ObjectStreamClass.class, "readObjectNoDataMethod", Method.class), + oscLookup.findVarHandle(ObjectStreamClass.class, "writeReplaceMethod", Method.class), + oscLookup.findVarHandle(ObjectStreamClass.class, "readResolveMethod", Method.class)); + } catch (Throwable ignored) { + return null; + } + } + + private static boolean canOpen(String className) { + try { + return canOpen(Class.forName(className)); + } catch (Throwable ignored) { + return false; + } + } + + private static boolean canOpen(Class type) { + try { + MethodHandles.privateLookupIn(type, MethodHandles.lookup()); + return true; + } catch (Throwable ignored) { + return false; + } + } + + private static class StringHandles { + private final VarHandle value; + private final VarHandle coder; + private final VarHandle count; + private final VarHandle offset; + + private StringHandles(VarHandle value, VarHandle coder, VarHandle count, VarHandle offset) { + this.value = value; + this.coder = coder; + this.count = count; + this.offset = offset; + } + } + + private static class StreamHandles { + private final VarHandle basBuf; + private final VarHandle basCount; + private final VarHandle bisBuf; + private final VarHandle bisPos; + private final VarHandle bisCount; + + private StreamHandles( + VarHandle basBuf, + VarHandle basCount, + VarHandle bisBuf, + VarHandle bisPos, + VarHandle bisCount) { + this.basBuf = basBuf; + this.basCount = basCount; + this.bisBuf = bisBuf; + this.bisPos = bisPos; + this.bisCount = bisCount; + } + } + + private static class ObjectStreamHandles { + private final VarHandle writeObjectMethod; + private final VarHandle readObjectMethod; + private final VarHandle readObjectNoDataMethod; + private final VarHandle writeReplaceMethod; + private final VarHandle readResolveMethod; + + private ObjectStreamHandles( + VarHandle writeObjectMethod, + VarHandle readObjectMethod, + VarHandle readObjectNoDataMethod, + VarHandle writeReplaceMethod, + VarHandle readResolveMethod) { + this.writeObjectMethod = writeObjectMethod; + this.readObjectMethod = readObjectMethod; + this.readObjectNoDataMethod = readObjectNoDataMethod; + this.writeReplaceMethod = writeReplaceMethod; + this.readResolveMethod = readResolveMethod; + } + } + + // The root native-image configuration names these root lazy helpers. Keep same-named JDK25 + // shadows so multi-release class lookup does not fall back to the root Unsafe offset helpers. + private static class StringCoderField {} + + private static class ByteArrayStreamFields {} + + public static Object getStringValue(String value) { + checkStringAccess("String.value"); + return STRING_VALUE_HANDLE.get(value); + } + + public static byte getStringCoder(String value) { + checkStringAccess("String.coder"); + if (STRING_CODER_HANDLE == null) { + throw new UnsupportedOperationException("String.coder is not available on this JDK"); + } + return (byte) STRING_CODER_HANDLE.get(value); + } + + public static int getStringOffset(String value) { + checkStringAccess("String.offset"); + if (STRING_OFFSET_HANDLE == null) { + throw new UnsupportedOperationException("String.offset is not available on this JDK"); + } + return (int) STRING_OFFSET_HANDLE.get(value); + } + + public static int getStringCount(String value) { + checkStringAccess("String.count"); + if (STRING_COUNT_HANDLE == null) { + throw new UnsupportedOperationException("String.count is not available on this JDK"); + } + return (int) STRING_COUNT_HANDLE.get(value); + } + + // CHECKSTYLE.OFF:MethodName + + public static Lookup _trustedLookup(Class objectClass) { + // CHECKSTYLE.ON:MethodName + if (GraalvmSupport.isGraalBuildTime()) { + return _Lookup._trustedLookup(objectClass); + } + return lookupCache.get(objectClass, () -> _Lookup._trustedLookup(objectClass)); + } + + public static MethodHandle readResolveHandle(Class cls, Method method) + throws IllegalAccessException { + try { + return _trustedLookup(cls).unreflect(method); + } catch (IllegalArgumentException e) { + if (cls != SerializedLambda.class) { + throw e; + } + // JDK25 rejects SerializedLambda itself as a privateLookupIn target. Reflective access still + // honors the same java.base/java.lang.invoke open requirement and avoids serializer-level + // versioning. + try { + method.setAccessible(true); + } catch (RuntimeException inaccessible) { + throw new IllegalStateException( + "SerializedLambda readResolve requires java.base/java.lang.invoke to be open to " + + "org.apache.fory.core,org.apache.fory.format", + inaccessible); + } + return MethodHandles.lookup().unreflect(method); + } + } + + private static final byte LATIN1 = 0; + private static final Byte LATIN1_BOXED = LATIN1; + private static final byte UTF16 = 1; + private static final Byte UTF16_BOXED = UTF16; + private static final Lookup STRING_LOOK_UP = + JDK_STRING_FIELD_ACCESS ? _trustedLookup(String.class) : null; + private static final MethodHandle STRING_ZERO_COPY_CTR_HANDLE = + JDK_STRING_FIELD_ACCESS ? getJavaStringZeroCopyCtrHandle() : null; + private static final BiFunction CHARS_STRING_ZERO_COPY_CTR = + JDK_STRING_FIELD_ACCESS ? getCharsStringZeroCopyCtr() : null; + private static final BiFunction BYTES_STRING_ZERO_COPY_CTR = + JDK_STRING_FIELD_ACCESS ? getBytesStringZeroCopyCtr() : null; + private static final Function LATIN_BYTES_STRING_ZERO_COPY_CTR = + JDK_STRING_FIELD_ACCESS ? getLatinBytesStringZeroCopyCtr() : null; + + public static String newCharsStringZeroCopy(char[] data) { + if (!JDK_STRING_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"); + } + if (CHARS_STRING_ZERO_COPY_CTR == null) { + return newCharsStringByHandle(data); + } + return CHARS_STRING_ZERO_COPY_CTR.apply(data, Boolean.TRUE); + } + + private static String newCharsStringByHandle(char[] data) { + MethodHandle handle = STRING_ZERO_COPY_CTR_HANDLE; + if (handle == null) { + return new String(data); + } + try { + return (String) handle.invokeExact(data, true); + } catch (Throwable ignored) { + return new String(data); + } + } + + public static String newBytesStringZeroCopy(byte coder, byte[] data) { + if (!JDK_STRING_FIELD_ACCESS) { + return newBytesStringSlow(coder, data); + } + if (coder == LATIN1) { + if (LATIN_BYTES_STRING_ZERO_COPY_CTR != null) { + return LATIN_BYTES_STRING_ZERO_COPY_CTR.apply(data); + } else if (BYTES_STRING_ZERO_COPY_CTR == null) { + return newBytesStringByHandle(coder, data); + } + return BYTES_STRING_ZERO_COPY_CTR.apply(data, LATIN1_BOXED); + } else if (coder == UTF16) { + if (BYTES_STRING_ZERO_COPY_CTR == null) { + return newBytesStringByHandle(coder, data); + } + return BYTES_STRING_ZERO_COPY_CTR.apply(data, UTF16_BOXED); + } else { + if (BYTES_STRING_ZERO_COPY_CTR == null) { + return newBytesStringSlow(coder, data); + } + return BYTES_STRING_ZERO_COPY_CTR.apply(data, coder); + } + } + + private static String newBytesStringSlow(byte coder, byte[] data) { + if (coder == LATIN1) { + return new String(data, StandardCharsets.ISO_8859_1); + } else if (coder == UTF16) { + char[] chars = new char[data.length >> 1]; + for (int i = 0, j = 0; i < data.length; i += 2) { + chars[j++] = (char) ((data[i] & 0xff) | ((data[i + 1] & 0xff) << 8)); + } + return new String(chars); + } else { + return new String(data, StandardCharsets.UTF_8); + } + } + + private static String newBytesStringByHandle(byte coder, byte[] data) { + MethodHandle handle = STRING_ZERO_COPY_CTR_HANDLE; + if (handle == null) { + return newBytesStringSlow(coder, data); + } + try { + return (String) handle.invokeExact(data, coder); + } catch (Throwable ignored) { + return newBytesStringSlow(coder, data); + } + } + + private static BiFunction getCharsStringZeroCopyCtr() { + if (!STRING_VALUE_FIELD_IS_CHARS) { + return null; + } + MethodHandle handle = STRING_ZERO_COPY_CTR_HANDLE; + if (handle == null) { + return null; + } + try { + CallSite callSite = + LambdaMetafactory.metafactory( + STRING_LOOK_UP, + "apply", + MethodType.methodType(BiFunction.class), + handle.type().generic(), + handle, + handle.type()); + return (BiFunction) callSite.getTarget().invokeExact(); + } catch (Throwable e) { + return null; + } + } + + private static BiFunction getBytesStringZeroCopyCtr() { + if (!STRING_VALUE_FIELD_IS_BYTES) { + return null; + } + MethodHandle handle = STRING_ZERO_COPY_CTR_HANDLE; + if (handle == null) { + return null; + } + try { + MethodType instantiatedMethodType = + MethodType.methodType(handle.type().returnType(), new Class[] {byte[].class, Byte.class}); + CallSite callSite = + LambdaMetafactory.metafactory( + STRING_LOOK_UP, + "apply", + MethodType.methodType(BiFunction.class), + handle.type().generic(), + handle, + instantiatedMethodType); + return (BiFunction) callSite.getTarget().invokeExact(); + } catch (Throwable e) { + return null; + } + } + + private static Function getLatinBytesStringZeroCopyCtr() { + if (!STRING_VALUE_FIELD_IS_BYTES || STRING_LOOK_UP == null) { + return null; + } + try { + Class clazz = Class.forName("java.lang.StringCoding"); + Lookup caller = STRING_LOOK_UP.in(clazz); + MethodHandle handle = + caller.findStatic( + clazz, "newStringLatin1", MethodType.methodType(String.class, byte[].class)); + return makeFunction(caller, handle, Function.class); + } catch (Throwable e) { + return null; + } + } + + private static MethodHandle getJavaStringZeroCopyCtrHandle() { + Preconditions.checkArgument(JdkVersion.MAJOR_VERSION >= 8); + if (STRING_LOOK_UP == null) { + return null; + } + try { + if (STRING_VALUE_FIELD_IS_CHARS) { + return STRING_LOOK_UP.findConstructor( + String.class, MethodType.methodType(void.class, char[].class, boolean.class)); + } else { + return STRING_LOOK_UP.findConstructor( + String.class, MethodType.methodType(void.class, byte[].class, byte.class)); + } + } catch (Exception e) { + return null; + } + } + + public static void wrap(ByteArrayOutputStream stream, MemoryBuffer buffer) { + Preconditions.checkNotNull(stream); + checkByteArrayStreamAccess("ByteArrayOutputStream"); + byte[] buf = (byte[]) BAS_BUF_HANDLE.get(stream); + int count = (int) BAS_COUNT_HANDLE.get(stream); + buffer.pointTo(buf, 0, buf.length); + buffer.writerIndex(count); + } + + public static void wrap(MemoryBuffer buffer, ByteArrayOutputStream stream) { + Preconditions.checkNotNull(stream); + checkByteArrayStreamAccess("ByteArrayOutputStream"); + byte[] bytes = buffer.getHeapMemory(); + Preconditions.checkNotNull(bytes); + BAS_BUF_HANDLE.set(stream, bytes); + BAS_COUNT_HANDLE.set(stream, buffer.writerIndex()); + } + + public static void wrap(ByteArrayInputStream stream, MemoryBuffer buffer) { + Preconditions.checkNotNull(stream); + checkByteArrayStreamAccess("ByteArrayInputStream"); + byte[] buf = (byte[]) BIS_BUF_HANDLE.get(stream); + int count = (int) BIS_COUNT_HANDLE.get(stream); + int pos = (int) BIS_POS_HANDLE.get(stream); + buffer.pointTo(buf, 0, count); + buffer.readerIndex(pos); + } + + public static Method getObjectStreamClassWriteObjectMethod(ObjectStreamClass objectStreamClass) { + checkObjectStreamAccess("ObjectStreamClass.writeObjectMethod"); + return (Method) OSC_WRITE_OBJECT_METHOD_HANDLE.get(objectStreamClass); + } + + public static Method getObjectStreamClassReadObjectMethod(ObjectStreamClass objectStreamClass) { + checkObjectStreamAccess("ObjectStreamClass.readObjectMethod"); + return (Method) OSC_READ_OBJECT_METHOD_HANDLE.get(objectStreamClass); + } + + public static Method getObjectStreamClassReadObjectNoDataMethod( + ObjectStreamClass objectStreamClass) { + checkObjectStreamAccess("ObjectStreamClass.readObjectNoDataMethod"); + return (Method) OSC_READ_OBJECT_NO_DATA_METHOD_HANDLE.get(objectStreamClass); + } + + public static Method getObjectStreamClassWriteReplaceMethod( + ObjectStreamClass objectStreamClass) { + checkObjectStreamAccess("ObjectStreamClass.writeReplaceMethod"); + return (Method) OSC_WRITE_REPLACE_METHOD_HANDLE.get(objectStreamClass); + } + + public static Method getObjectStreamClassReadResolveMethod(ObjectStreamClass objectStreamClass) { + checkObjectStreamAccess("ObjectStreamClass.readResolveMethod"); + return (Method) OSC_READ_RESOLVE_METHOD_HANDLE.get(objectStreamClass); + } + + private static void checkStringAccess(String target) { + if (!JDK_STRING_FIELD_ACCESS) { + throw new UnsupportedOperationException( + target + + " private access is unavailable; open java.base/java.lang to " + + "org.apache.fory.core,org.apache.fory.format"); + } + } + + private static void checkByteArrayStreamAccess(String target) { + if (!JDK_BYTE_ARRAY_STREAM_FIELD_ACCESS) { + throw new UnsupportedOperationException( + target + + " private access is unavailable; open java.base/java.io to " + + "org.apache.fory.core,org.apache.fory.format"); + } + } + + private static void checkObjectStreamAccess(String target) { + if (!JDK_OBJECT_STREAM_FIELD_ACCESS) { + throw new UnsupportedOperationException( + target + + " private access is unavailable; open java.base/java.io to " + + "org.apache.fory.core,org.apache.fory.format"); + } + } + + public static T tryMakeFunction( + Lookup lookup, MethodHandle handle, Class functionInterface) { + try { + return makeFunction(lookup, handle, functionInterface); + } catch (Throwable e) { + ExceptionUtils.ignore(e); + throw new IllegalStateException(); + } + } + + private static final MethodType jdkFunctionMethodType = + MethodType.methodType(Object.class, Object.class); + + @SuppressWarnings("unchecked") + public static Function makeJDKFunction(Lookup lookup, MethodHandle handle) { + return makeJDKFunction(lookup, handle, jdkFunctionMethodType); + } + + @SuppressWarnings("unchecked") + public static Function makeJDKFunction( + Lookup lookup, MethodHandle handle, MethodType methodType) { + try { + CallSite callSite = + LambdaMetafactory.metafactory( + lookup, + "apply", + MethodType.methodType(Function.class), + methodType, + handle, + boxedMethodType(handle.type())); + return (Function) callSite.getTarget().invokeExact(); + } catch (Throwable e) { + throw ExceptionUtils.throwException(e); + } + } + + private static final MethodType jdkConsumerMethodType = + MethodType.methodType(void.class, Object.class); + + @SuppressWarnings("unchecked") + public static Consumer makeJDKConsumer(Lookup lookup, MethodHandle handle) { + try { + CallSite callSite = + LambdaMetafactory.metafactory( + lookup, + "accept", + MethodType.methodType(Consumer.class), + jdkConsumerMethodType, + handle, + boxedMethodType(handle.type())); + return (Consumer) callSite.getTarget().invokeExact(); + } catch (Throwable e) { + throw ExceptionUtils.throwException(e); + } + } + + private static final MethodType jdkBiConsumerMethodType = + MethodType.methodType(void.class, Object.class, Object.class); + + @SuppressWarnings("unchecked") + public static BiConsumer makeJDKBiConsumer(Lookup lookup, MethodHandle handle) { + try { + CallSite callSite = + LambdaMetafactory.metafactory( + lookup, + "accept", + MethodType.methodType(BiConsumer.class), + jdkBiConsumerMethodType, + handle, + boxedMethodType(handle.type())); + return (BiConsumer) callSite.getTarget().invokeExact(); + } catch (Throwable e) { + throw ExceptionUtils.throwException(e); + } + } + + private static MethodType boxedMethodType(MethodType methodType) { + Class[] paramTypes = new Class[methodType.parameterCount()]; + for (int i = 0; i < paramTypes.length; i++) { + Class t = methodType.parameterType(i); + if (t.isPrimitive()) { + t = TypeUtils.wrap(t); + } + paramTypes[i] = t; + } + return MethodType.methodType(methodType.returnType(), paramTypes); + } + + @SuppressWarnings("unchecked") + public static T makeFunction(Lookup lookup, MethodHandle handle, Method methodToImpl) { + MethodType instantiatedMethodType = boxedMethodType(handle.type()); + MethodType methodToImplType = + MethodType.methodType(methodToImpl.getReturnType(), methodToImpl.getParameterTypes()); + try { + CallSite callSite = + LambdaMetafactory.metafactory( + lookup, + methodToImpl.getName(), + MethodType.methodType(methodToImpl.getDeclaringClass()), + methodToImplType, + handle, + instantiatedMethodType); + return (T) callSite.getTarget().invokeExact(); + } catch (Throwable e) { + throw ExceptionUtils.throwException(e); + } + } + + public static T makeFunction(Lookup lookup, MethodHandle handle, Class functionInterface) { + String invokedName = "apply"; + try { + Method method = null; + Method[] methods = functionInterface.getMethods(); + for (Method interfaceMethod : methods) { + if (interfaceMethod.getName().equals(invokedName)) { + method = interfaceMethod; + break; + } + } + if (method == null) { + Preconditions.checkArgument(methods.length == 1); + method = methods[0]; + invokedName = method.getName(); + } + MethodType interfaceType = + MethodType.methodType(method.getReturnType(), method.getParameterTypes()); + CallSite callSite = + LambdaMetafactory.metafactory( + lookup, + invokedName, + MethodType.methodType(functionInterface), + interfaceType, + handle, + interfaceType); + return (T) callSite.getTarget().invoke(); + } catch (Throwable e) { + throw ExceptionUtils.throwException(e); + } + } + + private static final Map, Tuple2, String>> methodMap = new HashMap<>(); + + static { + methodMap.put(boolean.class, Tuple2.of(Predicate.class, "test")); + methodMap.put(byte.class, Tuple2.of(ToByteFunction.class, "applyAsByte")); + methodMap.put(char.class, Tuple2.of(ToCharFunction.class, "applyAsChar")); + methodMap.put(short.class, Tuple2.of(ToShortFunction.class, "applyAsShort")); + methodMap.put(int.class, Tuple2.of(ToIntFunction.class, "applyAsInt")); + methodMap.put(long.class, Tuple2.of(ToLongFunction.class, "applyAsLong")); + methodMap.put(float.class, Tuple2.of(ToFloatFunction.class, "applyAsFloat")); + methodMap.put(double.class, Tuple2.of(ToDoubleFunction.class, "applyAsDouble")); + } + + public static Tuple2, String> getterMethodInfo(Class type) { + Tuple2, String> info = methodMap.get(type); + if (info == null) { + return Tuple2.of(Function.class, "apply"); + } + return info; + } + + public static Object makeGetterFunction( + MethodHandles.Lookup lookup, MethodHandle handle, Class returnType) { + Tuple2, String> methodInfo = methodMap.get(returnType); + MethodType factoryType; + if (methodInfo == null) { + methodInfo = Tuple2.of(Function.class, "apply"); + factoryType = jdkFunctionMethodType; + } else { + factoryType = MethodType.methodType(returnType, Object.class); + } + try { + CallSite callSite = + LambdaMetafactory.metafactory( + lookup, + methodInfo.f1, + MethodType.methodType(methodInfo.f0), + factoryType, + handle, + handle.type()); + return callSite.getTarget().invoke(); + } catch (LambdaConversionException e) { + return makeGetterFallback(handle, returnType); + } catch (ClassNotFoundException | NoClassDefFoundError e) { + return makeGetterFunction(lookup, handle, Object.class); + } catch (Throwable e) { + throw ExceptionUtils.throwException(e); + } + } + + private static Object makeGetterFallback(MethodHandle handle, Class returnType) { + if (returnType == boolean.class) { + return (Predicate) + value -> { + try { + return (boolean) handle.invoke(value); + } catch (Throwable e) { + throw ExceptionUtils.throwException(e); + } + }; + } else if (returnType == byte.class) { + return (ToByteFunction) + value -> { + try { + return (byte) handle.invoke(value); + } catch (Throwable e) { + throw ExceptionUtils.throwException(e); + } + }; + } else if (returnType == char.class) { + return (ToCharFunction) + value -> { + try { + return (char) handle.invoke(value); + } catch (Throwable e) { + throw ExceptionUtils.throwException(e); + } + }; + } else if (returnType == short.class) { + return (ToShortFunction) + value -> { + try { + return (short) handle.invoke(value); + } catch (Throwable e) { + throw ExceptionUtils.throwException(e); + } + }; + } else if (returnType == int.class) { + return (ToIntFunction) + value -> { + try { + return (int) handle.invoke(value); + } catch (Throwable e) { + throw ExceptionUtils.throwException(e); + } + }; + } else if (returnType == long.class) { + return (ToLongFunction) + value -> { + try { + return (long) handle.invoke(value); + } catch (Throwable e) { + throw ExceptionUtils.throwException(e); + } + }; + } else if (returnType == float.class) { + return (ToFloatFunction) + value -> { + try { + return (float) handle.invoke(value); + } catch (Throwable e) { + throw ExceptionUtils.throwException(e); + } + }; + } else if (returnType == double.class) { + return (ToDoubleFunction) + value -> { + try { + return (double) handle.invoke(value); + } catch (Throwable e) { + throw ExceptionUtils.throwException(e); + } + }; + } + return (Function) + value -> { + try { + return handle.invoke(value); + } catch (Throwable e) { + throw ExceptionUtils.throwException(e); + } + }; + } + + public static Object getModule(Class cls) { + Preconditions.checkArgument(JdkVersion.MAJOR_VERSION >= 9); + return cls.getModule(); + } + + public static Object addReads(Object thisModule, Object otherModule) { + Preconditions.checkArgument(JdkVersion.MAJOR_VERSION >= 9); + return ((Module) thisModule).addReads((Module) otherModule); + } + + public static Lookup privateLookupIn(Class targetClass, Lookup caller) { + return _Lookup.privateLookupIn(targetClass, caller); + } +} 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..b7a0b8272f --- /dev/null +++ b/java/fory-core/src/main/java25/org/apache/fory/platform/internal/_Lookup.java @@ -0,0 +1,74 @@ +/* + * 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.MethodHandles; +import java.lang.invoke.MethodHandles.Lookup; + +// CHECKSTYLE.OFF:TypeName +class _Lookup { + // CHECKSTYLE.ON:TypeName + static final Lookup IMPL_LOOKUP = MethodHandles.lookup(); + + // CHECKSTYLE.OFF:MethodName + public static Lookup _trustedLookup(Class objectClass) { + // CHECKSTYLE.ON:MethodName + return privateLookupIn(objectClass, MethodHandles.lookup()); + } + + public static Lookup privateLookupIn(Class targetClass, Lookup caller) { + try { + Module foryModule = _Lookup.class.getModule(); + Module targetModule = targetClass.getModule(); + if (foryModule != targetModule) { + foryModule.addReads(targetModule); + } + return MethodHandles.privateLookupIn(targetClass, caller); + } catch (IllegalAccessException e) { + throw new IllegalStateException(privateAccessMessage(targetClass), e); + } + } + + /** + * 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(privateAccessMessage(lookup.lookupClass()), e); + } + } + + private static String privateAccessMessage(Class targetClass) { + Module module = targetClass.getModule(); + Package pkg = targetClass.getPackage(); + String packageName = pkg == null ? "" : pkg.getName(); + return "Private lookup for " + + targetClass.getName() + + " requires package " + + packageName + + " in module " + + module.getName() + + " to be open to org.apache.fory.core,org.apache.fory.format"; + } +} diff --git a/java/fory-core/src/main/java25/org/apache/fory/reflect/FieldAccessor.java b/java/fory-core/src/main/java25/org/apache/fory/reflect/FieldAccessor.java new file mode 100644 index 0000000000..1c06e64634 --- /dev/null +++ b/java/fory-core/src/main/java25/org/apache/fory/reflect/FieldAccessor.java @@ -0,0 +1,1642 @@ +/* + * 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.invoke.MethodType; +import java.lang.invoke.VarHandle; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +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.platform.internal._JDKAccess; +import org.apache.fory.type.TypeUtils; +import org.apache.fory.util.Preconditions; +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; + +/** Field accessor for primitive types and object types. */ +@SuppressWarnings({"unchecked", "rawtypes"}) +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; + // Kept to preserve the root FieldAccessor shape for already compiled internal subclasses. + // JDK25 access is field-owned through VarHandle/MethodHandle and intentionally never uses it. + protected final long fieldOffset; + private final int accessKind; + + public FieldAccessor(Field field) { + this(field, -1); + } + + protected FieldAccessor(Field field, long fieldOffset) { + this.field = field; + this.fieldOffset = fieldOffset; + Preconditions.checkNotNull(field); + this.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); + + public void set(Object obj, Object value) { + throw new UnsupportedOperationException("Unsupported for field " + field); + } + + public final 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 final void copyObject(Object sourceObject, Object targetObject) { + putObject(targetObject, getObject(sourceObject)); + } + + public Field getField() { + return field; + } + + public boolean getBoolean(Object targetObject) { + return (Boolean) get(targetObject); + } + + public void putBoolean(Object targetObject, boolean value) { + set(targetObject, value); + } + + public byte getByte(Object targetObject) { + return (Byte) get(targetObject); + } + + public void putByte(Object targetObject, byte value) { + set(targetObject, value); + } + + public char getChar(Object targetObject) { + return (Character) get(targetObject); + } + + public void putChar(Object targetObject, char value) { + set(targetObject, value); + } + + public short getShort(Object targetObject) { + return (Short) get(targetObject); + } + + public void putShort(Object targetObject, short value) { + set(targetObject, value); + } + + public int getInt(Object targetObject) { + return (Integer) get(targetObject); + } + + public void putInt(Object targetObject, int value) { + set(targetObject, value); + } + + public long getLong(Object targetObject) { + return (Long) get(targetObject); + } + + public void putLong(Object targetObject, long value) { + set(targetObject, value); + } + + public float getFloat(Object targetObject) { + return (Float) get(targetObject); + } + + public void putFloat(Object targetObject, float value) { + set(targetObject, value); + } + + public double getDouble(Object targetObject) { + return (Double) get(targetObject); + } + + public void putDouble(Object targetObject, double value) { + set(targetObject, value); + } + + public final void putObject(Object targetObject, Object object) { + set(targetObject, object); + } + + public final Object getObject(Object targetObject) { + return get(targetObject); + } + + void checkObj(Object obj) { + if (!this.field.getDeclaringClass().isAssignableFrom(obj.getClass())) { + throw new IllegalArgumentException("Illegal class " + obj.getClass()); + } + } + + @Override + public String toString() { + return field.toString(); + } + + 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 static FieldAccessor createAccessor(Field field) { + Preconditions.checkArgument(!Modifier.isStatic(field.getModifiers()), field); + if (RecordUtils.isRecord(field.getDeclaringClass())) { + if (AndroidSupport.IS_ANDROID || GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { + return new ReflectiveRecordFieldAccessor(field); + } + return createRecordAccessor(field); + } + 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() || GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { + return new GeneratedAccessor(field); + } + FieldAccessor hiddenAccessor = HiddenFieldAccessorFactory.create(field); + if (hiddenAccessor != null) { + return hiddenAccessor; + } + return createVarHandleAccessor(field); + } + + public static FieldAccessor createStaticAccessor(Field field) { + Preconditions.checkArgument(Modifier.isStatic(field.getModifiers()), field); + if (AndroidSupport.IS_ANDROID) { + field.setAccessible(true); + return new ReflectiveStaticFieldAccessor(field); + } + return createVarHandleAccessor(field); + } + + private static FieldAccessor createVarHandleAccessor(Field 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); + } + } + + private static FieldAccessor createRecordAccessor(Field field) { + MethodHandle getter = recordGetter(field); + if (field.getType() == boolean.class) { + return new BooleanGetter(field, getter); + } else if (field.getType() == byte.class) { + return new ByteGetter(field, getter); + } else if (field.getType() == char.class) { + return new CharGetter(field, getter); + } else if (field.getType() == short.class) { + return new ShortGetter(field, getter); + } else if (field.getType() == int.class) { + return new IntGetter(field, getter); + } else if (field.getType() == long.class) { + return new LongGetter(field, getter); + } else if (field.getType() == float.class) { + return new FloatGetter(field, getter); + } else if (field.getType() == double.class) { + return new DoubleGetter(field, getter); + } else { + return new ObjectGetter(field, getter); + } + } + + private static VarHandle fieldHandle(Field field) { + try { + if (canUsePublicField(field)) { + try { + return findFieldHandle(MethodHandles.publicLookup(), field); + } catch (IllegalAccessException ignored) { + // The package may be opened but not exported. Fall through to privateLookupIn so + // --add-opens still enables access for named-module users. + } + } + return findFieldHandle(privateLookup(field), field); + } catch (IllegalAccessException e) { + throw accessFailure(field, e); + } catch (NoSuchFieldException e) { + throw new IllegalStateException("Failed to create VarHandle for field " + field, e); + } + } + + private static MethodHandle recordGetter(Field field) { + try { + if (Modifier.isPublic(field.getDeclaringClass().getModifiers())) { + try { + return findRecordGetter(MethodHandles.publicLookup(), field); + } catch (IllegalAccessException ignored) { + // The package may be opened but not exported. Fall through to privateLookupIn. + } + } + return findRecordGetter(privateLookup(field), field); + } catch (IllegalAccessException e) { + throw accessFailure(field, e); + } catch (NoSuchMethodException e) { + throw new IllegalStateException("Failed to find record accessor for field " + field, e); + } + } + + private static VarHandle findFieldHandle(MethodHandles.Lookup lookup, Field field) + throws IllegalAccessException, NoSuchFieldException { + if (Modifier.isStatic(field.getModifiers())) { + return lookup.findStaticVarHandle( + field.getDeclaringClass(), field.getName(), field.getType()); + } + return lookup.findVarHandle(field.getDeclaringClass(), field.getName(), field.getType()); + } + + private static MethodHandle findRecordGetter(MethodHandles.Lookup lookup, Field field) + throws IllegalAccessException, NoSuchMethodException { + return lookup.findVirtual( + field.getDeclaringClass(), field.getName(), MethodType.methodType(field.getType())); + } + + private static MethodHandles.Lookup privateLookup(Field field) { + Class declaringClass = field.getDeclaringClass(); + return _JDKAccess.privateLookupIn(declaringClass, MethodHandles.lookup()); + } + + private static boolean canUsePublicField(Field field) { + return Modifier.isPublic(field.getModifiers()) + && Modifier.isPublic(field.getDeclaringClass().getModifiers()); + } + + private static IllegalStateException accessFailure(Field field, Throwable cause) { + Class declaringClass = field.getDeclaringClass(); + Module targetModule = declaringClass.getModule(); + Package targetPackage = declaringClass.getPackage(); + String packageName = targetPackage == null ? "" : targetPackage.getName(); + String openTarget = + moduleName(targetModule) + (packageName.isEmpty() ? "" : "/" + packageName); + return new IllegalStateException( + "Cannot access field " + + field + + " because package " + + packageName + + " in module " + + moduleName(targetModule) + + " is not open to " + + moduleName(FieldAccessor.class.getModule()) + + ". For named modules, open the package with --add-opens=" + + openTarget + + "=org.apache.fory.core,org.apache.fory.format", + cause); + } + + private static String moduleName(Module module) { + String name = module.getName(); + return name == null ? "" : name; + } + + private static UnsupportedOperationException unsupportedWrite(Field field, Throwable cause) { + return new UnsupportedOperationException( + "Field cannot be written through supported JDK access APIs: " + field, cause); + } + + private static IllegalStateException finalMutationFailure(Field field, Throwable cause) { + return new IllegalStateException( + "Cannot write final field " + + field + + ". On JDK25+, start the JVM with " + + "--enable-final-field-mutation=org.apache.fory.core and open the declaring " + + "package to org.apache.fory.core,org.apache.fory.format.", + cause); + } + + private static RuntimeException getterFailure(Field field, Throwable cause) { + return new RuntimeException("Failed to read record field: " + field, cause); + } + + private static RuntimeException accessorFailure(Field field, Throwable cause) { + return new RuntimeException("Failed to access field: " + field, cause); + } + + 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 abstract static class VarHandleAccessor extends FieldAccessor { + protected final VarHandle handle; + protected final boolean isStatic; + protected final boolean isFinal; + protected volatile MethodHandle finalSetter; + + VarHandleAccessor(Field field) { + super(field, -1); + handle = fieldHandle(field); + isStatic = Modifier.isStatic(field.getModifiers()); + isFinal = Modifier.isFinal(field.getModifiers()); + } + + private static MethodHandle createFinalSetter(Field field) { + try { + field.setAccessible(true); + return privateLookup(field).unreflectSetter(field); + } catch (IllegalAccessException | RuntimeException e) { + throw finalMutationFailure(field, e); + } + } + + private MethodHandle finalSetter(Throwable cause) { + if (isStatic || !isFinal) { + throw unsupportedWrite(field, cause); + } + MethodHandle setter = finalSetter; + if (setter == null) { + setter = createFinalSetter(field); + finalSetter = setter; + } + return setter; + } + + protected void setFinal(Object obj, Object value, Throwable cause) { + MethodHandle setter = finalSetter(cause); + checkObj(obj); + try { + setter.invoke(obj, value); + } catch (Throwable e) { + throw finalMutationFailure(field, e); + } + } + + protected void setFinalBoolean(Object obj, boolean value, Throwable cause) { + MethodHandle setter = finalSetter(cause); + checkObj(obj); + try { + setter.invoke(obj, value); + } catch (Throwable e) { + throw finalMutationFailure(field, e); + } + } + + protected void setFinalByte(Object obj, byte value, Throwable cause) { + MethodHandle setter = finalSetter(cause); + checkObj(obj); + try { + setter.invoke(obj, value); + } catch (Throwable e) { + throw finalMutationFailure(field, e); + } + } + + protected void setFinalChar(Object obj, char value, Throwable cause) { + MethodHandle setter = finalSetter(cause); + checkObj(obj); + try { + setter.invoke(obj, value); + } catch (Throwable e) { + throw finalMutationFailure(field, e); + } + } + + protected void setFinalShort(Object obj, short value, Throwable cause) { + MethodHandle setter = finalSetter(cause); + checkObj(obj); + try { + setter.invoke(obj, value); + } catch (Throwable e) { + throw finalMutationFailure(field, e); + } + } + + protected void setFinalInt(Object obj, int value, Throwable cause) { + MethodHandle setter = finalSetter(cause); + checkObj(obj); + try { + setter.invoke(obj, value); + } catch (Throwable e) { + throw finalMutationFailure(field, e); + } + } + + protected void setFinalLong(Object obj, long value, Throwable cause) { + MethodHandle setter = finalSetter(cause); + checkObj(obj); + try { + setter.invoke(obj, value); + } catch (Throwable e) { + throw finalMutationFailure(field, e); + } + } + + protected void setFinalFloat(Object obj, float value, Throwable cause) { + MethodHandle setter = finalSetter(cause); + checkObj(obj); + try { + setter.invoke(obj, value); + } catch (Throwable e) { + throw finalMutationFailure(field, e); + } + } + + protected void setFinalDouble(Object obj, double value, Throwable cause) { + MethodHandle setter = finalSetter(cause); + checkObj(obj); + try { + setter.invoke(obj, value); + } catch (Throwable e) { + throw finalMutationFailure(field, e); + } + } + } + + /** Primitive boolean accessor. */ + public static class BooleanAccessor extends VarHandleAccessor { + public BooleanAccessor(Field field) { + super(field); + Preconditions.checkArgument(field.getType() == boolean.class); + } + + @Override + public Object get(Object obj) { + return getBoolean(obj); + } + + @Override + public boolean getBoolean(Object obj) { + if (isStatic) { + return (boolean) handle.get(); + } + checkObj(obj); + return (boolean) handle.get(obj); + } + + @Override + public void set(Object obj, Object value) { + putBoolean(obj, (Boolean) value); + } + + @Override + public void putBoolean(Object obj, boolean value) { + if (isFinal) { + setFinalBoolean(obj, value, null); + return; + } + try { + if (isStatic) { + handle.set(value); + } else { + checkObj(obj); + handle.set(obj, value); + } + } catch (UnsupportedOperationException e) { + setFinalBoolean(obj, value, e); + } + } + } + + public static class BooleanGetter extends FieldGetter { + private final Predicate getter; + private final MethodHandle getterHandle; + + public BooleanGetter(Field field, Predicate getter) { + super(field, getter); + this.getter = getter; + getterHandle = null; + Preconditions.checkArgument(field.getType() == boolean.class); + } + + private BooleanGetter(Field field, MethodHandle getter) { + super(field, getter); + this.getter = null; + getterHandle = getter; + Preconditions.checkArgument(field.getType() == boolean.class); + } + + @Override + public Boolean get(Object obj) { + return getBoolean(obj); + } + + @Override + public boolean getBoolean(Object obj) { + checkObj(obj); + if (getterHandle == null) { + return getter.test(obj); + } + try { + return (boolean) getterHandle.invoke(obj); + } catch (Throwable e) { + throw getterFailure(field, e); + } + } + } + + /** Primitive byte accessor. */ + public static class ByteAccessor extends VarHandleAccessor { + public ByteAccessor(Field field) { + super(field); + Preconditions.checkArgument(field.getType() == byte.class); + } + + @Override + public Byte get(Object obj) { + return getByte(obj); + } + + @Override + public byte getByte(Object obj) { + if (isStatic) { + return (byte) handle.get(); + } + checkObj(obj); + return (byte) handle.get(obj); + } + + @Override + public void set(Object obj, Object value) { + putByte(obj, (Byte) value); + } + + @Override + public void putByte(Object obj, byte value) { + if (isFinal) { + setFinalByte(obj, value, null); + return; + } + try { + if (isStatic) { + handle.set(value); + } else { + checkObj(obj); + handle.set(obj, value); + } + } catch (UnsupportedOperationException e) { + setFinalByte(obj, value, e); + } + } + } + + public static class ByteGetter extends FieldGetter { + + private final ToByteFunction getter; + private final MethodHandle getterHandle; + + public ByteGetter(Field field, ToByteFunction getter) { + super(field, getter); + this.getter = getter; + getterHandle = null; + Preconditions.checkArgument(field.getType() == byte.class); + } + + private ByteGetter(Field field, MethodHandle getter) { + super(field, getter); + this.getter = null; + getterHandle = getter; + Preconditions.checkArgument(field.getType() == byte.class); + } + + @Override + public Byte get(Object obj) { + return getByte(obj); + } + + @Override + public byte getByte(Object obj) { + checkObj(obj); + if (getterHandle == null) { + return getter.applyAsByte(obj); + } + try { + return (byte) getterHandle.invoke(obj); + } catch (Throwable e) { + throw getterFailure(field, e); + } + } + } + + /** Primitive char accessor. */ + public static class CharAccessor extends VarHandleAccessor { + public CharAccessor(Field field) { + super(field); + Preconditions.checkArgument(field.getType() == char.class); + } + + @Override + public Character get(Object obj) { + return getChar(obj); + } + + @Override + public char getChar(Object obj) { + if (isStatic) { + return (char) handle.get(); + } + checkObj(obj); + return (char) handle.get(obj); + } + + @Override + public void set(Object obj, Object value) { + putChar(obj, (Character) value); + } + + @Override + public void putChar(Object obj, char value) { + if (isFinal) { + setFinalChar(obj, value, null); + return; + } + try { + if (isStatic) { + handle.set(value); + } else { + checkObj(obj); + handle.set(obj, value); + } + } catch (UnsupportedOperationException e) { + setFinalChar(obj, value, e); + } + } + } + + public static class CharGetter extends FieldGetter { + private final ToCharFunction getter; + private final MethodHandle getterHandle; + + public CharGetter(Field field, ToCharFunction getter) { + super(field, getter); + this.getter = getter; + getterHandle = null; + Preconditions.checkArgument(field.getType() == char.class); + } + + private CharGetter(Field field, MethodHandle getter) { + super(field, getter); + this.getter = null; + getterHandle = getter; + Preconditions.checkArgument(field.getType() == char.class); + } + + @Override + public Character get(Object obj) { + return getChar(obj); + } + + @Override + public char getChar(Object obj) { + checkObj(obj); + if (getterHandle == null) { + return getter.applyAsChar(obj); + } + try { + return (char) getterHandle.invoke(obj); + } catch (Throwable e) { + throw getterFailure(field, e); + } + } + } + + /** Primitive short accessor. */ + public static class ShortAccessor extends VarHandleAccessor { + public ShortAccessor(Field field) { + super(field); + Preconditions.checkArgument(field.getType() == short.class); + } + + @Override + public Short get(Object obj) { + return getShort(obj); + } + + @Override + public short getShort(Object obj) { + if (isStatic) { + return (short) handle.get(); + } + checkObj(obj); + return (short) handle.get(obj); + } + + @Override + public void set(Object obj, Object value) { + putShort(obj, (Short) value); + } + + @Override + public void putShort(Object obj, short value) { + if (isFinal) { + setFinalShort(obj, value, null); + return; + } + try { + if (isStatic) { + handle.set(value); + } else { + checkObj(obj); + handle.set(obj, value); + } + } catch (UnsupportedOperationException e) { + setFinalShort(obj, value, e); + } + } + } + + public static class ShortGetter extends FieldGetter { + private final ToShortFunction getter; + private final MethodHandle getterHandle; + + public ShortGetter(Field field, ToShortFunction getter) { + super(field, getter); + this.getter = getter; + getterHandle = null; + Preconditions.checkArgument(field.getType() == short.class); + } + + private ShortGetter(Field field, MethodHandle getter) { + super(field, getter); + this.getter = null; + getterHandle = getter; + Preconditions.checkArgument(field.getType() == short.class); + } + + @Override + public Short get(Object obj) { + return getShort(obj); + } + + @Override + public short getShort(Object obj) { + checkObj(obj); + if (getterHandle == null) { + return getter.applyAsShort(obj); + } + try { + return (short) getterHandle.invoke(obj); + } catch (Throwable e) { + throw getterFailure(field, e); + } + } + } + + /** Primitive int accessor. */ + public static class IntAccessor extends VarHandleAccessor { + public IntAccessor(Field field) { + super(field); + Preconditions.checkArgument(field.getType() == int.class); + } + + @Override + public Integer get(Object obj) { + return getInt(obj); + } + + @Override + public int getInt(Object obj) { + if (isStatic) { + return (int) handle.get(); + } + checkObj(obj); + return (int) handle.get(obj); + } + + @Override + public void set(Object obj, Object value) { + putInt(obj, (Integer) value); + } + + @Override + public void putInt(Object obj, int value) { + if (isFinal) { + setFinalInt(obj, value, null); + return; + } + try { + if (isStatic) { + handle.set(value); + } else { + checkObj(obj); + handle.set(obj, value); + } + } catch (UnsupportedOperationException e) { + setFinalInt(obj, value, e); + } + } + } + + public static class IntGetter extends FieldGetter { + private final ToIntFunction getter; + private final MethodHandle getterHandle; + + public IntGetter(Field field, ToIntFunction getter) { + super(field, getter); + this.getter = getter; + getterHandle = null; + Preconditions.checkArgument(field.getType() == int.class); + } + + private IntGetter(Field field, MethodHandle getter) { + super(field, getter); + this.getter = null; + getterHandle = getter; + Preconditions.checkArgument(field.getType() == int.class); + } + + @Override + public Integer get(Object obj) { + return getInt(obj); + } + + @Override + public int getInt(Object obj) { + checkObj(obj); + if (getterHandle == null) { + return getter.applyAsInt(obj); + } + try { + return (int) getterHandle.invoke(obj); + } catch (Throwable e) { + throw getterFailure(field, e); + } + } + } + + /** Primitive long accessor. */ + public static class LongAccessor extends VarHandleAccessor { + public LongAccessor(Field field) { + super(field); + Preconditions.checkArgument(field.getType() == long.class); + } + + @Override + public Long get(Object obj) { + return getLong(obj); + } + + @Override + public long getLong(Object obj) { + if (isStatic) { + return (long) handle.get(); + } + checkObj(obj); + return (long) handle.get(obj); + } + + @Override + public void set(Object obj, Object value) { + putLong(obj, (Long) value); + } + + @Override + public void putLong(Object obj, long value) { + if (isFinal) { + setFinalLong(obj, value, null); + return; + } + try { + if (isStatic) { + handle.set(value); + } else { + checkObj(obj); + handle.set(obj, value); + } + } catch (UnsupportedOperationException e) { + setFinalLong(obj, value, e); + } + } + } + + public static class LongGetter extends FieldGetter { + private final ToLongFunction getter; + private final MethodHandle getterHandle; + + public LongGetter(Field field, ToLongFunction getter) { + super(field, getter); + this.getter = getter; + getterHandle = null; + Preconditions.checkArgument(field.getType() == long.class); + } + + private LongGetter(Field field, MethodHandle getter) { + super(field, getter); + this.getter = null; + getterHandle = getter; + Preconditions.checkArgument(field.getType() == long.class); + } + + @Override + public Long get(Object obj) { + return getLong(obj); + } + + @Override + public long getLong(Object obj) { + checkObj(obj); + if (getterHandle == null) { + return getter.applyAsLong(obj); + } + try { + return (long) getterHandle.invoke(obj); + } catch (Throwable e) { + throw getterFailure(field, e); + } + } + } + + /** Primitive float accessor. */ + public static class FloatAccessor extends VarHandleAccessor { + public FloatAccessor(Field field) { + super(field); + Preconditions.checkArgument(field.getType() == float.class); + } + + @Override + public Object get(Object obj) { + return getFloat(obj); + } + + @Override + public float getFloat(Object obj) { + if (isStatic) { + return (float) handle.get(); + } + checkObj(obj); + return (float) handle.get(obj); + } + + @Override + public void set(Object obj, Object value) { + putFloat(obj, (Float) value); + } + + @Override + public void putFloat(Object obj, float value) { + if (isFinal) { + setFinalFloat(obj, value, null); + return; + } + try { + if (isStatic) { + handle.set(value); + } else { + checkObj(obj); + handle.set(obj, value); + } + } catch (UnsupportedOperationException e) { + setFinalFloat(obj, value, e); + } + } + } + + public static class FloatGetter extends FieldGetter { + private final ToFloatFunction getter; + private final MethodHandle getterHandle; + + public FloatGetter(Field field, ToFloatFunction getter) { + super(field, getter); + this.getter = getter; + getterHandle = null; + Preconditions.checkArgument(field.getType() == float.class); + } + + private FloatGetter(Field field, MethodHandle getter) { + super(field, getter); + this.getter = null; + getterHandle = getter; + Preconditions.checkArgument(field.getType() == float.class); + } + + @Override + public Float get(Object obj) { + return getFloat(obj); + } + + @Override + public float getFloat(Object obj) { + checkObj(obj); + if (getterHandle == null) { + return getter.applyAsFloat(obj); + } + try { + return (float) getterHandle.invoke(obj); + } catch (Throwable e) { + throw getterFailure(field, e); + } + } + } + + /** Primitive double accessor. */ + public static class DoubleAccessor extends VarHandleAccessor { + public DoubleAccessor(Field field) { + super(field); + Preconditions.checkArgument(field.getType() == double.class); + } + + @Override + public Object get(Object obj) { + return getDouble(obj); + } + + @Override + public double getDouble(Object obj) { + if (isStatic) { + return (double) handle.get(); + } + checkObj(obj); + return (double) handle.get(obj); + } + + @Override + public void set(Object obj, Object value) { + putDouble(obj, (Double) value); + } + + @Override + public void putDouble(Object obj, double value) { + if (isFinal) { + setFinalDouble(obj, value, null); + return; + } + try { + if (isStatic) { + handle.set(value); + } else { + checkObj(obj); + handle.set(obj, value); + } + } catch (UnsupportedOperationException e) { + setFinalDouble(obj, value, e); + } + } + } + + public static class DoubleGetter extends FieldGetter { + private final ToDoubleFunction getter; + private final MethodHandle getterHandle; + + public DoubleGetter(Field field, ToDoubleFunction getter) { + super(field, getter); + this.getter = getter; + getterHandle = null; + Preconditions.checkArgument(field.getType() == double.class); + } + + private DoubleGetter(Field field, MethodHandle getter) { + super(field, getter); + this.getter = null; + getterHandle = getter; + Preconditions.checkArgument(field.getType() == double.class); + } + + @Override + public Double get(Object obj) { + return getDouble(obj); + } + + @Override + public double getDouble(Object obj) { + checkObj(obj); + if (getterHandle == null) { + return getter.applyAsDouble(obj); + } + try { + return (double) getterHandle.invoke(obj); + } catch (Throwable e) { + throw getterFailure(field, e); + } + } + } + + /** Object accessor. */ + public static class ObjectAccessor extends VarHandleAccessor { + public ObjectAccessor(Field field) { + super(field); + Preconditions.checkArgument(!TypeUtils.isPrimitive(field.getType())); + } + + @Override + public Object get(Object obj) { + if (isStatic) { + return handle.get(); + } + checkObj(obj); + return handle.get(obj); + } + + @Override + public void set(Object obj, Object value) { + if (isFinal) { + setFinal(obj, value, null); + return; + } + try { + if (isStatic) { + handle.set(value); + } else { + checkObj(obj); + handle.set(obj, value); + } + } catch (UnsupportedOperationException e) { + setFinal(obj, value, e); + } + } + } + + public static class ObjectGetter extends FieldGetter { + private final Function getter; + private final MethodHandle getterHandle; + + public ObjectGetter(Field field, Function getter) { + super(field, getter); + this.getter = getter; + getterHandle = null; + Preconditions.checkArgument(!field.getType().isPrimitive(), field); + } + + private ObjectGetter(Field field, MethodHandle getter) { + super(field, getter); + this.getter = null; + getterHandle = getter; + Preconditions.checkArgument(!field.getType().isPrimitive(), field); + } + + @Override + public Object get(Object obj) { + checkObj(obj); + if (getterHandle == null) { + return getter.apply(obj); + } + try { + return getterHandle.invoke(obj); + } catch (Throwable e) { + throw getterFailure(field, e); + } + } + } + + static final class ReflectiveStaticFieldAccessor extends FieldAccessor { + ReflectiveStaticFieldAccessor(Field field) { + super(field, -1); + } + + @Override + public Object get(Object obj) { + try { + return field.get(null); + } catch (IllegalAccessException | IllegalArgumentException e) { + throw new ForyException("Failed to read static field reflectively: " + field, e); + } + } + + @Override + public void set(Object obj, Object value) { + try { + field.set(null, value); + } catch (IllegalAccessException | IllegalArgumentException e) { + throw new ForyException("Failed to write static field reflectively: " + field, e); + } + } + } + + static final class StaticObjectAccessor extends ObjectAccessor { + StaticObjectAccessor(Field field) { + super(field); + Preconditions.checkArgument(Modifier.isStatic(field.getModifiers()), field); + } + } + + static final class GeneratedAccessor extends VarHandleAccessor { + GeneratedAccessor(Field field) { + super(field); + } + + @Override + public Object get(Object obj) { + try { + if (isStatic) { + return handle.get(); + } + checkObj(obj); + return handle.get(obj); + } catch (RuntimeException e) { + throw accessorFailure(field, e); + } + } + + @Override + public void set(Object obj, Object value) { + if (isFinal) { + setFinal(obj, value, null); + return; + } + try { + if (isStatic) { + handle.set(value); + } else { + checkObj(obj); + handle.set(obj, value); + } + } catch (UnsupportedOperationException e) { + setFinal(obj, value, e); + } catch (RuntimeException e) { + throw accessorFailure(field, e); + } + } + + @Override + public boolean getBoolean(Object obj) { + try { + if (isStatic) { + return (boolean) handle.get(); + } + checkObj(obj); + return (boolean) handle.get(obj); + } catch (RuntimeException e) { + throw accessorFailure(field, e); + } + } + + @Override + public void putBoolean(Object obj, boolean value) { + if (isFinal) { + setFinalBoolean(obj, value, null); + return; + } + try { + if (isStatic) { + handle.set(value); + } else { + checkObj(obj); + handle.set(obj, value); + } + } catch (UnsupportedOperationException e) { + setFinalBoolean(obj, value, e); + } catch (RuntimeException e) { + throw accessorFailure(field, e); + } + } + + @Override + public byte getByte(Object obj) { + try { + if (isStatic) { + return (byte) handle.get(); + } + checkObj(obj); + return (byte) handle.get(obj); + } catch (RuntimeException e) { + throw accessorFailure(field, e); + } + } + + @Override + public void putByte(Object obj, byte value) { + if (isFinal) { + setFinalByte(obj, value, null); + return; + } + try { + if (isStatic) { + handle.set(value); + } else { + checkObj(obj); + handle.set(obj, value); + } + } catch (UnsupportedOperationException e) { + setFinalByte(obj, value, e); + } catch (RuntimeException e) { + throw accessorFailure(field, e); + } + } + + @Override + public char getChar(Object obj) { + try { + if (isStatic) { + return (char) handle.get(); + } + checkObj(obj); + return (char) handle.get(obj); + } catch (RuntimeException e) { + throw accessorFailure(field, e); + } + } + + @Override + public void putChar(Object obj, char value) { + if (isFinal) { + setFinalChar(obj, value, null); + return; + } + try { + if (isStatic) { + handle.set(value); + } else { + checkObj(obj); + handle.set(obj, value); + } + } catch (UnsupportedOperationException e) { + setFinalChar(obj, value, e); + } catch (RuntimeException e) { + throw accessorFailure(field, e); + } + } + + @Override + public short getShort(Object obj) { + try { + if (isStatic) { + return (short) handle.get(); + } + checkObj(obj); + return (short) handle.get(obj); + } catch (RuntimeException e) { + throw accessorFailure(field, e); + } + } + + @Override + public void putShort(Object obj, short value) { + if (isFinal) { + setFinalShort(obj, value, null); + return; + } + try { + if (isStatic) { + handle.set(value); + } else { + checkObj(obj); + handle.set(obj, value); + } + } catch (UnsupportedOperationException e) { + setFinalShort(obj, value, e); + } catch (RuntimeException e) { + throw accessorFailure(field, e); + } + } + + @Override + public int getInt(Object obj) { + try { + if (isStatic) { + return (int) handle.get(); + } + checkObj(obj); + return (int) handle.get(obj); + } catch (RuntimeException e) { + throw accessorFailure(field, e); + } + } + + @Override + public void putInt(Object obj, int value) { + if (isFinal) { + setFinalInt(obj, value, null); + return; + } + try { + if (isStatic) { + handle.set(value); + } else { + checkObj(obj); + handle.set(obj, value); + } + } catch (UnsupportedOperationException e) { + setFinalInt(obj, value, e); + } catch (RuntimeException e) { + throw accessorFailure(field, e); + } + } + + @Override + public long getLong(Object obj) { + try { + if (isStatic) { + return (long) handle.get(); + } + checkObj(obj); + return (long) handle.get(obj); + } catch (RuntimeException e) { + throw accessorFailure(field, e); + } + } + + @Override + public void putLong(Object obj, long value) { + if (isFinal) { + setFinalLong(obj, value, null); + return; + } + try { + if (isStatic) { + handle.set(value); + } else { + checkObj(obj); + handle.set(obj, value); + } + } catch (UnsupportedOperationException e) { + setFinalLong(obj, value, e); + } catch (RuntimeException e) { + throw accessorFailure(field, e); + } + } + + @Override + public float getFloat(Object obj) { + try { + if (isStatic) { + return (float) handle.get(); + } + checkObj(obj); + return (float) handle.get(obj); + } catch (RuntimeException e) { + throw accessorFailure(field, e); + } + } + + @Override + public void putFloat(Object obj, float value) { + if (isFinal) { + setFinalFloat(obj, value, null); + return; + } + try { + if (isStatic) { + handle.set(value); + } else { + checkObj(obj); + handle.set(obj, value); + } + } catch (UnsupportedOperationException e) { + setFinalFloat(obj, value, e); + } catch (RuntimeException e) { + throw accessorFailure(field, e); + } + } + + @Override + public double getDouble(Object obj) { + try { + if (isStatic) { + return (double) handle.get(); + } + checkObj(obj); + return (double) handle.get(obj); + } catch (RuntimeException e) { + throw accessorFailure(field, e); + } + } + + @Override + public void putDouble(Object obj, double value) { + if (isFinal) { + setFinalDouble(obj, value, null); + return; + } + try { + if (isStatic) { + handle.set(value); + } else { + checkObj(obj); + handle.set(obj, value); + } + } catch (UnsupportedOperationException e) { + setFinalDouble(obj, value, e); + } catch (RuntimeException e) { + throw accessorFailure(field, e); + } + } + } +} diff --git a/java/fory-core/src/main/java25/org/apache/fory/reflect/HiddenFieldAccessorFactory.java b/java/fory-core/src/main/java25/org/apache/fory/reflect/HiddenFieldAccessorFactory.java new file mode 100644 index 0000000000..b9d88bbb5d --- /dev/null +++ b/java/fory-core/src/main/java25/org/apache/fory/reflect/HiddenFieldAccessorFactory.java @@ -0,0 +1,559 @@ +/* + * 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.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.fory.platform.internal.DefineClass; + +final class HiddenFieldAccessorFactory { + private static final int JAVA5_CLASS_VERSION = 49; + private static final int ACC_PUBLIC = 0x0001; + private static final int ACC_FINAL = 0x0010; + private static final int ACC_SUPER = 0x0020; + private static final String FIELD_ACCESSOR = "org/apache/fory/reflect/FieldAccessor"; + private static final String FIELD_CTR_DESC = "(Ljava/lang/reflect/Field;)V"; + private static final AtomicInteger IDS = new AtomicInteger(); + + private HiddenFieldAccessorFactory() {} + + static FieldAccessor create(Field field) { + if (Modifier.isStatic(field.getModifiers()) || Modifier.isFinal(field.getModifiers())) { + return null; + } + try { + byte[] bytes = new AccessorClass(field).bytes(); + Class accessorClass = DefineClass.defineHiddenNestmate(field.getDeclaringClass(), bytes); + Constructor constructor = accessorClass.getDeclaredConstructor(Field.class); + constructor.setAccessible(true); + return (FieldAccessor) constructor.newInstance(field); + } catch (ReflectiveOperationException | RuntimeException | LinkageError e) { + return null; + } + } + + private static final class AccessorClass { + private final Field field; + private final Class fieldType; + private final String owner; + private final String thisClass; + private final String fieldDesc; + private final Pool pool = new Pool(); + + private int codeUtf8; + private int initName; + private int initDesc; + private int getName; + private int getDesc; + private int setName; + private int setDesc; + private int fieldRef; + private int ownerClass; + private int thisClassIndex; + private int fieldAccessorClass; + private int superCtr; + + private AccessorClass(Field field) { + this.field = field; + this.fieldType = field.getType(); + owner = internalName(field.getDeclaringClass()); + thisClass = owner + "$ForyFieldAccessor$" + IDS.incrementAndGet(); + fieldDesc = descriptor(fieldType); + } + + private byte[] bytes() { + try { + preparePool(); + List methods = methods(); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + DataOutputStream out = new DataOutputStream(bytes); + out.writeInt(0xCAFEBABE); + out.writeShort(0); + out.writeShort(JAVA5_CLASS_VERSION); + pool.writeTo(out); + out.writeShort(ACC_FINAL | ACC_SUPER); + out.writeShort(thisClassIndex); + out.writeShort(fieldAccessorClass); + out.writeShort(0); + out.writeShort(0); + out.writeShort(methods.size()); + for (MethodDef method : methods) { + method.writeTo(out, codeUtf8); + } + out.writeShort(0); + return bytes.toByteArray(); + } catch (IOException e) { + throw new IllegalStateException("Failed to build field accessor bytecode", e); + } + } + + private void preparePool() { + codeUtf8 = pool.utf8("Code"); + initName = pool.utf8(""); + initDesc = pool.utf8(FIELD_CTR_DESC); + getName = pool.utf8("get"); + getDesc = pool.utf8("(Ljava/lang/Object;)Ljava/lang/Object;"); + setName = pool.utf8("set"); + setDesc = pool.utf8("(Ljava/lang/Object;Ljava/lang/Object;)V"); + thisClassIndex = pool.classInfo(thisClass); + ownerClass = pool.classInfo(owner); + fieldAccessorClass = pool.classInfo(FIELD_ACCESSOR); + superCtr = pool.methodRef(FIELD_ACCESSOR, "", FIELD_CTR_DESC); + fieldRef = pool.fieldRef(owner, field.getName(), fieldDesc); + if (fieldType.isPrimitive()) { + Primitive primitive = Primitive.of(fieldType); + pool.classInfo(primitive.wrapper); + pool.methodRef(primitive.wrapper, "valueOf", primitive.valueOfDesc); + pool.methodRef(primitive.wrapper, primitive.unboxName, primitive.unboxDesc); + } else { + pool.classInfo(castName(fieldType)); + } + } + + private List methods() throws IOException { + List methods = new ArrayList<>(); + methods.add(method(ACC_PUBLIC, initName, initDesc, 2, 2, constructorCode())); + methods.add(method(ACC_PUBLIC, getName, getDesc, 4, 2, objectGetterCode())); + if (fieldType.isPrimitive()) { + Primitive primitive = Primitive.of(fieldType); + methods.add( + method( + ACC_PUBLIC, + pool.utf8(primitive.getterName), + pool.utf8("(Ljava/lang/Object;)" + primitive.desc), + 3, + 2, + primitiveGetterCode(primitive))); + if (!Modifier.isFinal(field.getModifiers())) { + methods.add(method(ACC_PUBLIC, setName, setDesc, 4, 3, objectSetterCode(primitive))); + methods.add( + method( + ACC_PUBLIC, + pool.utf8(primitive.setterName), + pool.utf8("(Ljava/lang/Object;" + primitive.desc + ")V"), + 4, + primitive.maxLocals, + primitiveSetterCode(primitive))); + } + } else { + if (!Modifier.isFinal(field.getModifiers())) { + methods.add(method(ACC_PUBLIC, setName, setDesc, 3, 3, referenceSetterCode())); + } + } + return methods; + } + + private byte[] constructorCode() throws IOException { + Code code = new Code(); + code.u1(0x2A); // aload_0 + code.u1(0x2B); // aload_1 + code.u1(0xB7).u2(superCtr); // invokespecial + code.u1(0xB1); // return + return code.bytes(); + } + + private byte[] objectGetterCode() throws IOException { + Code code = directReadCode(); + if (fieldType.isPrimitive()) { + Primitive primitive = Primitive.of(fieldType); + code.u1(0xB8).u2(pool.methodRef(primitive.wrapper, "valueOf", primitive.valueOfDesc)); + } + code.u1(0xB0); // areturn + return code.bytes(); + } + + private byte[] primitiveGetterCode(Primitive primitive) throws IOException { + Code code = directReadCode(); + code.u1(primitive.returnOpcode); + return code.bytes(); + } + + private Code directReadCode() throws IOException { + Code code = new Code(); + code.u1(0x2B); // aload_1 + code.u1(0xC0).u2(ownerClass); // checkcast + code.u1(0xB4).u2(fieldRef); // getfield + return code; + } + + private byte[] objectSetterCode(Primitive primitive) throws IOException { + Code code = setterPrefix(); + code.u1(0x2C); // aload_2 + code.u1(0xC0).u2(pool.classInfo(primitive.wrapper)); + code.u1(0xB6).u2(pool.methodRef(primitive.wrapper, primitive.unboxName, primitive.unboxDesc)); + code.u1(0xB5).u2(fieldRef); // putfield + code.u1(0xB1); // return + return code.bytes(); + } + + private byte[] primitiveSetterCode(Primitive primitive) throws IOException { + Code code = setterPrefix(); + code.u1(primitive.loadOpcode); + code.u1(0xB5).u2(fieldRef); // putfield + code.u1(0xB1); // return + return code.bytes(); + } + + private byte[] referenceSetterCode() throws IOException { + Code code = setterPrefix(); + code.u1(0x2C); // aload_2 + code.u1(0xC0).u2(pool.classInfo(castName(fieldType))); + code.u1(0xB5).u2(fieldRef); // putfield + code.u1(0xB1); // return + return code.bytes(); + } + + private Code setterPrefix() throws IOException { + Code code = new Code(); + code.u1(0x2B); // aload_1 + code.u1(0xC0).u2(ownerClass); // checkcast + return code; + } + + private MethodDef method( + int access, + int name, + int desc, + int maxStack, + int maxLocals, + byte[] code) + throws IOException { + return new MethodDef(access, name, desc, maxStack, maxLocals, code); + } + } + + private static final class MethodDef { + private final int access; + private final int name; + private final int desc; + private final int maxStack; + private final int maxLocals; + private final byte[] code; + + private MethodDef(int access, int name, int desc, int maxStack, int maxLocals, byte[] code) { + this.access = access; + this.name = name; + this.desc = desc; + this.maxStack = maxStack; + this.maxLocals = maxLocals; + this.code = code; + } + + private void writeTo(DataOutputStream out, int codeUtf8) throws IOException { + out.writeShort(access); + out.writeShort(name); + out.writeShort(desc); + out.writeShort(1); + out.writeShort(codeUtf8); + out.writeInt(12 + code.length); + out.writeShort(maxStack); + out.writeShort(maxLocals); + out.writeInt(code.length); + out.write(code); + out.writeShort(0); + out.writeShort(0); + } + } + + private static String descriptor(Class type) { + if (type == void.class) { + return "V"; + } else if (type == boolean.class) { + return "Z"; + } else if (type == byte.class) { + return "B"; + } else if (type == char.class) { + return "C"; + } else if (type == short.class) { + return "S"; + } else if (type == int.class) { + return "I"; + } else if (type == long.class) { + return "J"; + } else if (type == float.class) { + return "F"; + } else if (type == double.class) { + return "D"; + } else if (type.isArray()) { + return type.getName().replace('.', '/'); + } + return "L" + internalName(type) + ";"; + } + + private static String castName(Class type) { + if (type.isArray()) { + return descriptor(type); + } + return internalName(type); + } + + private static String internalName(Class type) { + return type.getName().replace('.', '/'); + } + + private static final class Code { + private final ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + private final DataOutputStream out = new DataOutputStream(bytes); + + private Code u1(int value) throws IOException { + out.writeByte(value); + return this; + } + + private Code u2(int value) throws IOException { + out.writeShort(value); + return this; + } + + private byte[] bytes() throws IOException { + out.flush(); + return bytes.toByteArray(); + } + } + + private static final class Pool { + private final ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + private final DataOutputStream out = new DataOutputStream(bytes); + private int count = 1; + + private int utf8(String value) { + try { + int index = count++; + out.writeByte(1); + out.writeUTF(value); + return index; + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + private int classInfo(String name) { + try { + int nameIndex = utf8(name); + int index = count++; + out.writeByte(7); + out.writeShort(nameIndex); + return index; + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + private int nameAndType(String name, String desc) { + try { + int nameIndex = utf8(name); + int descIndex = utf8(desc); + int index = count++; + out.writeByte(12); + out.writeShort(nameIndex); + out.writeShort(descIndex); + return index; + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + private int fieldRef(String owner, String name, String desc) { + try { + int ownerIndex = classInfo(owner); + int nameAndTypeIndex = nameAndType(name, desc); + int index = count++; + out.writeByte(9); + out.writeShort(ownerIndex); + out.writeShort(nameAndTypeIndex); + return index; + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + private int methodRef(String owner, String name, String desc) { + try { + int ownerIndex = classInfo(owner); + int nameAndTypeIndex = nameAndType(name, desc); + int index = count++; + out.writeByte(10); + out.writeShort(ownerIndex); + out.writeShort(nameAndTypeIndex); + return index; + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + private void writeTo(DataOutputStream target) throws IOException { + out.flush(); + target.writeShort(count); + bytes.writeTo(target); + } + } + + private enum Primitive { + BOOLEAN( + boolean.class, + "Z", + "java/lang/Boolean", + "(Z)Ljava/lang/Boolean;", + "booleanValue", + "()Z", + "getBoolean", + "putBoolean", + 0x1C, + 0xAC, + 3), + BYTE( + byte.class, + "B", + "java/lang/Byte", + "(B)Ljava/lang/Byte;", + "byteValue", + "()B", + "getByte", + "putByte", + 0x1C, + 0xAC, + 3), + CHAR( + char.class, + "C", + "java/lang/Character", + "(C)Ljava/lang/Character;", + "charValue", + "()C", + "getChar", + "putChar", + 0x1C, + 0xAC, + 3), + SHORT( + short.class, + "S", + "java/lang/Short", + "(S)Ljava/lang/Short;", + "shortValue", + "()S", + "getShort", + "putShort", + 0x1C, + 0xAC, + 3), + INT( + int.class, + "I", + "java/lang/Integer", + "(I)Ljava/lang/Integer;", + "intValue", + "()I", + "getInt", + "putInt", + 0x1C, + 0xAC, + 3), + LONG( + long.class, + "J", + "java/lang/Long", + "(J)Ljava/lang/Long;", + "longValue", + "()J", + "getLong", + "putLong", + 0x20, + 0xAD, + 4), + FLOAT( + float.class, + "F", + "java/lang/Float", + "(F)Ljava/lang/Float;", + "floatValue", + "()F", + "getFloat", + "putFloat", + 0x24, + 0xAE, + 3), + DOUBLE( + double.class, + "D", + "java/lang/Double", + "(D)Ljava/lang/Double;", + "doubleValue", + "()D", + "getDouble", + "putDouble", + 0x28, + 0xAF, + 4); + + private final Class type; + private final String desc; + private final String wrapper; + private final String valueOfDesc; + private final String unboxName; + private final String unboxDesc; + private final String getterName; + private final String setterName; + private final int loadOpcode; + private final int returnOpcode; + private final int maxLocals; + + Primitive( + Class type, + String desc, + String wrapper, + String valueOfDesc, + String unboxName, + String unboxDesc, + String getterName, + String setterName, + int loadOpcode, + int returnOpcode, + int maxLocals) { + this.type = type; + this.desc = desc; + this.wrapper = wrapper; + this.valueOfDesc = valueOfDesc; + this.unboxName = unboxName; + this.unboxDesc = unboxDesc; + this.getterName = getterName; + this.setterName = setterName; + this.loadOpcode = loadOpcode; + this.returnOpcode = returnOpcode; + this.maxLocals = maxLocals; + } + + private static Primitive of(Class type) { + for (Primitive primitive : values()) { + if (primitive.type == type) { + return primitive; + } + } + throw new IllegalArgumentException("Not a primitive field type: " + type); + } + } +} 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..e5ac8063cb --- /dev/null +++ b/java/fory-core/src/main/java25/org/apache/fory/serializer/PlatformStringUtils.java @@ -0,0 +1,139 @@ +/* + * 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.VarHandle; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +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; + +/** JDK25 string internals used by {@link StringSerializer}. */ +final class PlatformStringUtils { + static final boolean JDK_STRING_FIELD_ACCESS = + !AndroidSupport.IS_ANDROID + && !GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE + && _JDKAccess.JDK_STRING_FIELD_ACCESS; + static final boolean STRING_VALUE_FIELD_IS_CHARS = + JDK_STRING_FIELD_ACCESS && _JDKAccess.STRING_VALUE_FIELD_IS_CHARS; + static final boolean STRING_VALUE_FIELD_IS_BYTES = + JDK_STRING_FIELD_ACCESS && _JDKAccess.STRING_VALUE_FIELD_IS_BYTES; + static final boolean STRING_HAS_COUNT_OFFSET = + JDK_STRING_FIELD_ACCESS && _JDKAccess.STRING_HAS_COUNT_OFFSET; + + private static final byte LATIN1 = 0; + private static final byte UTF16 = 1; + 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() {} + + static Object getStringValue(String value) { + return _JDKAccess.getStringValue(value); + } + + static byte getStringCoder(String value) { + return _JDKAccess.getStringCoder(value); + } + + static int getStringOffset(String value) { + return _JDKAccess.getStringOffset(value); + } + + static int getStringCount(String value) { + return _JDKAccess.getStringCount(value); + } + + static String newCharsStringZeroCopy(char[] data) { + if (!JDK_STRING_FIELD_ACCESS) { + return new String(data); + } + return _JDKAccess.newCharsStringZeroCopy(data); + } + + static String newBytesStringZeroCopy(byte coder, byte[] data) { + if (!JDK_STRING_FIELD_ACCESS) { + return newBytesStringSlow(coder, data); + } + return _JDKAccess.newBytesStringZeroCopy(coder, data); + } + + private static String newBytesStringSlow(byte coder, byte[] data) { + if (coder == LATIN1) { + return new String(data, StandardCharsets.ISO_8859_1); + } else if (coder == UTF16) { + char[] chars = new char[data.length >> 1]; + for (int i = 0, j = 0; i < data.length; i += 2) { + chars[j++] = (char) ((data[i] & 0xff) | ((data[i + 1] & 0xff) << 8)); + } + return new String(chars); + } else { + return new String(data, StandardCharsets.UTF_8); + } + } + + 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/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..99024c974e 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,6 +26,7 @@ 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,\ @@ -35,6 +36,7 @@ Args=--initialize-at-build-time=org.apache.fory.annotation.ForyField$Dynamic,\ org.apache.fory.serializer.collection.GuavaCollectionSerializers,\ 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,\ @@ -212,7 +214,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,\ @@ -251,6 +252,7 @@ Args=--initialize-at-build-time=org.apache.fory.annotation.ForyField$Dynamic,\ org.apache.fory.reflect.TypeRef$TypeVariableKey,\ org.apache.fory.reflect.TypeRef,\ org.apache.fory.reflect.ObjectCreators,\ + org.apache.fory.reflect.ObjectCreators$ConstructorObjectCreator,\ org.apache.fory.reflect.ObjectCreators$UnsafeObjectCreator,\ org.apache.fory.reflect.ObjectCreators$DeclaredNoArgCtrObjectCreator,\ org.apache.fory.reflect.ObjectCreators$ParentNoArgCtrObjectCreator,\ @@ -331,11 +333,12 @@ Args=--initialize-at-build-time=org.apache.fory.annotation.ForyField$Dynamic,\ org.apache.fory.serializer.BufferSerializers$ByteBufferSerializer,\ org.apache.fory.serializer.CompatibleSerializer,\ org.apache.fory.serializer.CompatibleLayerSerializer,\ + org.apache.fory.serializer.CompatibleLayerSerializerBase$FieldOnlyCreator,\ org.apache.fory.serializer.EnumSerializer,\ org.apache.fory.serializer.ExceptionSerializers,\ org.apache.fory.serializer.ExceptionSerializers$ExceptionSerializer,\ + org.apache.fory.serializer.ExceptionSerializers$FieldOnlyCreator,\ 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 +399,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 +460,9 @@ 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$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$UnmodifiableCollectionSerializer,\ org.apache.fory.serializer.collection.UnmodifiableSerializers$UnmodifiableMapSerializer,\ org.apache.fory.serializer.collection.UnmodifiableSerializers,\ @@ -492,9 +492,10 @@ 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$FieldOnlyCreator,\ org.apache.fory.serializer.collection.ChildContainerSerializers$ChildMapSerializer,\ org.apache.fory.builder.LayerMarkerClassGenerator,\ org.apache.fory.builder.LayerMarkerClassGenerator$1,\ @@ -534,9 +535,10 @@ 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.platform.internal._JDKAccess$ByteArrayStreamFields,\ + org.apache.fory.platform.internal._JDKAccess$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,\ 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..12684e9326 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 @@ -27,6 +27,7 @@ import com.google.common.collect.HashBasedTable; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import java.beans.ConstructorProperties; import java.io.Serializable; import java.lang.invoke.MethodHandles; import java.math.BigDecimal; @@ -50,7 +51,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; @@ -434,11 +434,22 @@ class A {} } @Data - @AllArgsConstructor private static class IgnoreFields { @Ignore int f1; @Ignore long f2; long f3; + + @ConstructorProperties({"f1", "f2", "f3"}) + IgnoreFields(int f1, long f2, long f3) { + this.f1 = f1; + this.f2 = f2; + this.f3 = f3; + } + + @ConstructorProperties({"f3"}) + IgnoreFields(long f3) { + this.f3 = f3; + } } @Test @@ -451,13 +462,33 @@ public void testIgnoreFields() { } @Data - @AllArgsConstructor private static class ExposeFields { @Expose int f1; @Expose long f2; long f3; @Expose ImmutableMap map1; ImmutableMap map2; + + @ConstructorProperties({"f1", "f2", "f3", "map1", "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; + } + + @ConstructorProperties({"f1", "f2", "map1"}) + ExposeFields(int f1, long f2, ImmutableMap map1) { + this.f1 = f1; + this.f2 = f2; + this.map1 = map1; + } } @Test @@ -474,11 +505,17 @@ public void testExposeFields() { } @Data - @AllArgsConstructor private static class ExposeFields2 { @Expose int f1; @Ignore long f2; long f3; + + @ConstructorProperties({"f1", "f2", "f3"}) + ExposeFields2(int f1, long f2, long f3) { + this.f1 = f1; + this.f2 = f2; + this.f3 = f3; + } } @Test @@ -596,7 +633,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 @@ -674,6 +711,7 @@ static class Struct1 { int f1; String f2; + @ConstructorProperties({"f1", "f2"}) public Struct1(int f1, String f2) { this.f1 = f1; this.f2 = f2; @@ -732,10 +770,15 @@ public void testMaxDepth() { assertThrows(InsecureException.class, () -> fory.deserialize(bytes)); } - @AllArgsConstructor static class MaxDepth { int f1; Object f2; + + @ConstructorProperties({"f1", "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..476caef205 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 @@ -66,11 +66,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()); 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..ee7971e7fe 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,13 @@ 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"; 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..67ed2b8823 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,8 +22,10 @@ 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.*; @@ -31,15 +33,56 @@ 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.ObjectCreators; 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(System.getProperty("java.class.path"), 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); + 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) { + args.add("--add-opens=java.base/java.lang=ALL-UNNAMED"); + args.add("--add-opens=java.base/java.lang.invoke=ALL-UNNAMED"); + args.add("--add-opens=java.base/java.lang.reflect=ALL-UNNAMED"); + args.add("--add-opens=java.base/jdk.internal.reflect=ALL-UNNAMED"); + args.add("--add-opens=java.base/java.util=ALL-UNNAMED"); + args.add("--add-opens=java.base/java.util.concurrent=ALL-UNNAMED"); + args.add("--add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED"); + args.add("--add-opens=java.base/java.io=ALL-UNNAMED"); + args.add("--add-opens=java.base/java.net=ALL-UNNAMED"); + args.add("--add-opens=java.base/java.math=ALL-UNNAMED"); + if (hasInputArg("--sun-misc-unsafe-memory-access=deny")) { + args.add("--sun-misc-unsafe-memory-access=deny"); + } + } + return args; + } + + private static boolean hasInputArg(String arg) { + return ManagementFactory.getRuntimeMXBean().getInputArguments().contains(arg); + } + @SuppressWarnings("unchecked") public static T getFieldValue(Object obj, String fieldName) { return (T) @@ -106,7 +149,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) ObjectCreators.getObjectCreator(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/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..e13bbff62c 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 @@ -21,11 +21,11 @@ import static org.testng.Assert.assertEquals; +import java.beans.ConstructorProperties; import java.io.Serializable; 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 +48,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 +67,34 @@ 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; + @ConstructorProperties({"type", "id"}) 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); + } + + @ConstructorProperties({"nodes", "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..1ed4fbbdaa 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 @@ -23,6 +23,7 @@ import static org.testng.Assert.assertThrows; import static org.testng.Assert.assertTrue; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; @@ -30,8 +31,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 +85,52 @@ public void testBufferWrite() { assertEquals(buffer.readerIndex(), buffer.writerIndex()); } + @Test + public void testByteArrayStreamWrap() { + if (!MemoryUtils.JDK_BYTE_ARRAY_STREAM_FIELD_ACCESS) { + return; + } + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(8); + outputStream.write(new byte[] {1, 2, 3}, 0, 3); + MemoryBuffer buffer = MemoryUtils.buffer(1); + MemoryUtils.wrap(outputStream, buffer); + assertEquals(buffer.writerIndex(), 3); + assertEquals(buffer.getByte(0), (byte) 1); + buffer.writeByte((byte) 4); + MemoryUtils.wrap(buffer, outputStream); + assertEquals(outputStream.size(), 4); + assertEquals(outputStream.toByteArray(), new byte[] {1, 2, 3, 4}); + + ByteArrayInputStream inputStream = new ByteArrayInputStream(new byte[] {5, 6, 7}); + assertEquals(inputStream.read(), 5); + MemoryUtils.wrap(inputStream, buffer); + assertEquals(buffer.readerIndex(), 1); + assertEquals(buffer.readByte(), (byte) 6); + } + + @Test + public void testFromDirectByteBufferRejectsHeapBuffer() { + 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 +232,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 +298,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 +414,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 +463,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 +477,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); 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 98% 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..3e05a00792 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; 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..0b333ec972 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 @@ -27,6 +27,7 @@ import java.nio.charset.StandardCharsets; import lombok.AllArgsConstructor; import org.apache.fory.platform.AndroidSupport; +import org.apache.fory.platform.JdkVersion; import org.apache.fory.reflect.FieldAccessor.GeneratedAccessor; import org.testng.Assert; import org.testng.annotations.Test; @@ -46,14 +47,65 @@ 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); + if (JdkVersion.MAJOR_VERSION >= 25) { + Assert.assertTrue(isHidden(intAccessor.getClass())); + Assert.assertTrue(isHidden(objectAccessor.getClass())); + } + } + + @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"); + } + + private static boolean isHidden(Class cls) throws Exception { + return (Boolean) Class.class.getMethod("isHidden").invoke(cls); } @Test @@ -107,7 +159,6 @@ private static void assertAccessor( 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"); checkEquals(accessor.get(fields), expected, "initial " + fieldName); accessor.set(fields, replacement); checkEquals(accessor.get(fields), replacement, "updated " + fieldName); @@ -137,4 +188,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/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..828289c39e 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 @@ -21,7 +21,6 @@ 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 +36,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 +54,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); } @@ -173,6 +168,9 @@ private static void verifyOutputStreamSerialization(Fory fory) { } private static void verifyMemoryUtilsStreamWrapGuards() { + check( + !MemoryUtils.JDK_INTERNAL_FIELD_ACCESS, + "Android must report JDK internal field access unsupported"); expectUnsupportedAndroidWrap( () -> MemoryUtils.wrap(new ByteArrayOutputStream(), MemoryUtils.buffer(8)), "ByteArrayOutputStream direct wrapping"); 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..a003b55c00 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 @@ -160,6 +160,9 @@ private static void addAddOpens(ArrayList command) { } addAddOpens(command, "java.base/java.io=ALL-UNNAMED"); addAddOpens(command, "java.base/java.lang=ALL-UNNAMED"); + addAddOpens(command, "java.base/java.lang.invoke=ALL-UNNAMED"); + addAddOpens(command, "java.base/java.lang.reflect=ALL-UNNAMED"); + addAddOpens(command, "java.base/jdk.internal.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..56d19e4291 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 @@ -23,6 +23,7 @@ import static org.testng.Assert.assertThrows; import static org.testng.Assert.assertTrue; +import java.beans.ConstructorProperties; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.util.Arrays; @@ -486,6 +487,7 @@ public void testArrayStructZeroCopy(Fory fory) { static class A { final int f1; + @ConstructorProperties({"f1"}) A(int f1) { this.f1 = f1; } @@ -495,6 +497,7 @@ static class A { static class B extends A { final String f2; + @ConstructorProperties({"f1", "f2"}) B(int f1, String f2) { super(f1); this.f2 = f2; @@ -517,6 +520,11 @@ static class GenericArrayWrapper { public GenericArrayWrapper(Class clazz, int capacity) { this.array = (T[]) Array.newInstance(clazz, capacity); } + + @ConstructorProperties({"array"}) + 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/ObjectSerializerTest.java b/java/fory-core/src/test/java/org/apache/fory/serializer/ObjectSerializerTest.java index 162b984c6f..f0b2235db3 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 @@ -21,7 +21,9 @@ import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotSame; +import static org.testng.Assert.assertSame; +import java.beans.ConstructorProperties; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; @@ -30,9 +32,13 @@ import lombok.Data; 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.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.JdkVersion; import org.apache.fory.test.bean.Cyclic; import org.apache.fory.util.Preconditions; import org.testng.Assert; @@ -171,6 +177,572 @@ public void testCopyCircularReference(Fory fory) { assertNotSame(cyclic1, cyclic); } + public static final class ConstructorCycle { + private final String name; + private ConstructorCycle next; + + @ConstructorProperties("name") + public ConstructorCycle(String name) { + this.name = name; + } + } + + public static final class ConstructorCycleBeforeFinal { + @ForyField(id = 0) + private ConstructorCycleBeforeFinal next; + + @ForyField(id = 1) + private final String name; + + @ConstructorProperties("name") + public ConstructorCycleBeforeFinal(String name) { + this.name = name; + } + } + + public static final class ConstructorOrder { + private int id; + private final String name; + + @ConstructorProperties("name") + public ConstructorOrder(String name) { + this.name = name; + } + } + + public static final class ConstructorInterveningRef { + @ForyField(id = 0) + private Object first; + + @ForyField(id = 1) + private final String name; + + @ForyField(id = 2) + private Object second; + + @ConstructorProperties("name") + public ConstructorInterveningRef(String name) { + this.name = name; + } + } + + public static final class ConstructorBackrefRoot { + private final ConstructorBackrefChild child; + + @ConstructorProperties("child") + public ConstructorBackrefRoot(ConstructorBackrefChild child) { + this.child = child; + } + } + + public static final class ConstructorBackrefChild { + private ConstructorBackrefRoot root; + } + + 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; + + @ConstructorProperties("label") + public FinalPostCtorBean(String label) { + id = -1; + this.label = label; + } + + private FinalPostCtorBean(int value, String label) { + id = value; + this.label = label; + } + } + + @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 testConstructorFieldProtocolOrder() { + ConstructorOrder value = new ConstructorOrder("root"); + value.id = 42; + Fory fory = + Fory.builder() + .withXlang(false) + .withRefTracking(true) + .withCodegen(false) + .withNumberCompressed(false) + .requireClassRegistration(false) + .build(); + ObjectSerializer serializer = + new ObjectSerializer<>(fory.getTypeResolver(), ConstructorOrder.class); + MemoryBuffer buffer = MemoryUtils.buffer(32); + withWriteContext(fory, buffer, context -> serializer.write(context, value)); + assertEquals(buffer.readInt32(), 42); + } + + @Test + public void testConstructorFieldProtocolOrderCodegen() { + ConstructorOrder value = new ConstructorOrder("root"); + value.id = 42; + Fory fory = + Fory.builder() + .withXlang(false) + .withRefTracking(true) + .withCodegen(true) + .withNumberCompressed(false) + .requireClassRegistration(false) + .build(); + Serializer serializer = + Serializers.newSerializer( + fory, + ConstructorOrder.class, + CodecUtils.loadOrGenObjectCodecClass(ConstructorOrder.class, fory)); + MemoryBuffer buffer = MemoryUtils.buffer(32); + withWriteContext(fory, buffer, context -> serializer.write(context, value)); + assertEquals(buffer.readInt32(), 42); + } + + @Test + public void testCtorInterveningRef() { + Fory fory = + Fory.builder() + .withXlang(false) + .withRefTracking(true) + .withCodegen(false) + .requireClassRegistration(false) + .build(); + ConstructorInterveningRef newValue = + roundTripWithSerializer( + fory, + new ObjectSerializer<>(fory.getTypeResolver(), ConstructorInterveningRef.class), + newConstructorInterveningRef()); + assertInterveningRef(newValue); + } + + @Test + public void testCtorInterveningRefCodegen() { + Fory fory = + Fory.builder() + .withXlang(false) + .withRefTracking(true) + .withCodegen(true) + .requireClassRegistration(false) + .build(); + Serializer serializer = + Serializers.newSerializer( + fory, + ConstructorInterveningRef.class, + CodecUtils.loadOrGenObjectCodecClass(ConstructorInterveningRef.class, fory)); + ConstructorInterveningRef newValue = + roundTripWithSerializer(fory, serializer, newConstructorInterveningRef()); + assertInterveningRef(newValue); + } + + @Test + public void testCtorInterveningRefCompat() { + Fory fory = + Fory.builder() + .withXlang(false) + .withRefTracking(true) + .withCompatible(true) + .withCodegen(false) + .requireClassRegistration(false) + .build(); + TypeDef typeDef = fory.getTypeResolver().getTypeDef(ConstructorInterveningRef.class, true); + CompatibleSerializer serializer = + new CompatibleSerializer<>( + fory.getTypeResolver(), ConstructorInterveningRef.class, typeDef); + ConstructorInterveningRef newValue = + roundTripWithSerializer(fory, serializer, newConstructorInterveningRef()); + assertInterveningRef(newValue); + } + + @Test + public void testCtorInterveningRefCompatGen() { + Fory fory = + Fory.builder() + .withXlang(false) + .withRefTracking(true) + .withCompatible(true) + .withCodegen(true) + .requireClassRegistration(false) + .build(); + TypeDef typeDef = fory.getTypeResolver().getTypeDef(ConstructorInterveningRef.class, true); + Serializer serializer = + Serializers.newSerializer( + fory, + ConstructorInterveningRef.class, + CodecUtils.loadOrGenCompatibleCodecClass( + fory, ConstructorInterveningRef.class, typeDef)); + ConstructorInterveningRef newValue = + roundTripWithSerializer(fory, serializer, newConstructorInterveningRef()); + assertInterveningRef(newValue); + } + + @Test + public void testConstructorFieldBackrefRejected() { + if (JdkVersion.MAJOR_VERSION < 25) { + return; + } + ConstructorBackrefChild child = new ConstructorBackrefChild(); + ConstructorBackrefRoot value = new ConstructorBackrefRoot(child); + child.root = value; + Fory fory = + Fory.builder() + .withXlang(false) + .withRefTracking(true) + .withCodegen(false) + .requireClassRegistration(false) + .build(); + ObjectSerializer serializer = + new ObjectSerializer<>(fory.getTypeResolver(), ConstructorBackrefRoot.class); + MemoryBuffer buffer = MemoryUtils.buffer(32); + withWriteContext( + fory, + buffer, + context -> { + context.writeRefOrNull(value); + serializer.write(context, value); + }); + Assert.assertThrows( + org.apache.fory.exception.ForyException.class, + () -> + withReadContext( + fory, + buffer, + context -> { + byte tag = context.readRefOrNull(); + Preconditions.checkArgument(tag == Fory.REF_VALUE_FLAG); + context.preserveRefId(); + return serializer.read(context); + })); + } + + @Test + public void testConstructorFieldCycle() { + ConstructorCycle value = new ConstructorCycle("root"); + value.next = value; + Fory fory = + Fory.builder() + .withXlang(false) + .withRefTracking(true) + .withCodegen(false) + .requireClassRegistration(false) + .build(); + ConstructorCycle newValue = + roundTripWithSerializer( + fory, new ObjectSerializer<>(fory.getTypeResolver(), ConstructorCycle.class), value); + assertEquals(newValue.name, value.name); + assertSame(newValue.next, newValue); + } + + @Test + public void testConstructorFieldCycleBeforeFinal() { + ConstructorCycleBeforeFinal value = new ConstructorCycleBeforeFinal("root"); + value.next = value; + Fory fory = + Fory.builder() + .withXlang(false) + .withRefTracking(true) + .withCodegen(false) + .requireClassRegistration(false) + .build(); + ConstructorCycleBeforeFinal newValue = + roundTripWithSerializer( + fory, + new ObjectSerializer<>(fory.getTypeResolver(), ConstructorCycleBeforeFinal.class), + value); + assertEquals(newValue.name, value.name); + assertSame(newValue.next, newValue); + } + + @Test + public void testConstructorFieldCycleCodegen() { + ConstructorCycle value = new ConstructorCycle("root"); + value.next = value; + Fory fory = + Fory.builder() + .withXlang(false) + .withRefTracking(true) + .withCodegen(true) + .requireClassRegistration(false) + .build(); + Serializer serializer = + Serializers.newSerializer( + fory, + ConstructorCycle.class, + CodecUtils.loadOrGenObjectCodecClass(ConstructorCycle.class, fory)); + ConstructorCycle newValue = roundTripWithSerializer(fory, serializer, value); + assertEquals(newValue.name, value.name); + assertSame(newValue.next, newValue); + } + + @Test + public void testConstructorFieldCycleBeforeFinalCodegen() { + ConstructorCycleBeforeFinal value = new ConstructorCycleBeforeFinal("root"); + value.next = value; + Fory fory = + Fory.builder() + .withXlang(false) + .withRefTracking(true) + .withCodegen(true) + .requireClassRegistration(false) + .build(); + Serializer serializer = + Serializers.newSerializer( + fory, + ConstructorCycleBeforeFinal.class, + CodecUtils.loadOrGenObjectCodecClass(ConstructorCycleBeforeFinal.class, fory)); + ConstructorCycleBeforeFinal newValue = roundTripWithSerializer(fory, serializer, value); + assertEquals(newValue.name, value.name); + assertSame(newValue.next, newValue); + } + + @Test + public void testConstructorFieldCycleCompatible() { + ConstructorCycle value = new ConstructorCycle("root"); + value.next = value; + Fory fory = + Fory.builder() + .withXlang(false) + .withRefTracking(true) + .withCompatible(true) + .withCodegen(true) + .requireClassRegistration(false) + .build(); + TypeDef typeDef = fory.getTypeResolver().getTypeDef(ConstructorCycle.class, true); + Serializer serializer = + Serializers.newSerializer( + fory, + ConstructorCycle.class, + CodecUtils.loadOrGenCompatibleCodecClass(fory, ConstructorCycle.class, typeDef)); + ConstructorCycle newValue = roundTripWithSerializer(fory, serializer, value); + assertEquals(newValue.name, value.name); + assertSame(newValue.next, newValue); + } + + @Test + public void testConstructorFieldCycleBeforeFinalCompatible() { + ConstructorCycleBeforeFinal value = new ConstructorCycleBeforeFinal("root"); + value.next = value; + Fory fory = + Fory.builder() + .withXlang(false) + .withRefTracking(true) + .withCompatible(true) + .withCodegen(true) + .requireClassRegistration(false) + .build(); + TypeDef typeDef = fory.getTypeResolver().getTypeDef(ConstructorCycleBeforeFinal.class, true); + Serializer serializer = + Serializers.newSerializer( + fory, + ConstructorCycleBeforeFinal.class, + CodecUtils.loadOrGenCompatibleCodecClass( + fory, ConstructorCycleBeforeFinal.class, typeDef)); + ConstructorCycleBeforeFinal newValue = roundTripWithSerializer(fory, serializer, value); + assertEquals(newValue.name, value.name); + assertSame(newValue.next, newValue); + } + + @Test + public void testConstructorFieldCycleCompatibleNonCodegen() { + ConstructorCycleBeforeFinal value = new ConstructorCycleBeforeFinal("root"); + value.next = value; + Fory fory = + Fory.builder() + .withXlang(false) + .withRefTracking(true) + .withCompatible(true) + .withCodegen(false) + .requireClassRegistration(false) + .build(); + TypeDef typeDef = fory.getTypeResolver().getTypeDef(ConstructorCycleBeforeFinal.class, true); + CompatibleSerializer serializer = + new CompatibleSerializer<>( + fory.getTypeResolver(), ConstructorCycleBeforeFinal.class, typeDef); + ConstructorCycleBeforeFinal newValue = roundTripWithSerializer(fory, serializer, value); + assertEquals(newValue.name, value.name); + assertSame(newValue.next, newValue); + } + + @Test + public void testConstructorFieldBackrefCompatibleRejected() { + if (JdkVersion.MAJOR_VERSION < 25) { + return; + } + ConstructorBackrefChild child = new ConstructorBackrefChild(); + ConstructorBackrefRoot value = new ConstructorBackrefRoot(child); + child.root = value; + Fory fory = + Fory.builder() + .withXlang(false) + .withRefTracking(true) + .withCompatible(true) + .withCodegen(false) + .requireClassRegistration(false) + .build(); + TypeDef typeDef = fory.getTypeResolver().getTypeDef(ConstructorBackrefRoot.class, true); + CompatibleSerializer serializer = + new CompatibleSerializer<>(fory.getTypeResolver(), ConstructorBackrefRoot.class, typeDef); + MemoryBuffer buffer = MemoryUtils.buffer(32); + withWriteContext( + fory, + buffer, + context -> { + context.writeRefOrNull(value); + serializer.write(context, value); + }); + Assert.assertThrows( + org.apache.fory.exception.ForyException.class, + () -> + withReadContext( + fory, + buffer, + context -> { + byte tag = context.readRefOrNull(); + Preconditions.checkArgument(tag == Fory.REF_VALUE_FLAG); + context.preserveRefId(); + return serializer.read(context); + })); + } + + @Test(dataProvider = "foryCopyConfig") + public void testConstructorFieldCycleCopy(Fory fory) { + ConstructorCycle value = new ConstructorCycle("root"); + value.next = value; + ObjectSerializer serializer = + new ObjectSerializer<>(fory.getTypeResolver(), ConstructorCycle.class); + ConstructorCycle newValue = withCopyContext(fory, context -> serializer.copy(context, value)); + assertEquals(newValue.name, value.name); + assertSame(newValue.next, newValue); + } + + 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; + } + + private static ConstructorInterveningRef newConstructorInterveningRef() { + ConstructorInterveningRef value = new ConstructorInterveningRef("root"); + Object shared = new String("shared"); + value.first = shared; + value.second = shared; + return value; + } + + private static void assertInterveningRef(ConstructorInterveningRef value) { + assertEquals(value.name, "root"); + assertEquals(value.first, "shared"); + assertSame(value.second, value.first); + Assert.assertNotSame(value.second, value); + } + @Data public static class A { Integer f1; @@ -202,14 +774,15 @@ public void testSerialization() { public void testAndroidObjectSerializerReflectionPaths() throws Exception { String javaBin = System.getProperty("java.home") + File.separator + "bin" + File.separator + "java"; - Process process = + ProcessBuilder processBuilder = new ProcessBuilder( javaBin, "-cp", System.getProperty("java.class.path"), AndroidObjectSerializerProbe.class.getName()) - .redirectErrorStream(true) - .start(); + .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 d623d609c7..df49ec926d 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 @@ -494,7 +494,7 @@ public void testWriteObjectReplace(Fory fory) throws MalformedURLException { new ObjectStreamSerializer(fory.getTypeResolver(), WriteObjectTestClass4.class)); Assert.assertEquals( - serDeCheckSerializer(fory, new URL("http://test"), "ReplaceResolve"), + serDeCheckSerializer(fory, new URL("http://test"), "URLSerializer"), new URL("http://test")); WriteObjectTestClass4 testClassObj4 = new WriteObjectTestClass4(new char[] {'a', 'b'}); serDeCheckSerializer(fory, testClassObj4, "ObjectStreamSerializer"); 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 027f006174..67f7cac9d9 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 @@ -621,6 +621,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 95c41e6714..3ce6a7f2be 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 @@ -36,8 +36,7 @@ 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.platform.internal._JDKAccess; import org.apache.fory.util.MathUtils; import org.apache.fory.util.StringUtils; import org.testng.Assert; @@ -138,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[]) _JDKAccess.getStringValue(value); int numBytes = MathUtils.doubleExact(value.length()); buffer.writeCharsWithSize(chars); } diff --git a/java/fory-core/src/test/java/org/apache/fory/serializer/URLSerializerTest.java b/java/fory-core/src/test/java/org/apache/fory/serializer/URLSerializerTest.java index 5035051921..5da5aa16c6 100644 --- a/java/fory-core/src/test/java/org/apache/fory/serializer/URLSerializerTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/serializer/URLSerializerTest.java @@ -31,7 +31,7 @@ public class URLSerializerTest extends ForyTestBase { @Test(dataProvider = "javaFory") public void testDefaultWrite(Fory fory) throws MalformedURLException { Assert.assertEquals( - serDeCheckSerializer(fory, new URL("http://test"), "ReplaceResolve"), + serDeCheckSerializer(fory, new URL("http://test"), "URLSerializer"), new URL("http://test")); } 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..0f6cda5edd 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 @@ -85,7 +85,9 @@ public void testAndroidCollectionFeaturePaths() throws Exception { command.add(jvmSubListPayload); command.add(jvmEnumMapPayload); command.add(jvmEmptyEnumMapPayload); - Process process = new ProcessBuilder(command).redirectErrorStream(true).start(); + 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); 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..7eb639c0d1 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 @@ -28,6 +28,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedSet; +import java.beans.ConstructorProperties; import java.io.Externalizable; import java.io.IOException; import java.io.ObjectInput; @@ -881,10 +882,15 @@ public void testCopyOnWriteArrayList(Fory fory) { } @Data - @AllArgsConstructor public static class CollectionViewTestStruct { Collection collection; Set set; + + @ConstructorProperties({"collection", "set"}) + public CollectionViewTestStruct(Collection collection, Set set) { + this.collection = collection; + this.set = set; + } } @Test(dataProvider = "javaFory") @@ -1369,7 +1375,16 @@ 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<>()); + } + + @ConstructorProperties({"data"}) + public TestClassForDefaultCollectionSerializer(List data) { + this.data = data; + } @Override public Iterator iterator() { 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 2db9d5c842..ef69240218 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 @@ -26,6 +26,7 @@ import static org.testng.Assert.assertEquals; import com.google.common.collect.ImmutableMap; +import java.beans.ConstructorProperties; import java.io.Externalizable; import java.io.IOException; import java.io.ObjectInput; @@ -825,7 +826,16 @@ 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<>()); + } + + @ConstructorProperties({"data"}) + public TestClass1ForDefaultMap(Set data) { + this.data = data; + } @Override public Set> entrySet() { @@ -840,7 +850,16 @@ 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<>()); + } + + @ConstructorProperties({"data"}) + public TestClass2ForDefaultMap(Set> data) { + this.data = data; + } @Override public Set> entrySet() { @@ -1177,10 +1196,16 @@ public int hashCode() { } @Data - @AllArgsConstructor public static class LazyMapCollectionFieldStruct { List> mapList; PrivateMap map; + + @ConstructorProperties({"mapList", "map"}) + LazyMapCollectionFieldStruct( + List> mapList, PrivateMap map) { + this.mapList = mapList; + this.map = map; + } } @Data @@ -1387,8 +1412,19 @@ 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<>()); + } + + @ConstructorProperties({"valueMap", "keyMap"}) + 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/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..9711e869a1 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 @@ -32,10 +32,10 @@ import org.apache.fory.context.ReadContext; import org.apache.fory.context.WriteContext; import org.apache.fory.memory.MemoryBuffer; +import org.apache.fory.platform.internal._JDKAccess; 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 { diff --git a/java/fory-format/pom.xml b/java/fory-format/pom.xml index 01cdd773ec..1e9c2adaeb 100644 --- a/java/fory-format/pom.xml +++ b/java/fory-format/pom.xml @@ -105,6 +105,7 @@ + true org.apache.fory.format @@ -114,7 +115,10 @@ org.apache.maven.plugins maven-surefire-plugin - --add-opens=java.base/java.nio=ALL-UNNAMED + + ${argLine} + --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED + @@ -177,6 +181,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 fdb85c8f2f..8273704f76 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; @@ -187,43 +191,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; } @@ -252,7 +286,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; @@ -263,48 +297,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/type/CustomTypeEncoderRegistry.java b/java/fory-format/src/main/java/org/apache/fory/format/type/CustomTypeEncoderRegistry.java index b952087970..f1c97f7a82 100644 --- a/java/fory-format/src/main/java/org/apache/fory/format/type/CustomTypeEncoderRegistry.java +++ b/java/fory-format/src/main/java/org/apache/fory/format/type/CustomTypeEncoderRegistry.java @@ -32,8 +32,8 @@ import org.apache.fory.codegen.JaninoUtils; import org.apache.fory.format.encoder.CustomCodec; import org.apache.fory.format.encoder.CustomCollectionFactory; +import org.apache.fory.platform.internal.DefineClass; import org.apache.fory.reflect.TypeRef; -import org.apache.fory.util.unsafe.DefineClass; /** * Keep a registry of custom codecs and collection factories. In order to deliver peak performance, 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-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..0b7983a120 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,10 +21,10 @@ import static org.apache.fory.integration_tests.TestUtils.serDeCheck; +import java.beans.ConstructorProperties; 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; @@ -94,9 +94,13 @@ public void testImmutableMapStruct() { } @Data - @AllArgsConstructor public static class Pojo { List> data; + + @ConstructorProperties("data") + public Pojo(List> data) { + this.data = data; + } } @DataProvider diff --git a/java/fory-simd/pom.xml b/java/fory-simd/pom.xml index 675e183748..0202698c3c 100644 --- a/java/fory-simd/pom.xml +++ b/java/fory-simd/pom.xml @@ -74,7 +74,7 @@ org.apache.maven.plugins maven-surefire-plugin - --add-modules=jdk.incubator.vector + ${argLine} --add-modules=jdk.incubator.vector 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..7b7dfab896 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,53 @@ package org.apache.fory.test.bean; -import lombok.AllArgsConstructor; +import java.beans.ConstructorProperties; import lombok.Data; public class AccessBeans { @Data - @AllArgsConstructor private static class PrivateClass { public int f1; int f2; private int f3; + + @ConstructorProperties({"f1", "f2", "f3"}) + 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; + + @ConstructorProperties({"f1", "f2", "f3"}) + 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; + + @ConstructorProperties({"f1", "f2", "f3"}) + 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 +73,17 @@ public static class PublicClass { private DefaultLevelClass f4; private PrivateClass f5; private FinalPrivateClass f6; + + @ConstructorProperties({"f1", "f2", "f3", "f4", "f5", "f6"}) + 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/src/test/java/org/apache/fory/serializer/collection/GuavaCollectionSerializersTest.java b/java/fory-testsuite/src/test/java/org/apache/fory/serializer/collection/GuavaCollectionSerializersTest.java index b103a1467f..744ec9d0a7 100644 --- a/java/fory-testsuite/src/test/java/org/apache/fory/serializer/collection/GuavaCollectionSerializersTest.java +++ b/java/fory-testsuite/src/test/java/org/apache/fory/serializer/collection/GuavaCollectionSerializersTest.java @@ -25,6 +25,7 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedMap; import com.google.common.collect.ImmutableSortedSet; +import java.beans.ConstructorProperties; import java.util.List; import java.util.Objects; import org.apache.fory.Fory; @@ -229,6 +230,7 @@ public void testNestedRefTrackingCopy(Fory fory) { public static final class Pojo { private final List> data; + @ConstructorProperties("data") public Pojo(List> data) { this.data = data; } diff --git a/java/fory-testsuite/src/test/java/org/apache/fory/test/FastJsonTest.java b/java/fory-testsuite/src/test/java/org/apache/fory/test/FastJsonTest.java index 841d0b0028..08cdecf248 100644 --- a/java/fory-testsuite/src/test/java/org/apache/fory/test/FastJsonTest.java +++ b/java/fory-testsuite/src/test/java/org/apache/fory/test/FastJsonTest.java @@ -22,6 +22,7 @@ import com.alibaba.fastjson.JSONObject; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; +import java.beans.ConstructorProperties; import java.util.List; import org.apache.fory.Fory; import org.apache.fory.collection.Collections; @@ -34,6 +35,7 @@ public static class DemoResponse { private JSONObject json; private List objects; + @ConstructorProperties("json") public DemoResponse(JSONObject json) { this.json = json; objects = Collections.ofArrayList(json); 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 4e1d14ae43..8d35a37597 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -74,6 +74,7 @@ 3.1.12 1.13 ${basedir} + 3.3.0 1.18.38 4.11.0 @@ -109,6 +110,38 @@ fory-latest-jdk-tests + + + jdk25-test-classpath + + [25,] + + fory.jdk25.test.classpath + true + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + org.apache.fory:fory-core + + + + ${maven.multiModuleProjectDirectory}/fory-core/target/jdk25-test-classes + + + + + + + @@ -238,6 +271,7 @@ ${maven.compiler.source} ${maven.compiler.target} + true diff --git a/kotlin/fory-kotlin-tests/pom.xml b/kotlin/fory-kotlin-tests/pom.xml index f8e0409b8f..2aad72b680 100644 --- a/kotlin/fory-kotlin-tests/pom.xml +++ b/kotlin/fory-kotlin-tests/pom.xml @@ -105,6 +105,7 @@ compile + true ${project.basedir}/src/main/kotlin ${project.build.directory}/generated-sources/ksp @@ -125,6 +126,16 @@ fory-kotlin-tests-xlang-peer false + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + diff --git a/kotlin/fory-kotlin/pom.xml b/kotlin/fory-kotlin/pom.xml index 139cc6dc7e..00719b4789 100644 --- a/kotlin/fory-kotlin/pom.xml +++ b/kotlin/fory-kotlin/pom.xml @@ -44,6 +44,7 @@ compile + true ${project.basedir}/src/main/kotlin ${project.basedir}/src/main/java @@ -60,6 +61,7 @@ test-compile + true ${project.basedir}/src/test/kotlin diff --git a/kotlin/fory-kotlin/src/main/java/org/apache/fory/serializer/kotlin/KotlinSerializers.java b/kotlin/fory-kotlin/src/main/java/org/apache/fory/serializer/kotlin/KotlinSerializers.java index 36acb0c5d8..c830124196 100644 --- a/kotlin/fory-kotlin/src/main/java/org/apache/fory/serializer/kotlin/KotlinSerializers.java +++ b/kotlin/fory-kotlin/src/main/java/org/apache/fory/serializer/kotlin/KotlinSerializers.java @@ -125,20 +125,38 @@ public static void registerSerializers(Fory fory) { // Ranges and Progressions. registerIfAbsent(resolver, kotlin.ranges.CharRange.class); + resolver.registerSerializer(kotlin.ranges.CharRange.class, new CharRangeSerializer(config)); registerIfAbsent(resolver, kotlin.ranges.CharProgression.class); + resolver.registerSerializer( + kotlin.ranges.CharProgression.class, new CharProgressionSerializer(config)); registerIfAbsent(resolver, kotlin.ranges.IntRange.class); + resolver.registerSerializer(kotlin.ranges.IntRange.class, new IntRangeSerializer(config)); registerIfAbsent(resolver, kotlin.ranges.IntProgression.class); + resolver.registerSerializer( + kotlin.ranges.IntProgression.class, new IntProgressionSerializer(config)); registerIfAbsent(resolver, kotlin.ranges.LongRange.class); + resolver.registerSerializer(kotlin.ranges.LongRange.class, new LongRangeSerializer(config)); registerIfAbsent(resolver, kotlin.ranges.LongProgression.class); + resolver.registerSerializer( + kotlin.ranges.LongProgression.class, new LongProgressionSerializer(config)); registerIfAbsent(resolver, kotlin.ranges.UIntRange.class); + resolver.registerSerializer(kotlin.ranges.UIntRange.class, new UIntRangeSerializer(config)); registerIfAbsent(resolver, kotlin.ranges.UIntProgression.class); + resolver.registerSerializer( + kotlin.ranges.UIntProgression.class, new UIntProgressionSerializer(config)); registerIfAbsent(resolver, kotlin.ranges.ULongRange.class); + resolver.registerSerializer(kotlin.ranges.ULongRange.class, new ULongRangeSerializer(config)); registerIfAbsent(resolver, kotlin.ranges.ULongProgression.class); + resolver.registerSerializer( + kotlin.ranges.ULongProgression.class, new ULongProgressionSerializer(config)); // Built-in classes. registerIfAbsent(resolver, kotlin.Pair.class); + resolver.registerSerializer(kotlin.Pair.class, new PairSerializer(config)); registerIfAbsent(resolver, kotlin.Triple.class); + resolver.registerSerializer(kotlin.Triple.class, new TripleSerializer(config)); registerIfAbsent(resolver, kotlin.Result.class); + resolver.registerSerializer(kotlin.Result.class, new ResultSerializer(config)); registerIfAbsent(resolver, Result.Failure.class); // kotlin.random diff --git a/kotlin/fory-kotlin/src/main/kotlin/org/apache/fory/serializer/kotlin/KotlinBuiltinSerializers.kt b/kotlin/fory-kotlin/src/main/kotlin/org/apache/fory/serializer/kotlin/KotlinBuiltinSerializers.kt new file mode 100644 index 0000000000..56362b8525 --- /dev/null +++ b/kotlin/fory-kotlin/src/main/kotlin/org/apache/fory/serializer/kotlin/KotlinBuiltinSerializers.kt @@ -0,0 +1,236 @@ +/* + * 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.kotlin + +import org.apache.fory.config.Config +import org.apache.fory.context.ReadContext +import org.apache.fory.context.WriteContext +import org.apache.fory.serializer.ImmutableSerializer +import org.apache.fory.serializer.Shareable + +public class PairSerializer(config: Config) : + ImmutableSerializer>(config, Pair::class.java), Shareable { + override fun write(writeContext: WriteContext, value: Pair<*, *>) { + writeContext.writeRef(value.first) + writeContext.writeRef(value.second) + } + + override fun read(readContext: ReadContext): Pair<*, *> { + return Pair(readContext.readRef(), readContext.readRef()) + } +} + +public class TripleSerializer(config: Config) : + ImmutableSerializer>(config, Triple::class.java), Shareable { + override fun write(writeContext: WriteContext, value: Triple<*, *, *>) { + writeContext.writeRef(value.first) + writeContext.writeRef(value.second) + writeContext.writeRef(value.third) + } + + override fun read(readContext: ReadContext): Triple<*, *, *> { + return Triple(readContext.readRef(), readContext.readRef(), readContext.readRef()) + } +} + +public class ResultSerializer(config: Config) : + ImmutableSerializer>(config, Result::class.java), Shareable { + override fun write(writeContext: WriteContext, value: Result<*>) { + val failure = value.exceptionOrNull() + val buffer = writeContext.buffer + buffer.writeBoolean(failure == null) + if (failure == null) { + writeContext.writeRef(value.getOrNull()) + } else { + writeContext.writeRef(failure) + } + } + + override fun read(readContext: ReadContext): Result<*> { + return if (readContext.buffer.readBoolean()) { + Result.success(readContext.readRef()) + } else { + Result.failure(readContext.readRef() as Throwable) + } + } +} + +public class CharRangeSerializer(config: Config) : + ImmutableSerializer(config, CharRange::class.java), Shareable { + override fun write(writeContext: WriteContext, value: CharRange) { + val buffer = writeContext.buffer + buffer.writeInt16(value.first.code.toShort()) + buffer.writeInt16(value.last.code.toShort()) + } + + override fun read(readContext: ReadContext): CharRange { + val buffer = readContext.buffer + return CharRange(buffer.readInt16().toInt().toChar(), buffer.readInt16().toInt().toChar()) + } +} + +public class CharProgressionSerializer(config: Config) : + ImmutableSerializer(config, CharProgression::class.java), Shareable { + override fun write(writeContext: WriteContext, value: CharProgression) { + val buffer = writeContext.buffer + buffer.writeInt16(value.first.code.toShort()) + buffer.writeInt16(value.last.code.toShort()) + buffer.writeInt32(value.step) + } + + override fun read(readContext: ReadContext): CharProgression { + val buffer = readContext.buffer + return CharProgression.fromClosedRange( + buffer.readInt16().toInt().toChar(), + buffer.readInt16().toInt().toChar(), + buffer.readInt32() + ) + } +} + +public class IntRangeSerializer(config: Config) : + ImmutableSerializer(config, IntRange::class.java), Shareable { + override fun write(writeContext: WriteContext, value: IntRange) { + val buffer = writeContext.buffer + buffer.writeInt32(value.first) + buffer.writeInt32(value.last) + } + + override fun read(readContext: ReadContext): IntRange { + val buffer = readContext.buffer + return IntRange(buffer.readInt32(), buffer.readInt32()) + } +} + +public class IntProgressionSerializer(config: Config) : + ImmutableSerializer(config, IntProgression::class.java), Shareable { + override fun write(writeContext: WriteContext, value: IntProgression) { + val buffer = writeContext.buffer + buffer.writeInt32(value.first) + buffer.writeInt32(value.last) + buffer.writeInt32(value.step) + } + + override fun read(readContext: ReadContext): IntProgression { + val buffer = readContext.buffer + return IntProgression.fromClosedRange( + buffer.readInt32(), + buffer.readInt32(), + buffer.readInt32() + ) + } +} + +public class LongRangeSerializer(config: Config) : + ImmutableSerializer(config, LongRange::class.java), Shareable { + override fun write(writeContext: WriteContext, value: LongRange) { + val buffer = writeContext.buffer + buffer.writeInt64(value.first) + buffer.writeInt64(value.last) + } + + override fun read(readContext: ReadContext): LongRange { + val buffer = readContext.buffer + return LongRange(buffer.readInt64(), buffer.readInt64()) + } +} + +public class LongProgressionSerializer(config: Config) : + ImmutableSerializer(config, LongProgression::class.java), Shareable { + override fun write(writeContext: WriteContext, value: LongProgression) { + val buffer = writeContext.buffer + buffer.writeInt64(value.first) + buffer.writeInt64(value.last) + buffer.writeInt64(value.step) + } + + override fun read(readContext: ReadContext): LongProgression { + val buffer = readContext.buffer + val first = buffer.readInt64() + val last = buffer.readInt64() + val step = buffer.readInt64() + return LongProgression.fromClosedRange(first, last, step) + } +} + +public class UIntRangeSerializer(config: Config) : + ImmutableSerializer(config, UIntRange::class.java), Shareable { + override fun write(writeContext: WriteContext, value: UIntRange) { + val buffer = writeContext.buffer + buffer.writeInt32(value.first.toInt()) + buffer.writeInt32(value.last.toInt()) + } + + override fun read(readContext: ReadContext): UIntRange { + val buffer = readContext.buffer + return UIntRange(buffer.readInt32().toUInt(), buffer.readInt32().toUInt()) + } +} + +public class UIntProgressionSerializer(config: Config) : + ImmutableSerializer(config, UIntProgression::class.java), Shareable { + override fun write(writeContext: WriteContext, value: UIntProgression) { + val buffer = writeContext.buffer + buffer.writeInt32(value.first.toInt()) + buffer.writeInt32(value.last.toInt()) + buffer.writeInt32(value.step) + } + + override fun read(readContext: ReadContext): UIntProgression { + val buffer = readContext.buffer + return UIntProgression.fromClosedRange( + buffer.readInt32().toUInt(), + buffer.readInt32().toUInt(), + buffer.readInt32() + ) + } +} + +public class ULongRangeSerializer(config: Config) : + ImmutableSerializer(config, ULongRange::class.java), Shareable { + override fun write(writeContext: WriteContext, value: ULongRange) { + val buffer = writeContext.buffer + buffer.writeInt64(value.first.toLong()) + buffer.writeInt64(value.last.toLong()) + } + + override fun read(readContext: ReadContext): ULongRange { + val buffer = readContext.buffer + return ULongRange(buffer.readInt64().toULong(), buffer.readInt64().toULong()) + } +} + +public class ULongProgressionSerializer(config: Config) : + ImmutableSerializer(config, ULongProgression::class.java), Shareable { + override fun write(writeContext: WriteContext, value: ULongProgression) { + val buffer = writeContext.buffer + buffer.writeInt64(value.first.toLong()) + buffer.writeInt64(value.last.toLong()) + buffer.writeInt64(value.step) + } + + override fun read(readContext: ReadContext): ULongProgression { + val buffer = readContext.buffer + val first = buffer.readInt64().toULong() + val last = buffer.readInt64().toULong() + val step = buffer.readInt64() + return ULongProgression.fromClosedRange(first, last, step) + } +} 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..26850ff5d6 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.ObjectCreators 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,68 @@ 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, seen) + } + return newPublicDefaultInstance(clazz, seen) + } finally { + seen.remove(clazz) + } + } + + private fun newDefaultInstance(clazz: Class<*>, seen: IdentityHashMap, Boolean>): Any? { + val creator = ObjectCreators.getObjectCreator(clazz) + if (!creator.hasConstructorFields()) { + return creator.newInstance() + } + val parameterTypes = creator.getConstructorFieldTypes() + val args = arrayOfNulls(parameterTypes.size) + for (i in parameterTypes.indices) { + args[i] = getDefaultValueForType(parameterTypes[i], seen) ?: return null + } + return creator.newInstanceWithArguments(*args) + } + + 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 a51d0aa55f..228b4357d7 100644 --- a/scala/build.sbt +++ b/scala/build.sbt @@ -47,6 +47,20 @@ libraryDependencies ++= Seq( "dev.zio" %% "zio" % "2.1.7" % Test, ) +Test / fork := true +Test / javaOptions ++= Seq( + "--add-opens=java.base/java.lang=ALL-UNNAMED", + "--add-opens=java.base/java.lang.invoke=ALL-UNNAMED", + "--add-opens=java.base/java.lang.reflect=ALL-UNNAMED", + "--add-opens=java.base/jdk.internal.reflect=ALL-UNNAMED", + "--add-opens=java.base/java.util=ALL-UNNAMED", + "--add-opens=java.base/java.util.concurrent=ALL-UNNAMED", + "--add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED", + "--add-opens=java.base/java.io=ALL-UNNAMED", + "--add-opens=java.base/java.net=ALL-UNNAMED", + "--add-opens=java.base/java.math=ALL-UNNAMED", +) + 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..3fa114f104 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,7 @@ 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 org.apache.fory.platform.internal._JDKAccess import java.lang.invoke.{MethodHandle, MethodHandles} import scala.collection.immutable.NumericRange