From 2c0352480a40c00b3ff0ac2dc2a95817a2f199e4 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 23 May 2026 20:41:21 +0800 Subject: [PATCH 01/34] refactor(java): move buffer equality out of UnsafeOps --- .../org/apache/fory/memory/MemoryBuffer.java | 36 ++++++- .../org/apache/fory/platform/UnsafeOps.java | 41 -------- .../apache/fory/memory/MemoryBufferTest.java | 3 + .../apache/fory/reflect/UnsafeOpsTest.java | 93 ------------------- 4 files changed, 36 insertions(+), 137 deletions(-) delete mode 100644 java/fory-core/src/test/java/org/apache/fory/reflect/UnsafeOpsTest.java 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..89fc343931 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 @@ -64,6 +64,7 @@ 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 boolean LITTLE_ENDIAN = NativeByteOrder.IS_LITTLE_ENDIAN; + private static final boolean UNALIGNED = !AndroidSupport.IS_ANDROID && UnsafeOps.unaligned(); // Global allocator instance that can be customized private static volatile MemoryAllocator globalAllocator = new DefaultMemoryAllocator(); @@ -3716,7 +3717,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 +3741,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, UnsafeOps.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 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 index c8ddf1ae5a..6c9c273016 100644 --- 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 @@ -234,47 +234,6 @@ public static Object[] copyObjectArray(Object[] arr) { 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 { 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..1af57bf458 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 @@ -340,8 +340,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 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; - } -} From b9129acaf7cf330f08dc1bf714a359dac25595fc Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 23 May 2026 20:53:07 +0800 Subject: [PATCH 02/34] refactor(java): move direct buffer address access to MemoryBuffer --- .../apache/fory/memory/ByteBufferUtil.java | 27 ------------- .../org/apache/fory/memory/MemoryBuffer.java | 40 +++++++++++++++---- 2 files changed, 33 insertions(+), 34 deletions(-) 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/MemoryBuffer.java b/java/fory-core/src/main/java/org/apache/fory/memory/MemoryBuffer.java index 89fc343931..47d69b6ee2 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,7 +19,10 @@ 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; @@ -68,6 +71,20 @@ public final class MemoryBuffer { // 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 = UnsafeOps.objectFieldOffset(addressField); + checkArgument(BUFFER_ADDRESS_FIELD_OFFSET != 0); + } catch (NoSuchFieldException e) { + throw new IllegalStateException(e); + } + } + } + // 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 @@ -187,12 +204,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 UnsafeOps.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); @@ -370,7 +397,7 @@ 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); @@ -395,7 +422,7 @@ 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); @@ -3678,7 +3705,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(); @@ -3878,8 +3905,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()); @@ -3896,7 +3922,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); } From 1fcdfeab6afeb45884e148ca8b01de9ac02bb20c Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 23 May 2026 20:55:45 +0800 Subject: [PATCH 03/34] remove unused api in UnsafeOps --- .../org/apache/fory/platform/UnsafeOps.java | 37 ------------------- 1 file changed, 37 deletions(-) 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 index 6c9c273016..7ca7d09bd0 100644 --- 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 @@ -182,37 +182,6 @@ 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) { @@ -228,12 +197,6 @@ public static void copyMemory( } } - public static Object[] copyObjectArray(Object[] arr) { - Object[] objects = new Object[arr.length]; - System.arraycopy(arr, 0, objects, 0, arr.length); - return objects; - } - /** Create an instance of type. This method don't call constructor. */ public static T newInstance(Class type) { try { From 83e1015cda494b064a166ab8f1519159921e5579 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 23 May 2026 21:15:19 +0800 Subject: [PATCH 04/34] refactor(java): hide field offsets behind accessors --- .../apache/fory/reflect/FieldAccessor.java | 358 ++++++++++++++++- .../serializer/AbstractObjectSerializer.java | 368 ++++-------------- .../apache/fory/util/DefaultValueUtils.java | 20 +- .../fory/reflect/FieldAccessorTest.java | 10 +- 4 files changed, 429 insertions(+), 327 deletions(-) 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..eda245f973 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 @@ -47,11 +47,7 @@ import org.apache.fory.util.record.RecordUtils; import org.apache.fory.util.unsafe._JDKAccess; -/** - * Field accessor for primitive types and object types. - * - *

Note for primitive types, there will be box/unbox overhead. - */ +/** Field accessor for primitive types and object types. */ @SuppressWarnings({"unchecked", "rawtypes"}) public abstract class FieldAccessor { protected final Field field; @@ -84,7 +80,71 @@ public Field getField() { return field; } - public final void putObject(Object targetObject, Object object) { + 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 void putObject(Object targetObject, Object object) { // For primitive fields, we must use set() which calls the correct UnsafeOps.putXxx method. // UnsafeOps.putObject writes object references, not primitive values. if (fieldOffset != -1 && !field.getType().isPrimitive()) { @@ -94,7 +154,7 @@ public final void putObject(Object targetObject, Object object) { } } - public final Object getObject(Object targetObject) { + public Object getObject(Object targetObject) { // For primitive fields, we must use get() which calls the correct UnsafeOps.getXxx method // and returns the boxed value. UnsafeOps.getObject interprets primitive bytes as object // refs. @@ -105,10 +165,6 @@ public final Object getObject(Object targetObject) { } } - public long getFieldOffset() { - return fieldOffset; - } - void checkObj(Object obj) { if (!this.field.getDeclaringClass().isAssignableFrom(obj.getClass())) { throw new IllegalArgumentException("Illegal class " + obj.getClass()); @@ -236,14 +292,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); } @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); + UnsafeOps.putBoolean(obj, fieldOffset, value); } } @@ -258,6 +324,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 +343,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); } @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); + UnsafeOps.putByte(obj, fieldOffset, value); } } @@ -295,6 +376,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 +394,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); } @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); + UnsafeOps.putChar(obj, fieldOffset, value); } } @@ -330,6 +426,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 +444,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); } @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); + UnsafeOps.putShort(obj, fieldOffset, value); } } @@ -365,6 +476,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 +494,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); } @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); + UnsafeOps.putInt(obj, fieldOffset, value); } } @@ -400,6 +526,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 +544,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); } @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); + UnsafeOps.putLong(obj, fieldOffset, value); } } @@ -435,6 +576,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 +594,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); } @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); + UnsafeOps.putFloat(obj, fieldOffset, value); } } @@ -470,6 +626,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 +644,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); } @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); + UnsafeOps.putDouble(obj, fieldOffset, value); } } @@ -505,6 +676,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); } } @@ -594,5 +770,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/serializer/AbstractObjectSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/AbstractObjectSerializer.java index 4ac9e2ab5f..58bc955f9d 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 @@ -35,7 +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.FieldAccessor; import org.apache.fory.reflect.ObjectCreator; import org.apache.fory.reflect.ObjectCreators; @@ -243,75 +242,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 +253,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 +693,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()); - return; - case DispatchId.INT8: - fieldAccessor.set(targetObject, buffer.readByte()); - return; - case DispatchId.UINT8: - fieldAccessor.set(targetObject, buffer.readByte() & 0xFF); - return; - case DispatchId.CHAR: - fieldAccessor.set(targetObject, buffer.readChar()); - return; - case DispatchId.INT16: - fieldAccessor.set(targetObject, buffer.readInt16()); - return; - case DispatchId.UINT16: - fieldAccessor.set(targetObject, buffer.readInt16() & 0xFFFF); - return; - case DispatchId.INT32: - fieldAccessor.set(targetObject, buffer.readInt32()); - return; - case DispatchId.UINT32: - fieldAccessor.set(targetObject, Integer.toUnsignedLong(buffer.readInt32())); - return; - case DispatchId.VARINT32: - fieldAccessor.set(targetObject, buffer.readVarInt32()); - return; - case DispatchId.VAR_UINT32: - fieldAccessor.set(targetObject, Integer.toUnsignedLong(buffer.readVarUInt32())); - return; - case DispatchId.FLOAT32: - fieldAccessor.set(targetObject, buffer.readFloat32()); - return; - case DispatchId.INT64: - case DispatchId.UINT64: - fieldAccessor.set(targetObject, buffer.readInt64()); - return; - case DispatchId.VARINT64: - fieldAccessor.set(targetObject, buffer.readVarInt64()); - return; - case DispatchId.TAGGED_INT64: - fieldAccessor.set(targetObject, buffer.readTaggedInt64()); - return; - case DispatchId.VAR_UINT64: - fieldAccessor.set(targetObject, buffer.readVarUInt64()); - return; - case DispatchId.TAGGED_UINT64: - fieldAccessor.set(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()); + fieldAccessor.putBoolean(targetObject, buffer.readBoolean()); return; case DispatchId.INT8: - UnsafeOps.putByte(targetObject, fieldOffset, buffer.readByte()); + fieldAccessor.putByte(targetObject, buffer.readByte()); return; case DispatchId.UINT8: - UnsafeOps.putInt(targetObject, fieldOffset, buffer.readByte() & 0xFF); + fieldAccessor.putInt(targetObject, buffer.readByte() & 0xFF); return; case DispatchId.CHAR: - UnsafeOps.putChar(targetObject, fieldOffset, buffer.readChar()); + fieldAccessor.putChar(targetObject, buffer.readChar()); return; case DispatchId.INT16: - UnsafeOps.putShort(targetObject, fieldOffset, buffer.readInt16()); + fieldAccessor.putShort(targetObject, buffer.readInt16()); return; case DispatchId.UINT16: - UnsafeOps.putInt(targetObject, fieldOffset, buffer.readInt16() & 0xFFFF); + fieldAccessor.putInt(targetObject, buffer.readInt16() & 0xFFFF); return; case DispatchId.INT32: - UnsafeOps.putInt(targetObject, fieldOffset, buffer.readInt32()); + fieldAccessor.putInt(targetObject, buffer.readInt32()); return; case DispatchId.UINT32: - UnsafeOps.putLong(targetObject, fieldOffset, Integer.toUnsignedLong(buffer.readInt32())); + fieldAccessor.putLong(targetObject, Integer.toUnsignedLong(buffer.readInt32())); return; case DispatchId.VARINT32: - UnsafeOps.putInt(targetObject, fieldOffset, buffer.readVarInt32()); + fieldAccessor.putInt(targetObject, buffer.readVarInt32()); return; case DispatchId.VAR_UINT32: - UnsafeOps.putLong( - targetObject, fieldOffset, Integer.toUnsignedLong(buffer.readVarUInt32())); + fieldAccessor.putLong(targetObject, Integer.toUnsignedLong(buffer.readVarUInt32())); return; case DispatchId.FLOAT32: - UnsafeOps.putFloat(targetObject, fieldOffset, buffer.readFloat32()); + fieldAccessor.putFloat(targetObject, buffer.readFloat32()); return; case DispatchId.INT64: case DispatchId.UINT64: - UnsafeOps.putLong(targetObject, fieldOffset, buffer.readInt64()); + fieldAccessor.putLong(targetObject, buffer.readInt64()); return; case DispatchId.VARINT64: - UnsafeOps.putLong(targetObject, fieldOffset, buffer.readVarInt64()); + fieldAccessor.putLong(targetObject, buffer.readVarInt64()); return; case DispatchId.TAGGED_INT64: - UnsafeOps.putLong(targetObject, fieldOffset, buffer.readTaggedInt64()); + fieldAccessor.putLong(targetObject, buffer.readTaggedInt64()); return; case DispatchId.VAR_UINT64: - UnsafeOps.putLong(targetObject, fieldOffset, buffer.readVarUInt64()); + fieldAccessor.putLong(targetObject, buffer.readVarUInt64()); return; case DispatchId.TAGGED_UINT64: - UnsafeOps.putLong(targetObject, fieldOffset, buffer.readTaggedUInt64()); + fieldAccessor.putLong(targetObject, 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); @@ -1043,18 +891,11 @@ private Object[] copyFields(CopyContext copyContext, T originObj) { for (int i = 0; i < fieldInfos.length; i++) { SerializationFieldInfo fieldInfo = fieldInfos[i]; FieldAccessor fieldAccessor = fieldInfo.fieldAccessor; - long fieldOffset = fieldAccessor.getFieldOffset(); - if (fieldOffset != -1) { - if (fieldInfo.isPrimitiveField) { - fieldValues[i] = copyPrimitiveField(originObj, fieldOffset, fieldInfo.dispatchId); - } else { - fieldValues[i] = - copyNotPrimitiveField(copyContext, originObj, fieldOffset, fieldInfo.dispatchId); - } + if (fieldInfo.isPrimitiveField) { + fieldValues[i] = copyPrimitiveField(originObj, fieldAccessor, fieldInfo.dispatchId); } else { - // field in record class has offset -1 - Object fieldValue = fieldAccessor.get(originObj); - fieldValues[i] = copyContext.copyObject(fieldValue, fieldInfo.dispatchId); + fieldValues[i] = + copyNotPrimitiveField(copyContext, originObj, fieldAccessor, fieldInfo.dispatchId); } } return RecordUtils.remapping(copyRecordInfo, fieldValues); @@ -1075,17 +916,11 @@ public static void copyFields( Object newObj) { for (SerializationFieldInfo fieldInfo : fieldInfos) { FieldAccessor fieldAccessor = fieldInfo.fieldAccessor; - long fieldOffset = fieldAccessor.getFieldOffset(); - if (fieldOffset == -1) { - Object fieldValue = fieldAccessor.getObject(originObj); - fieldAccessor.putObject( - newObj, copyFieldValue(copyContext, fieldValue, fieldInfo.dispatchId)); - continue; - } if (fieldInfo.isPrimitiveField) { - copySetPrimitiveField(originObj, newObj, fieldOffset, fieldInfo.dispatchId); + copySetPrimitiveField(originObj, newObj, fieldAccessor, fieldInfo.dispatchId); } else { - copySetNotPrimitiveField(copyContext, originObj, newObj, fieldOffset, fieldInfo.dispatchId); + copySetNotPrimitiveField( + copyContext, originObj, newObj, fieldAccessor, fieldInfo.dispatchId); } } } @@ -1129,33 +964,33 @@ private static Object copyFieldValue(CopyContext copyContext, Object fieldValue, } private static void copySetPrimitiveField( - Object originObj, Object newObj, long fieldOffset, int typeId) { + Object originObj, Object newObj, FieldAccessor fieldAccessor, int typeId) { switch (typeId) { case DispatchId.BOOL: - UnsafeOps.putBoolean(newObj, fieldOffset, UnsafeOps.getBoolean(originObj, fieldOffset)); + fieldAccessor.putBoolean(newObj, fieldAccessor.getBoolean(originObj)); break; case DispatchId.INT8: - UnsafeOps.putByte(newObj, fieldOffset, UnsafeOps.getByte(originObj, fieldOffset)); + fieldAccessor.putByte(newObj, fieldAccessor.getByte(originObj)); break; case DispatchId.UINT8: - UnsafeOps.putInt(newObj, fieldOffset, UnsafeOps.getInt(originObj, fieldOffset)); + fieldAccessor.putInt(newObj, fieldAccessor.getInt(originObj)); break; case DispatchId.CHAR: - UnsafeOps.putChar(newObj, fieldOffset, UnsafeOps.getChar(originObj, fieldOffset)); + fieldAccessor.putChar(newObj, fieldAccessor.getChar(originObj)); break; case DispatchId.INT16: - UnsafeOps.putShort(newObj, fieldOffset, UnsafeOps.getShort(originObj, fieldOffset)); + fieldAccessor.putShort(newObj, fieldAccessor.getShort(originObj)); break; case DispatchId.UINT16: - UnsafeOps.putInt(newObj, fieldOffset, UnsafeOps.getInt(originObj, fieldOffset)); + fieldAccessor.putInt(newObj, fieldAccessor.getInt(originObj)); break; case DispatchId.INT32: case DispatchId.VARINT32: - UnsafeOps.putInt(newObj, fieldOffset, UnsafeOps.getInt(originObj, fieldOffset)); + fieldAccessor.putInt(newObj, fieldAccessor.getInt(originObj)); break; case DispatchId.UINT32: case DispatchId.VAR_UINT32: - UnsafeOps.putLong(newObj, fieldOffset, UnsafeOps.getLong(originObj, fieldOffset)); + fieldAccessor.putLong(newObj, fieldAccessor.getLong(originObj)); break; case DispatchId.INT64: case DispatchId.VARINT64: @@ -1163,13 +998,13 @@ private static void copySetPrimitiveField( case DispatchId.UINT64: case DispatchId.VAR_UINT64: case DispatchId.TAGGED_UINT64: - UnsafeOps.putLong(newObj, fieldOffset, UnsafeOps.getLong(originObj, fieldOffset)); + fieldAccessor.putLong(newObj, fieldAccessor.getLong(originObj)); break; case DispatchId.FLOAT32: - UnsafeOps.putFloat(newObj, fieldOffset, UnsafeOps.getFloat(originObj, fieldOffset)); + fieldAccessor.putFloat(newObj, fieldAccessor.getFloat(originObj)); break; case DispatchId.FLOAT64: - UnsafeOps.putDouble(newObj, fieldOffset, UnsafeOps.getDouble(originObj, fieldOffset)); + fieldAccessor.putDouble(newObj, fieldAccessor.getDouble(originObj)); break; default: throw new RuntimeException("Unknown primitive type: " + typeId); @@ -1177,109 +1012,54 @@ private static void copySetPrimitiveField( } 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) { + Object fieldValue = fieldAccessor.getObject(originObj); + fieldAccessor.putObject(newObj, copyFieldValue(copyContext, fieldValue, typeId)); } - 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() { 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..a6713e8717 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,7 +34,6 @@ 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.FieldAccessor; import org.apache.fory.resolver.TypeResolver; import org.apache.fory.type.ScalaTypes; @@ -418,35 +417,30 @@ public static void setDefaultValues(Object obj, DefaultValueField[] defaultValue 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); + fieldAccessor.putBoolean(obj, (Boolean) defaultValue); break; case Types.INT8: - UnsafeOps.putByte(obj, fieldOffset, (Byte) defaultValue); + fieldAccessor.putByte(obj, (Byte) defaultValue); break; case Types.INT16: - UnsafeOps.putShort(obj, fieldOffset, (Short) defaultValue); + fieldAccessor.putShort(obj, (Short) defaultValue); break; case Types.INT32: case Types.VARINT32: - UnsafeOps.putInt(obj, fieldOffset, (Integer) defaultValue); + fieldAccessor.putInt(obj, (Integer) defaultValue); break; case Types.INT64: case Types.VARINT64: case Types.TAGGED_INT64: - UnsafeOps.putLong(obj, fieldOffset, (Long) defaultValue); + fieldAccessor.putLong(obj, (Long) defaultValue); break; case Types.FLOAT32: - UnsafeOps.putFloat(obj, fieldOffset, (Float) defaultValue); + fieldAccessor.putFloat(obj, (Float) defaultValue); break; case Types.FLOAT64: - UnsafeOps.putDouble(obj, fieldOffset, (Double) defaultValue); + fieldAccessor.putDouble(obj, (Double) defaultValue); break; default: // Object type (including String, char, boxed types not covered above) 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..68209013fb 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 @@ -46,14 +46,23 @@ 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 @@ -107,7 +116,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); From 4179012fd2b4455529097dad6887db821d572589 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 23 May 2026 21:22:21 +0800 Subject: [PATCH 05/34] remove unused code --- .../org/apache/fory/memory/LittleEndian.java | 33 ------------------- 1 file changed, 33 deletions(-) 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..0e0c2d246a 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 @@ -58,23 +58,6 @@ 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); @@ -93,20 +76,4 @@ public static void putInt64(byte[] o, int index, long 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); - } } From c3f76efc70ce9e5088a783530d59954fda30d7b1 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 23 May 2026 14:10:16 +0800 Subject: [PATCH 06/34] remove UNSAFE.throwException usage --- .../apache/fory/util/unsafe/_JDKAccess.java | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) 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 index e9e8774f60..d41b302679 100644 --- a/java/fory-core/src/main/java/org/apache/fory/util/unsafe/_JDKAccess.java +++ b/java/fory-core/src/main/java/org/apache/fory/util/unsafe/_JDKAccess.java @@ -132,8 +132,7 @@ public static Function makeJDKFunction( boxedMethodType(handle.type())); return (Function) callSite.getTarget().invokeExact(); } catch (Throwable e) { - UNSAFE.throwException(e); - throw new IllegalStateException(e); + throw ExceptionUtils.throwException(e); } } @@ -153,8 +152,7 @@ public static Consumer makeJDKConsumer(Lookup lookup, MethodHandle handle boxedMethodType(handle.type())); return (Consumer) callSite.getTarget().invokeExact(); } catch (Throwable e) { - UNSAFE.throwException(e); - throw new IllegalStateException(e); + throw ExceptionUtils.throwException(e); } } @@ -174,8 +172,7 @@ public static BiConsumer makeJDKBiConsumer(Lookup lookup, MethodHan boxedMethodType(handle.type())); return (BiConsumer) callSite.getTarget().invokeExact(); } catch (Throwable e) { - UNSAFE.throwException(e); - throw new IllegalStateException(e); + throw ExceptionUtils.throwException(e); } } @@ -208,8 +205,7 @@ public static T makeFunction(Lookup lookup, MethodHandle handle, Method meth instantiatedMethodType); return (T) callSite.getTarget().invokeExact(); } catch (Throwable e) { - UNSAFE.throwException(e); - throw new IllegalStateException(e); + throw ExceptionUtils.throwException(e); } } @@ -243,8 +239,7 @@ public static T makeFunction(Lookup lookup, MethodHandle handle, Class fu // FIXME(chaokunyang) why use invokeExact will fail. return (T) callSite.getTarget().invoke(); } catch (Throwable e) { - UNSAFE.throwException(e); - throw new IllegalStateException(e); + throw ExceptionUtils.throwException(e); } } @@ -296,8 +291,7 @@ public static Object makeGetterFunction( // represented by handle, then exception will be thrown. return makeGetterFunction(lookup, handle, Object.class); } catch (Throwable e) { - UNSAFE.throwException(e); - throw new IllegalStateException(e); + throw ExceptionUtils.throwException(e); } } @@ -315,7 +309,7 @@ public static Object getModule(Class cls) { try { return getModuleMethod.invoke(cls); } catch (IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException(e); + throw ExceptionUtils.throwException(e); } } @@ -332,7 +326,7 @@ public static Object addReads(Object thisModule, Object otherModule) { } return addReadsHandle.invoke(thisModule, otherModule); } catch (Throwable e) { - throw new RuntimeException(e); + throw ExceptionUtils.throwException(e); } } } From 0add063f2f505d5d08ddb1d815852257bfbace95 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 23 May 2026 22:11:32 +0800 Subject: [PATCH 07/34] refactor(java): add Fory byte array streams --- .../src/main/java/org/apache/fory/Fory.java | 10 +-- .../fory/io/ForyByteArrayInputStream.java | 82 +++++++++++++++++++ .../fory/io/ForyByteArrayOutputStream.java | 63 ++++++++++++++ .../org/apache/fory/memory/MemoryUtils.java | 75 ++++------------- .../test/java/org/apache/fory/StreamTest.java | 6 ++ .../apache/fory/memory/MemoryBufferTest.java | 23 ++++++ .../serializer/AndroidDynamicFeatureTest.java | 45 +++++----- 7 files changed, 217 insertions(+), 87 deletions(-) create mode 100644 java/fory-core/src/main/java/org/apache/fory/io/ForyByteArrayInputStream.java create mode 100644 java/fory-core/src/main/java/org/apache/fory/io/ForyByteArrayOutputStream.java 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..533f0ebb57 100644 --- a/java/fory-core/src/main/java/org/apache/fory/Fory.java +++ b/java/fory-core/src/main/java/org/apache/fory/Fory.java @@ -19,7 +19,6 @@ package org.apache.fory; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; @@ -46,13 +45,13 @@ import org.apache.fory.exception.DeserializationException; import org.apache.fory.exception.ForyException; import org.apache.fory.exception.SerializationException; +import org.apache.fory.io.ForyByteArrayOutputStream; import org.apache.fory.io.ForyInputStream; import org.apache.fory.io.ForyReadableChannel; 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.resolver.ClassResolver; import org.apache.fory.resolver.SharedRegistry; import org.apache.fory.resolver.TypeChecker; @@ -564,12 +563,13 @@ public T copy(T obj) { private void serializeToStream(OutputStream outputStream, Consumer function) { MemoryBuffer buf = getBuffer(); - if (!AndroidSupport.IS_ANDROID && outputStream.getClass() == ByteArrayOutputStream.class) { + if (outputStream instanceof ForyByteArrayOutputStream) { + ForyByteArrayOutputStream byteArrayStream = (ForyByteArrayOutputStream) outputStream; byte[] oldBytes = buf.getHeapMemory(); // Note: This should not be null. assert oldBytes != null; - MemoryUtils.wrap((ByteArrayOutputStream) outputStream, buf); + MemoryUtils.wrap(byteArrayStream, buf); function.accept(buf); - MemoryUtils.wrap(buf, (ByteArrayOutputStream) outputStream); + MemoryUtils.wrap(buf, byteArrayStream); buf.pointTo(oldBytes, 0, oldBytes.length); resetBuffer(); } else { diff --git a/java/fory-core/src/main/java/org/apache/fory/io/ForyByteArrayInputStream.java b/java/fory-core/src/main/java/org/apache/fory/io/ForyByteArrayInputStream.java new file mode 100644 index 0000000000..a622b78d36 --- /dev/null +++ b/java/fory-core/src/main/java/org/apache/fory/io/ForyByteArrayInputStream.java @@ -0,0 +1,82 @@ +/* + * 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.io; + +import java.io.ByteArrayInputStream; +import org.apache.fory.util.Preconditions; + +/** A {@link ByteArrayInputStream} with public accessors for its protected state. */ +public class ForyByteArrayInputStream extends ByteArrayInputStream { + public ForyByteArrayInputStream(byte[] buffer) { + super(buffer); + } + + public ForyByteArrayInputStream(byte[] buffer, int offset, int length) { + super(buffer, offset, length); + } + + public byte[] getBuffer() { + return buf; + } + + public void setBuffer(byte[] buffer) { + buf = Preconditions.checkNotNull(buffer); + pos = 0; + mark = 0; + count = buffer.length; + } + + public void setBuffer(byte[] buffer, int offset, int length) { + Preconditions.checkNotNull(buffer); + Preconditions.checkArgument(offset >= 0 && length >= 0 && length <= buffer.length - offset); + buf = buffer; + pos = offset; + mark = offset; + count = offset + length; + } + + public int getPosition() { + return pos; + } + + public void setPosition(int position) { + Preconditions.checkArgument(position >= 0 && position <= count); + pos = position; + } + + public int getMark() { + return mark; + } + + public void setMark(int mark) { + Preconditions.checkArgument(mark >= 0 && mark <= count); + this.mark = mark; + } + + public int getCount() { + return count; + } + + public void setCount(int count) { + Preconditions.checkArgument(count >= 0 && count <= buf.length); + Preconditions.checkArgument(pos <= count && mark <= count); + this.count = count; + } +} diff --git a/java/fory-core/src/main/java/org/apache/fory/io/ForyByteArrayOutputStream.java b/java/fory-core/src/main/java/org/apache/fory/io/ForyByteArrayOutputStream.java new file mode 100644 index 0000000000..572febfd78 --- /dev/null +++ b/java/fory-core/src/main/java/org/apache/fory/io/ForyByteArrayOutputStream.java @@ -0,0 +1,63 @@ +/* + * 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.io; + +import java.io.ByteArrayOutputStream; +import org.apache.fory.util.Preconditions; + +/** A {@link ByteArrayOutputStream} with public accessors for its backing byte array and count. */ +public class ForyByteArrayOutputStream extends ByteArrayOutputStream { + public ForyByteArrayOutputStream() { + super(); + } + + public ForyByteArrayOutputStream(int size) { + super(size); + } + + public ForyByteArrayOutputStream(byte[] buffer) { + setBuffer(buffer); + } + + public ForyByteArrayOutputStream(byte[] buffer, int count) { + setBuffer(buffer); + setCount(count); + } + + public byte[] getBuffer() { + return buf; + } + + public void setBuffer(byte[] buffer) { + buf = Preconditions.checkNotNull(buffer); + if (count > buffer.length) { + count = buffer.length; + } + } + + public int getCount() { + return count; + } + + public void setCount(int count) { + Preconditions.checkArgument(count >= 0 && count <= buf.length); + this.count = count; + } +} 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..6eef8aa81c 100644 --- a/java/fory-core/src/main/java/org/apache/fory/memory/MemoryUtils.java +++ b/java/fory-core/src/main/java/org/apache/fory/memory/MemoryUtils.java @@ -19,11 +19,10 @@ package org.apache.fory.memory; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.nio.ByteBuffer; +import org.apache.fory.io.ForyByteArrayInputStream; +import org.apache.fory.io.ForyByteArrayOutputStream; import org.apache.fory.platform.AndroidSupport; -import org.apache.fory.platform.UnsafeOps; import org.apache.fory.util.Preconditions; /** Memory utils for fory. */ @@ -71,77 +70,39 @@ 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. + * Wrap a {@link ForyByteArrayOutputStream} into a {@link MemoryBuffer}. The writerIndex of buffer + * will be the count of the stream. */ - public static void wrap(ByteArrayOutputStream stream, MemoryBuffer buffer) { - if (AndroidSupport.IS_ANDROID) { - throw new UnsupportedOperationException( - "ByteArrayOutputStream direct wrapping is not supported on Android"); - } + public static void wrap(ForyByteArrayOutputStream stream, MemoryBuffer buffer) { Preconditions.checkNotNull(stream); - byte[] buf = (byte[]) UnsafeOps.getObject(stream, Offset.BAS_BUF_BUF); - int count = UnsafeOps.getInt(stream, Offset.BAS_BUF_COUNT); + byte[] buf = stream.getBuffer(); + int count = stream.getCount(); buffer.pointTo(buf, 0, buf.length); buffer.writerIndex(count); } /** - * Wrap a @link MemoryBuffer} into a {@link ByteArrayOutputStream}. The count of stream will be - * the writerIndex of buffer. + * Wrap a {@link MemoryBuffer} into a {@link ForyByteArrayOutputStream}. The count of the stream + * will be the writerIndex of buffer. */ - public static void wrap(MemoryBuffer buffer, ByteArrayOutputStream stream) { - if (AndroidSupport.IS_ANDROID) { - throw new UnsupportedOperationException( - "ByteArrayOutputStream direct wrapping is not supported on Android"); - } + public static void wrap(MemoryBuffer buffer, ForyByteArrayOutputStream stream) { 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()); + stream.setBuffer(bytes); + stream.setCount(buffer.writerIndex()); } /** - * Wrap a {@link ByteArrayInputStream} into a {@link MemoryBuffer}. The readerIndex of buffer will - * be the pos of stream. + * Wrap a {@link ForyByteArrayInputStream} into a {@link MemoryBuffer}. The readerIndex of buffer + * will be the position of the stream. */ - public static void wrap(ByteArrayInputStream stream, MemoryBuffer buffer) { - if (AndroidSupport.IS_ANDROID) { - throw new UnsupportedOperationException( - "ByteArrayInputStream direct wrapping is not supported on Android"); - } + public static void wrap(ForyByteArrayInputStream stream, MemoryBuffer buffer) { 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); + byte[] buf = stream.getBuffer(); + int count = stream.getCount(); + int pos = stream.getPosition(); buffer.pointTo(buf, 0, count); buffer.readerIndex(pos); } 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..40860d8b72 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 @@ -38,6 +38,7 @@ import java.util.List; import java.util.Map; import org.apache.fory.exception.DeserializationException; +import org.apache.fory.io.ForyByteArrayOutputStream; import org.apache.fory.io.ForyInputStream; import org.apache.fory.io.ForyReadableChannel; import org.apache.fory.io.ForyStreamReader; @@ -133,6 +134,11 @@ public void testBufferReset() { assertEquals(o, new byte[1000 * 1000]); assertEquals(fory.deserialize(bas.toByteArray()), new byte[1000 * 1000]); + ForyByteArrayOutputStream foryBas = new ForyByteArrayOutputStream(); + fory.serialize(foryBas, "fory-stream"); + checkBuffer(fory); + assertEquals(fory.deserialize(foryBas.toByteArray()), "fory-stream"); + bas.reset(); fory.serialize(bas, new byte[1000 * 1000]); checkBuffer(fory); 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 1af57bf458..5d035e30cb 100644 --- a/java/fory-core/src/test/java/org/apache/fory/memory/MemoryBufferTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/memory/MemoryBufferTest.java @@ -30,6 +30,8 @@ import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.Random; +import org.apache.fory.io.ForyByteArrayInputStream; +import org.apache.fory.io.ForyByteArrayOutputStream; import org.apache.fory.platform.AndroidSupport; import org.testng.Assert; import org.testng.annotations.Test; @@ -81,6 +83,27 @@ public void testBufferWrite() { assertEquals(buffer.readerIndex(), buffer.writerIndex()); } + @Test + public void testForyByteArrayStreamWrap() { + ForyByteArrayOutputStream outputStream = new ForyByteArrayOutputStream(8); + outputStream.write(new byte[] {1, 2, 3}, 0, 3); + MemoryBuffer buffer = MemoryUtils.buffer(1); + MemoryUtils.wrap(outputStream, buffer); + assertEquals(buffer.getHeapMemory(), outputStream.getBuffer()); + assertEquals(buffer.writerIndex(), 3); + buffer.writeByte((byte) 4); + MemoryUtils.wrap(buffer, outputStream); + assertEquals(outputStream.getCount(), 4); + assertEquals(outputStream.getBuffer()[3], (byte) 4); + + ForyByteArrayInputStream inputStream = new ForyByteArrayInputStream(new byte[] {5, 6, 7}); + assertEquals(inputStream.read(), 5); + MemoryUtils.wrap(inputStream, buffer); + assertEquals(buffer.getHeapMemory(), inputStream.getBuffer()); + assertEquals(buffer.readerIndex(), 1); + assertEquals(buffer.readByte(), (byte) 6); + } + @Test public void testAndroidHeapMemoryBufferPaths() throws Exception { String javaBin = 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..3be0ea6756 100644 --- a/java/fory-core/src/test/java/org/apache/fory/serializer/AndroidDynamicFeatureTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/serializer/AndroidDynamicFeatureTest.java @@ -19,7 +19,6 @@ package org.apache.fory.serializer; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; @@ -39,6 +38,9 @@ import org.apache.fory.Fory; import org.apache.fory.context.ReadContext; import org.apache.fory.context.WriteContext; +import org.apache.fory.io.ForyByteArrayInputStream; +import org.apache.fory.io.ForyByteArrayOutputStream; +import org.apache.fory.memory.MemoryBuffer; import org.apache.fory.memory.MemoryUtils; import org.apache.fory.platform.AndroidSupport; import org.apache.fory.resolver.TypeResolver; @@ -87,7 +89,7 @@ public static void main(String[] args) { LambdaSerializer.STUB_LAMBDA_CLASS == LambdaSerializer.ReplaceStub.class, "Android must not create a runtime lambda stub class"); verifyReflectiveGetter(); - verifyMemoryUtilsStreamWrapGuards(); + verifyMemoryUtilsStreamWraps(); verifyXlangUnion(); verifyFory(false); @@ -172,18 +174,22 @@ private static void verifyOutputStreamSerialization(Fory fory) { checkEquals(fory.deserialize(outputStream.toByteArray()), value, "OutputStream round trip"); } - private static void verifyMemoryUtilsStreamWrapGuards() { - expectUnsupportedAndroidWrap( - () -> MemoryUtils.wrap(new ByteArrayOutputStream(), MemoryUtils.buffer(8)), - "ByteArrayOutputStream direct wrapping"); - expectUnsupportedAndroidWrap( - () -> MemoryUtils.wrap(MemoryUtils.buffer(8), new ByteArrayOutputStream()), - "ByteArrayOutputStream direct wrapping"); - expectUnsupportedAndroidWrap( - () -> - MemoryUtils.wrap( - new ByteArrayInputStream(new byte[] {1, 2, 3}), MemoryUtils.buffer(8)), - "ByteArrayInputStream direct wrapping"); + private static void verifyMemoryUtilsStreamWraps() { + MemoryBuffer buffer = MemoryUtils.buffer(8); + ForyByteArrayOutputStream outputStream = new ForyByteArrayOutputStream(8); + outputStream.write(new byte[] {1, 2}, 0, 2); + MemoryUtils.wrap(outputStream, buffer); + checkEquals(buffer.writerIndex(), 2, "Output stream writer index"); + buffer.writeByte((byte) 3); + MemoryUtils.wrap(buffer, outputStream); + checkEquals(outputStream.getCount(), 3, "Output stream count"); + checkEquals(outputStream.getBuffer()[2], (byte) 3, "Output stream buffer"); + + ForyByteArrayInputStream inputStream = new ForyByteArrayInputStream(new byte[] {4, 5, 6}); + checkEquals(inputStream.read(), 4, "Input stream first byte"); + MemoryUtils.wrap(inputStream, buffer); + checkEquals(buffer.readerIndex(), 1, "Input stream reader index"); + checkEquals(buffer.readByte(), (byte) 5, "Input stream buffer"); } private static void verifyXlangUnion() { @@ -257,17 +263,6 @@ private static void expectUnsupported(Serializer serializer) { } } - private static void expectUnsupportedAndroidWrap(Runnable operation, String messageFragment) { - try { - operation.run(); - throw new AssertionError("Expected Android unsafe stream wrapping to fail"); - } catch (UnsupportedOperationException expected) { - check( - expected.getMessage().contains(messageFragment), - "Unexpected unsupported message: " + expected.getMessage()); - } - } - private static void check(boolean value, String message) { if (!value) { throw new AssertionError(message); From 4603f536f420032fec6f9903614ed8f32e987b74 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 23 May 2026 22:28:48 +0800 Subject: [PATCH 08/34] refactor(java): route byte array stream wrapping through JDK access --- .../src/main/java/org/apache/fory/Fory.java | 10 +-- .../fory/io/ForyByteArrayInputStream.java | 82 ------------------- .../fory/io/ForyByteArrayOutputStream.java | 63 -------------- .../org/apache/fory/memory/MemoryUtils.java | 55 ++++++------- .../apache/fory/util/unsafe/_JDKAccess.java | 54 ++++++++++++ .../test/java/org/apache/fory/StreamTest.java | 6 -- .../apache/fory/memory/MemoryBufferTest.java | 19 +++-- .../serializer/AndroidDynamicFeatureTest.java | 48 ++++++----- 8 files changed, 124 insertions(+), 213 deletions(-) delete mode 100644 java/fory-core/src/main/java/org/apache/fory/io/ForyByteArrayInputStream.java delete mode 100644 java/fory-core/src/main/java/org/apache/fory/io/ForyByteArrayOutputStream.java 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 533f0ebb57..140d83f28c 100644 --- a/java/fory-core/src/main/java/org/apache/fory/Fory.java +++ b/java/fory-core/src/main/java/org/apache/fory/Fory.java @@ -19,6 +19,7 @@ package org.apache.fory; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; @@ -45,7 +46,6 @@ import org.apache.fory.exception.DeserializationException; import org.apache.fory.exception.ForyException; import org.apache.fory.exception.SerializationException; -import org.apache.fory.io.ForyByteArrayOutputStream; import org.apache.fory.io.ForyInputStream; import org.apache.fory.io.ForyReadableChannel; import org.apache.fory.logging.Logger; @@ -563,13 +563,13 @@ public T copy(T obj) { private void serializeToStream(OutputStream outputStream, Consumer function) { MemoryBuffer buf = getBuffer(); - if (outputStream instanceof ForyByteArrayOutputStream) { - ForyByteArrayOutputStream byteArrayStream = (ForyByteArrayOutputStream) outputStream; + if (MemoryUtils.BYTE_ARRAY_STREAM_WRAP_SUPPORTED + && outputStream.getClass() == ByteArrayOutputStream.class) { byte[] oldBytes = buf.getHeapMemory(); // Note: This should not be null. assert oldBytes != null; - MemoryUtils.wrap(byteArrayStream, buf); + MemoryUtils.wrap((ByteArrayOutputStream) outputStream, buf); function.accept(buf); - MemoryUtils.wrap(buf, byteArrayStream); + MemoryUtils.wrap(buf, (ByteArrayOutputStream) outputStream); buf.pointTo(oldBytes, 0, oldBytes.length); resetBuffer(); } else { diff --git a/java/fory-core/src/main/java/org/apache/fory/io/ForyByteArrayInputStream.java b/java/fory-core/src/main/java/org/apache/fory/io/ForyByteArrayInputStream.java deleted file mode 100644 index a622b78d36..0000000000 --- a/java/fory-core/src/main/java/org/apache/fory/io/ForyByteArrayInputStream.java +++ /dev/null @@ -1,82 +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.io; - -import java.io.ByteArrayInputStream; -import org.apache.fory.util.Preconditions; - -/** A {@link ByteArrayInputStream} with public accessors for its protected state. */ -public class ForyByteArrayInputStream extends ByteArrayInputStream { - public ForyByteArrayInputStream(byte[] buffer) { - super(buffer); - } - - public ForyByteArrayInputStream(byte[] buffer, int offset, int length) { - super(buffer, offset, length); - } - - public byte[] getBuffer() { - return buf; - } - - public void setBuffer(byte[] buffer) { - buf = Preconditions.checkNotNull(buffer); - pos = 0; - mark = 0; - count = buffer.length; - } - - public void setBuffer(byte[] buffer, int offset, int length) { - Preconditions.checkNotNull(buffer); - Preconditions.checkArgument(offset >= 0 && length >= 0 && length <= buffer.length - offset); - buf = buffer; - pos = offset; - mark = offset; - count = offset + length; - } - - public int getPosition() { - return pos; - } - - public void setPosition(int position) { - Preconditions.checkArgument(position >= 0 && position <= count); - pos = position; - } - - public int getMark() { - return mark; - } - - public void setMark(int mark) { - Preconditions.checkArgument(mark >= 0 && mark <= count); - this.mark = mark; - } - - public int getCount() { - return count; - } - - public void setCount(int count) { - Preconditions.checkArgument(count >= 0 && count <= buf.length); - Preconditions.checkArgument(pos <= count && mark <= count); - this.count = count; - } -} diff --git a/java/fory-core/src/main/java/org/apache/fory/io/ForyByteArrayOutputStream.java b/java/fory-core/src/main/java/org/apache/fory/io/ForyByteArrayOutputStream.java deleted file mode 100644 index 572febfd78..0000000000 --- a/java/fory-core/src/main/java/org/apache/fory/io/ForyByteArrayOutputStream.java +++ /dev/null @@ -1,63 +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.io; - -import java.io.ByteArrayOutputStream; -import org.apache.fory.util.Preconditions; - -/** A {@link ByteArrayOutputStream} with public accessors for its backing byte array and count. */ -public class ForyByteArrayOutputStream extends ByteArrayOutputStream { - public ForyByteArrayOutputStream() { - super(); - } - - public ForyByteArrayOutputStream(int size) { - super(size); - } - - public ForyByteArrayOutputStream(byte[] buffer) { - setBuffer(buffer); - } - - public ForyByteArrayOutputStream(byte[] buffer, int count) { - setBuffer(buffer); - setCount(count); - } - - public byte[] getBuffer() { - return buf; - } - - public void setBuffer(byte[] buffer) { - buf = Preconditions.checkNotNull(buffer); - if (count > buffer.length) { - count = buffer.length; - } - } - - public int getCount() { - return count; - } - - public void setCount(int count) { - Preconditions.checkArgument(count >= 0 && count <= buf.length); - this.count = count; - } -} 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 6eef8aa81c..da42d6a9ac 100644 --- a/java/fory-core/src/main/java/org/apache/fory/memory/MemoryUtils.java +++ b/java/fory-core/src/main/java/org/apache/fory/memory/MemoryUtils.java @@ -19,14 +19,16 @@ package org.apache.fory.memory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.nio.ByteBuffer; -import org.apache.fory.io.ForyByteArrayInputStream; -import org.apache.fory.io.ForyByteArrayOutputStream; import org.apache.fory.platform.AndroidSupport; -import org.apache.fory.util.Preconditions; +import org.apache.fory.util.unsafe._JDKAccess; /** Memory utils for fory. */ public class MemoryUtils { + public static final boolean BYTE_ARRAY_STREAM_WRAP_SUPPORTED = + !AndroidSupport.IS_ANDROID && _JDKAccess.BYTE_ARRAY_STREAM_WRAP_SUPPORTED; public static MemoryBuffer buffer(int size) { return wrap(new byte[size]); @@ -71,40 +73,37 @@ public static MemoryBuffer wrap(ByteBuffer buffer) { } /** - * Wrap a {@link ForyByteArrayOutputStream} into a {@link MemoryBuffer}. The writerIndex of buffer - * will be the count of the stream. + * Wrap a {@link ByteArrayOutputStream} into a {@link MemoryBuffer}. The writerIndex of buffer + * will be the count of stream. */ - public static void wrap(ForyByteArrayOutputStream stream, MemoryBuffer buffer) { - Preconditions.checkNotNull(stream); - byte[] buf = stream.getBuffer(); - int count = stream.getCount(); - buffer.pointTo(buf, 0, buf.length); - buffer.writerIndex(count); + public static void wrap(ByteArrayOutputStream stream, MemoryBuffer buffer) { + checkByteArrayStreamWrap("ByteArrayOutputStream"); + _JDKAccess.wrap(stream, buffer); } /** - * Wrap a {@link MemoryBuffer} into a {@link ForyByteArrayOutputStream}. The count of the stream - * will be the writerIndex of buffer. + * Wrap a @link MemoryBuffer} into a {@link ByteArrayOutputStream}. The count of stream will be + * the writerIndex of buffer. */ - public static void wrap(MemoryBuffer buffer, ForyByteArrayOutputStream stream) { - Preconditions.checkNotNull(stream); - byte[] bytes = buffer.getHeapMemory(); - Preconditions.checkNotNull(bytes); - stream.setBuffer(bytes); - stream.setCount(buffer.writerIndex()); + public static void wrap(MemoryBuffer buffer, ByteArrayOutputStream stream) { + checkByteArrayStreamWrap("ByteArrayOutputStream"); + _JDKAccess.wrap(buffer, stream); } /** - * Wrap a {@link ForyByteArrayInputStream} into a {@link MemoryBuffer}. The readerIndex of buffer - * will be the position of the stream. + * Wrap a {@link ByteArrayInputStream} into a {@link MemoryBuffer}. The readerIndex of buffer will + * be the pos of stream. */ - public static void wrap(ForyByteArrayInputStream stream, MemoryBuffer buffer) { - Preconditions.checkNotNull(stream); - byte[] buf = stream.getBuffer(); - int count = stream.getCount(); - int pos = stream.getPosition(); - buffer.pointTo(buf, 0, count); - buffer.readerIndex(pos); + public static void wrap(ByteArrayInputStream stream, MemoryBuffer buffer) { + checkByteArrayStreamWrap("ByteArrayInputStream"); + _JDKAccess.wrap(stream, buffer); + } + + private static void checkByteArrayStreamWrap(String streamType) { + if (!BYTE_ARRAY_STREAM_WRAP_SUPPORTED) { + throw new UnsupportedOperationException( + streamType + " direct wrapping is not supported on this platform"); + } } private static MemoryBuffer copyToHeapBuffer(ByteBuffer buffer) { 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 index d41b302679..bd2f143666 100644 --- a/java/fory-core/src/main/java/org/apache/fory/util/unsafe/_JDKAccess.java +++ b/java/fory-core/src/main/java/org/apache/fory/util/unsafe/_JDKAccess.java @@ -19,6 +19,8 @@ package org.apache.fory.util.unsafe; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.lang.invoke.CallSite; import java.lang.invoke.LambdaMetafactory; import java.lang.invoke.MethodHandle; @@ -39,6 +41,7 @@ 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; @@ -56,6 +59,7 @@ public class _JDKAccess { // CHECKSTYLE.ON:TypeName public static final boolean IS_OPEN_J9; public static final Unsafe UNSAFE; + public static final boolean BYTE_ARRAY_STREAM_WRAP_SUPPORTED; public static final Class _INNER_UNSAFE_CLASS; public static final Object _INNER_UNSAFE; @@ -71,6 +75,7 @@ public class _JDKAccess { throw new UnsupportedOperationException("Unsafe is not supported in this platform."); } UNSAFE = unsafe; + BYTE_ARRAY_STREAM_WRAP_SUPPORTED = true; if (JdkVersion.MAJOR_VERSION >= 11) { try { Field theInternalUnsafeField = Unsafe.class.getDeclaredField("theInternalUnsafe"); @@ -100,6 +105,55 @@ public static Lookup _trustedLookup(Class objectClass) { return lookupCache.get(objectClass, () -> _Lookup._trustedLookup(objectClass)); } + // 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); + } + public static T tryMakeFunction( Lookup lookup, MethodHandle handle, Class functionInterface) { try { 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 40860d8b72..e25b486493 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 @@ -38,7 +38,6 @@ import java.util.List; import java.util.Map; import org.apache.fory.exception.DeserializationException; -import org.apache.fory.io.ForyByteArrayOutputStream; import org.apache.fory.io.ForyInputStream; import org.apache.fory.io.ForyReadableChannel; import org.apache.fory.io.ForyStreamReader; @@ -134,11 +133,6 @@ public void testBufferReset() { assertEquals(o, new byte[1000 * 1000]); assertEquals(fory.deserialize(bas.toByteArray()), new byte[1000 * 1000]); - ForyByteArrayOutputStream foryBas = new ForyByteArrayOutputStream(); - fory.serialize(foryBas, "fory-stream"); - checkBuffer(fory); - assertEquals(fory.deserialize(foryBas.toByteArray()), "fory-stream"); - bas.reset(); fory.serialize(bas, new byte[1000 * 1000]); checkBuffer(fory); 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 5d035e30cb..9ee62e3876 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,6 @@ import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.Random; -import org.apache.fory.io.ForyByteArrayInputStream; -import org.apache.fory.io.ForyByteArrayOutputStream; import org.apache.fory.platform.AndroidSupport; import org.testng.Assert; import org.testng.annotations.Test; @@ -84,22 +83,24 @@ public void testBufferWrite() { } @Test - public void testForyByteArrayStreamWrap() { - ForyByteArrayOutputStream outputStream = new ForyByteArrayOutputStream(8); + public void testByteArrayStreamWrap() { + if (!MemoryUtils.BYTE_ARRAY_STREAM_WRAP_SUPPORTED) { + 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.getHeapMemory(), outputStream.getBuffer()); assertEquals(buffer.writerIndex(), 3); + assertEquals(buffer.getByte(0), (byte) 1); buffer.writeByte((byte) 4); MemoryUtils.wrap(buffer, outputStream); - assertEquals(outputStream.getCount(), 4); - assertEquals(outputStream.getBuffer()[3], (byte) 4); + assertEquals(outputStream.size(), 4); + assertEquals(outputStream.toByteArray(), new byte[] {1, 2, 3, 4}); - ForyByteArrayInputStream inputStream = new ForyByteArrayInputStream(new byte[] {5, 6, 7}); + ByteArrayInputStream inputStream = new ByteArrayInputStream(new byte[] {5, 6, 7}); assertEquals(inputStream.read(), 5); MemoryUtils.wrap(inputStream, buffer); - assertEquals(buffer.getHeapMemory(), inputStream.getBuffer()); assertEquals(buffer.readerIndex(), 1); assertEquals(buffer.readByte(), (byte) 6); } 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 3be0ea6756..5e251c2d06 100644 --- a/java/fory-core/src/test/java/org/apache/fory/serializer/AndroidDynamicFeatureTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/serializer/AndroidDynamicFeatureTest.java @@ -19,6 +19,7 @@ package org.apache.fory.serializer; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; @@ -38,9 +39,6 @@ import org.apache.fory.Fory; import org.apache.fory.context.ReadContext; import org.apache.fory.context.WriteContext; -import org.apache.fory.io.ForyByteArrayInputStream; -import org.apache.fory.io.ForyByteArrayOutputStream; -import org.apache.fory.memory.MemoryBuffer; import org.apache.fory.memory.MemoryUtils; import org.apache.fory.platform.AndroidSupport; import org.apache.fory.resolver.TypeResolver; @@ -89,7 +87,7 @@ public static void main(String[] args) { LambdaSerializer.STUB_LAMBDA_CLASS == LambdaSerializer.ReplaceStub.class, "Android must not create a runtime lambda stub class"); verifyReflectiveGetter(); - verifyMemoryUtilsStreamWraps(); + verifyMemoryUtilsStreamWrapGuards(); verifyXlangUnion(); verifyFory(false); @@ -174,22 +172,21 @@ private static void verifyOutputStreamSerialization(Fory fory) { checkEquals(fory.deserialize(outputStream.toByteArray()), value, "OutputStream round trip"); } - private static void verifyMemoryUtilsStreamWraps() { - MemoryBuffer buffer = MemoryUtils.buffer(8); - ForyByteArrayOutputStream outputStream = new ForyByteArrayOutputStream(8); - outputStream.write(new byte[] {1, 2}, 0, 2); - MemoryUtils.wrap(outputStream, buffer); - checkEquals(buffer.writerIndex(), 2, "Output stream writer index"); - buffer.writeByte((byte) 3); - MemoryUtils.wrap(buffer, outputStream); - checkEquals(outputStream.getCount(), 3, "Output stream count"); - checkEquals(outputStream.getBuffer()[2], (byte) 3, "Output stream buffer"); - - ForyByteArrayInputStream inputStream = new ForyByteArrayInputStream(new byte[] {4, 5, 6}); - checkEquals(inputStream.read(), 4, "Input stream first byte"); - MemoryUtils.wrap(inputStream, buffer); - checkEquals(buffer.readerIndex(), 1, "Input stream reader index"); - checkEquals(buffer.readByte(), (byte) 5, "Input stream buffer"); + private static void verifyMemoryUtilsStreamWrapGuards() { + check( + !MemoryUtils.BYTE_ARRAY_STREAM_WRAP_SUPPORTED, + "Android must report byte-array stream wrapping unsupported"); + expectUnsupportedAndroidWrap( + () -> MemoryUtils.wrap(new ByteArrayOutputStream(), MemoryUtils.buffer(8)), + "ByteArrayOutputStream direct wrapping"); + expectUnsupportedAndroidWrap( + () -> MemoryUtils.wrap(MemoryUtils.buffer(8), new ByteArrayOutputStream()), + "ByteArrayOutputStream direct wrapping"); + expectUnsupportedAndroidWrap( + () -> + MemoryUtils.wrap( + new ByteArrayInputStream(new byte[] {1, 2, 3}), MemoryUtils.buffer(8)), + "ByteArrayInputStream direct wrapping"); } private static void verifyXlangUnion() { @@ -263,6 +260,17 @@ private static void expectUnsupported(Serializer serializer) { } } + private static void expectUnsupportedAndroidWrap(Runnable operation, String messageFragment) { + try { + operation.run(); + throw new AssertionError("Expected Android unsafe stream wrapping to fail"); + } catch (UnsupportedOperationException expected) { + check( + expected.getMessage().contains(messageFragment), + "Unexpected unsupported message: " + expected.getMessage()); + } + } + private static void check(boolean value, String message) { if (!value) { throw new AssertionError(message); From 8361bd4396b7174bbd96eea62ffbf111b0047ae4 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 23 May 2026 22:29:21 +0800 Subject: [PATCH 09/34] remove unsafe fields sort --- .../java/org/apache/fory/meta/TypeDef.java | 27 ------------------- .../org/apache/fory/meta/TypeDefTest.java | 11 -------- 2 files changed, 38 deletions(-) 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..86a82e3e37 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,12 @@ 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,7 +36,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; @@ -78,30 +75,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/test/java/org/apache/fory/meta/TypeDefTest.java b/java/fory-core/src/test/java/org/apache/fory/meta/TypeDefTest.java index 19e5378c3f..6367a8625c 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 @@ -85,17 +85,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(); From c56356545d9ed0f2650eb547656233e73e1c4710 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 23 May 2026 22:54:24 +0800 Subject: [PATCH 10/34] refactor(java): route private field access through accessors --- .../src/main/java/org/apache/fory/Fory.java | 2 +- .../org/apache/fory/memory/MemoryUtils.java | 9 +- .../apache/fory/reflect/FieldAccessor.java | 57 +++++++++ .../apache/fory/reflect/ReflectionUtils.java | 32 +---- .../fory/serializer/ExceptionSerializers.java | 19 ++- .../apache/fory/serializer/FieldGroups.java | 2 +- .../fory/serializer/JdkProxySerializer.java | 15 +-- .../fory/serializer/StringSerializer.java | 120 +++++++----------- .../collection/CollectionSerializers.java | 49 ++++--- .../serializer/collection/MapSerializers.java | 15 +-- .../collection/SubListSerializers.java | 33 ++--- .../collection/SynchronizedSerializers.java | 54 ++++---- .../collection/UnmodifiableSerializers.java | 55 ++++---- .../scala/SingletonCollectionSerializer.java | 14 +- .../scala/SingletonMapSerializer.java | 14 +- .../scala/SingletonObjectSerializer.java | 14 +- .../apache/fory/util/unsafe/_JDKAccess.java | 77 ++++++++++- .../apache/fory/codegen/JaninoUtilsTest.java | 9 +- .../apache/fory/memory/MemoryBufferTest.java | 2 +- .../serializer/AndroidDynamicFeatureTest.java | 4 +- .../fory/serializer/StringSerializerTest.java | 6 +- .../SynchronizedSerializersTest.java | 28 ++-- .../UnmodifiableSerializersTest.java | 27 ++-- 23 files changed, 352 insertions(+), 305 deletions(-) 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 140d83f28c..ed770247a3 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 @@ -563,7 +563,7 @@ public T copy(T obj) { private void serializeToStream(OutputStream outputStream, Consumer function) { MemoryBuffer buf = getBuffer(); - if (MemoryUtils.BYTE_ARRAY_STREAM_WRAP_SUPPORTED + if (MemoryUtils.JDK_INTERNAL_FIELD_ACCESS && outputStream.getClass() == ByteArrayOutputStream.class) { byte[] oldBytes = buf.getHeapMemory(); // Note: This should not be null. assert oldBytes != null; 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 da42d6a9ac..9194937c4e 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 @@ -27,8 +27,11 @@ /** Memory utils for fory. */ public class MemoryUtils { - public static final boolean BYTE_ARRAY_STREAM_WRAP_SUPPORTED = - !AndroidSupport.IS_ANDROID && _JDKAccess.BYTE_ARRAY_STREAM_WRAP_SUPPORTED; + // JDK25+ internal-field access must be backed by supported access in the multi-release classes. + // When a JDK25+ path needs java.nio private fields, the JVM must be launched with: + // --add-opens=java.base/java.nio=org.apache.fory.core + public static final boolean JDK_INTERNAL_FIELD_ACCESS = + !AndroidSupport.IS_ANDROID && _JDKAccess.JDK_INTERNAL_FIELD_ACCESS; public static MemoryBuffer buffer(int size) { return wrap(new byte[size]); @@ -100,7 +103,7 @@ public static void wrap(ByteArrayInputStream stream, MemoryBuffer buffer) { } private static void checkByteArrayStreamWrap(String streamType) { - if (!BYTE_ARRAY_STREAM_WRAP_SUPPORTED) { + if (!JDK_INTERNAL_FIELD_ACCESS) { throw new UnsupportedOperationException( streamType + " direct wrapping is not supported on this platform"); } 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 eda245f973..2ff89fc529 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; @@ -191,6 +192,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); @@ -251,6 +253,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; @@ -720,6 +731,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 = UnsafeOps.UNSAFE.staticFieldBase(field); + offset = UnsafeOps.UNSAFE.staticFieldOffset(field); + } + + @Override + public Object get(Object obj) { + return UnsafeOps.getObject(base, offset); + } + + @Override + public void set(Object obj, Object value) { + UnsafeOps.putObject(base, offset, value); + } + } + static final class GeneratedAccessor extends FieldAccessor { private static final ClassValueCache>> cache = ClassValueCache.newClassKeyCache(8); 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..e09b3361bd 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 @@ -499,30 +499,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 +521,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/serializer/ExceptionSerializers.java b/java/fory-core/src/main/java/org/apache/fory/serializer/ExceptionSerializers.java index dc22b47edb..e648da8716 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,10 +38,11 @@ import org.apache.fory.context.WriteContext; import org.apache.fory.exception.ForyException; import org.apache.fory.memory.MemoryBuffer; +import org.apache.fory.memory.MemoryUtils; import org.apache.fory.meta.TypeDef; import org.apache.fory.platform.AndroidSupport; import org.apache.fory.platform.GraalvmSupport; -import org.apache.fory.platform.UnsafeOps; +import org.apache.fory.reflect.FieldAccessor; import org.apache.fory.reflect.ObjectCreator; import org.apache.fory.reflect.ObjectCreators; import org.apache.fory.reflect.ReflectionUtils; @@ -69,11 +70,11 @@ public ExceptionSerializer(TypeResolver typeResolver, Class type) { this.typeResolver = typeResolver; messageConstructor = getOptionalMessageConstructor(type); objectCreator = - messageConstructor == null && !AndroidSupport.IS_ANDROID + messageConstructor == null && MemoryUtils.JDK_INTERNAL_FIELD_ACCESS ? createThrowableObjectCreator(type) : null; slotsSerializers = buildSlotsSerializers(typeResolver, type); - if (AndroidSupport.IS_ANDROID + if (!MemoryUtils.JDK_INTERNAL_FIELD_ACCESS && isJdkThrowable(type) && hasSubclassFields(slotsSerializers)) { throw new ForyException( @@ -110,7 +111,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_INTERNAL_FIELD_ACCESS) { return readAndroidThrowableWithoutDetailMessageField( readContext, stackTrace, slotsSerializers); } @@ -120,7 +121,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); } @@ -532,15 +533,13 @@ private static boolean containsPendingThrowable(Throwable throwable, Set { private static final byte UTF8 = 2; private static final int DEFAULT_BUFFER_SIZE = 1024; - // Make offset compatible with graalvm native image. - private static final long STRING_VALUE_FIELD_OFFSET; private static final boolean STRING_HAS_COUNT_OFFSET; - private static final long STRING_COUNT_FIELD_OFFSET; - private static final long STRING_OFFSET_FIELD_OFFSET; - - private static class Offset { - // Make offset compatible with graalvm native image. - private static final long STRING_CODER_FIELD_OFFSET; - - static { - if (AndroidSupport.IS_ANDROID) { - STRING_CODER_FIELD_OFFSET = -1; - } else { - try { - STRING_CODER_FIELD_OFFSET = - UnsafeOps.objectFieldOffset(String.class.getDeclaredField("coder")); - } catch (NoSuchFieldException e) { - throw new RuntimeException(e); - } - } - } - } static { - if (AndroidSupport.IS_ANDROID) { + if (!jdkInternalFieldAccess()) { 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; - } + STRING_VALUE_FIELD_IS_CHARS = _JDKAccess.STRING_VALUE_FIELD_IS_CHARS; + STRING_VALUE_FIELD_IS_BYTES = _JDKAccess.STRING_VALUE_FIELD_IS_BYTES; + STRING_HAS_COUNT_OFFSET = _JDKAccess.STRING_HAS_COUNT_OFFSET; } } + private static boolean jdkInternalFieldAccess() { + return !AndroidSupport.IS_ANDROID && _JDKAccess.JDK_INTERNAL_FIELD_ACCESS; + } + private final boolean compressString; private final boolean writeNumUtf16BytesForUtf8Encoding; private final boolean xlang; @@ -363,7 +315,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 +349,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) { @@ -474,10 +426,26 @@ private static boolean isLatin(char[] chars) { return true; } + private static Object getStringValue(String value) { + return _JDKAccess.getStringValue(value); + } + + private static byte getStringCoder(String value) { + return _JDKAccess.getStringCoder(value); + } + + private static int getStringOffset(String value) { + return _JDKAccess.getStringOffset(value); + } + + private static int getStringCount(String value) { + return _JDKAccess.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 +459,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,9 +476,9 @@ 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 char[] chars = (char[]) getStringValue(value); + final int offset = getStringOffset(value); + final int count = getStringCount(value); final byte coder = SlicedStringUtil.bestCoder(chars, offset, count); if (coder == LATIN1) { SlicedStringUtil.writeCharsLatin1WithOffset(this, buffer, chars, offset, count); @@ -527,8 +495,8 @@ public void writeCompressedCharsStringWithOffset(MemoryBuffer buffer, String val @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); } @@ -568,7 +536,7 @@ 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); + final char[] chars = (char[]) getStringValue(value); if (StringUtils.isLatin(chars)) { writeCharsLatin1(buffer, chars, chars.length); } else { @@ -578,9 +546,9 @@ 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); + final char[] chars = (char[]) getStringValue(value); + final int offset = getStringOffset(value); + final int count = getStringCount(value); if (SlicedStringUtil.isLatin(chars, offset, count)) { SlicedStringUtil.writeCharsLatin1WithOffset(this, buffer, chars, offset, count); } else { @@ -918,16 +886,16 @@ private void writeBytesUTF8PerfOptimized(MemoryBuffer buffer, byte[] bytes) { } private static final MethodHandles.Lookup STRING_LOOK_UP = - AndroidSupport.IS_ANDROID ? null : _JDKAccess._trustedLookup(String.class); + jdkInternalFieldAccess() ? _JDKAccess._trustedLookup(String.class) : null; private static final BiFunction CHARS_STRING_ZERO_COPY_CTR = - AndroidSupport.IS_ANDROID ? null : getCharsStringZeroCopyCtr(); + jdkInternalFieldAccess() ? getCharsStringZeroCopyCtr() : null; private static final BiFunction BYTES_STRING_ZERO_COPY_CTR = - AndroidSupport.IS_ANDROID ? null : getBytesStringZeroCopyCtr(); + jdkInternalFieldAccess() ? getBytesStringZeroCopyCtr() : null; private static final Function LATIN_BYTES_STRING_ZERO_COPY_CTR = - AndroidSupport.IS_ANDROID ? null : getLatinBytesStringZeroCopyCtr(); + jdkInternalFieldAccess() ? getLatinBytesStringZeroCopyCtr() : null; public static String newCharsStringZeroCopy(char[] data) { - if (AndroidSupport.IS_ANDROID) { + if (!jdkInternalFieldAccess()) { return newCharsStringSlow(data); } if (!STRING_VALUE_FIELD_IS_CHARS) { @@ -944,7 +912,7 @@ private static String newCharsStringSlow(char[] data) { // coder param first to make inline call args // `(buffer.readByte(), buffer.readBytesWithSizeEmbedded())` work. public static String newBytesStringZeroCopy(byte coder, byte[] data) { - if (AndroidSupport.IS_ANDROID) { + if (!jdkInternalFieldAccess()) { return newBytesStringSlow(coder, data); } if (coder == LATIN1) { 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..853c9071f9 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 @@ -57,9 +57,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.reflect.FieldAccessor; import org.apache.fory.reflect.ReflectionUtils; import org.apache.fory.resolver.ClassResolver; import org.apache.fory.resolver.TypeInfo; @@ -129,14 +130,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 +162,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_INTERNAL_FIELD_ACCESS || GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE ? value.toArray() - : (Object[]) UnsafeOps.getObject(value, ArrayFieldOffset.VALUE); + : (Object[]) ArrayAccess.ACCESSOR.getObject(value); writeContext.writeRef(array); } } @@ -553,7 +553,7 @@ public static final class SetFromMapSerializer extends CollectionSerializer(); private static final class JvmSetFromMapAccess { - private static final long MAP_FIELD_OFFSET; + private static final FieldAccessor MAP_ACCESSOR; private static final MethodHandle M_SETTER; private static final MethodHandle S_SETTER; @@ -561,7 +561,7 @@ private static final class JvmSetFromMapAccess { try { Class type = Class.forName("java.util.Collections$SetFromMap"); Field mapField = type.getDeclaredField("m"); - MAP_FIELD_OFFSET = UnsafeOps.objectFieldOffset(mapField); + MAP_ACCESSOR = FieldAccessor.createAccessor(mapField); MethodHandles.Lookup lookup = _JDKAccess._trustedLookup(type); M_SETTER = lookup.findSetter(type, "m", Map.class); S_SETTER = lookup.findSetter(type, "s", Set.class); @@ -588,7 +588,7 @@ 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_INTERNAL_FIELD_ACCESS || GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { throw new UnsupportedOperationException( "This runtime cannot read legacy SetFromMap payloads that require hidden JDK field " + "restoration"); @@ -610,12 +610,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_INTERNAL_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); @@ -627,7 +626,7 @@ 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) { + if (!MemoryUtils.JDK_INTERNAL_FIELD_ACCESS || GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { HashMap source = new HashMap<>(value.size()); for (Object element : value) { source.put(element, Boolean.TRUE); @@ -635,7 +634,7 @@ public Collection onCollectionWrite(WriteContext writeContext, Set value) { map = source; typeInfo = typeResolver.getTypeInfo(HashMap.class); } else { - map = (Map) UnsafeOps.getObject(value, JvmSetFromMapAccess.MAP_FIELD_OFFSET); + map = (Map) JvmSetFromMapAccess.MAP_ACCESSOR.getObject(value); typeInfo = typeResolver.getTypeInfo(map.getClass()); } MapLikeSerializer mapSerializer = (MapLikeSerializer) typeInfo.getSerializer(); @@ -861,13 +860,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 +878,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 +945,10 @@ public LinkedBlockingQueueSerializer( } private static int getCapacity(LinkedBlockingQueue queue) { - if (AndroidSupport.IS_ANDROID || GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { + if (!MemoryUtils.JDK_INTERNAL_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/MapSerializers.java b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/MapSerializers.java index 953314a03d..b94fd6f366 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_INTERNAL_FIELD_ACCESS && value.isEmpty()) { buffer.writeByte(JAVA_SERIALIZED_EMPTY_ENUM_MAP); getJavaSerializer().write(writeContext, value); return value; @@ -402,7 +401,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..fab1967e4c 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_INTERNAL_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..b7f03987f7 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_INTERNAL_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_INTERNAL_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_INTERNAL_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_INTERNAL_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_INTERNAL_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_INTERNAL_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..4b0cd0372c 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_INTERNAL_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_INTERNAL_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_INTERNAL_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_INTERNAL_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_INTERNAL_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_INTERNAL_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/util/unsafe/_JDKAccess.java b/java/fory-core/src/main/java/org/apache/fory/util/unsafe/_JDKAccess.java index bd2f143666..4787acc98d 100644 --- a/java/fory-core/src/main/java/org/apache/fory/util/unsafe/_JDKAccess.java +++ b/java/fory-core/src/main/java/org/apache/fory/util/unsafe/_JDKAccess.java @@ -59,7 +59,10 @@ public class _JDKAccess { // CHECKSTYLE.ON:TypeName public static final boolean IS_OPEN_J9; public static final Unsafe UNSAFE; - public static final boolean BYTE_ARRAY_STREAM_WRAP_SUPPORTED; + // 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 Class _INNER_UNSAFE_CLASS; public static final Object _INNER_UNSAFE; @@ -75,7 +78,7 @@ public class _JDKAccess { throw new UnsupportedOperationException("Unsafe is not supported in this platform."); } UNSAFE = unsafe; - BYTE_ARRAY_STREAM_WRAP_SUPPORTED = true; + JDK_INTERNAL_FIELD_ACCESS = true; if (JdkVersion.MAJOR_VERSION >= 11) { try { Field theInternalUnsafeField = Unsafe.class.getDeclaredField("theInternalUnsafe"); @@ -93,6 +96,76 @@ public class _JDKAccess { 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; + private static final long STRING_VALUE_FIELD_OFFSET; + private static final long STRING_COUNT_FIELD_OFFSET; + private static final long STRING_OFFSET_FIELD_OFFSET; + + 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; + 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 Object getStringValue(String value) { + return UNSAFE.getObject(value, STRING_VALUE_FIELD_OFFSET); + } + + public static byte getStringCoder(String value) { + return UNSAFE.getByte(value, StringCoderField.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) { 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/memory/MemoryBufferTest.java b/java/fory-core/src/test/java/org/apache/fory/memory/MemoryBufferTest.java index 9ee62e3876..03f38a9a16 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 @@ -84,7 +84,7 @@ public void testBufferWrite() { @Test public void testByteArrayStreamWrap() { - if (!MemoryUtils.BYTE_ARRAY_STREAM_WRAP_SUPPORTED) { + if (!MemoryUtils.JDK_INTERNAL_FIELD_ACCESS) { return; } ByteArrayOutputStream outputStream = new ByteArrayOutputStream(8); 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 5e251c2d06..667de876ae 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 @@ -174,8 +174,8 @@ private static void verifyOutputStreamSerialization(Fory fory) { private static void verifyMemoryUtilsStreamWrapGuards() { check( - !MemoryUtils.BYTE_ARRAY_STREAM_WRAP_SUPPORTED, - "Android must report byte-array stream wrapping unsupported"); + !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/StringSerializerTest.java b/java/fory-core/src/test/java/org/apache/fory/serializer/StringSerializerTest.java index 95c41e6714..b621677bad 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,10 +36,9 @@ import org.apache.fory.memory.MemoryBuffer; import org.apache.fory.memory.MemoryUtils; import org.apache.fory.platform.JdkVersion; -import org.apache.fory.platform.UnsafeOps; -import org.apache.fory.reflect.ReflectionUtils; import org.apache.fory.util.MathUtils; import org.apache.fory.util.StringUtils; +import org.apache.fory.util.unsafe._JDKAccess; import org.testng.Assert; import org.testng.SkipException; import org.testng.annotations.DataProvider; @@ -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/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)); From 1187a4a667e1a587cbc42acde7f0ee6005617b93 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 23 May 2026 23:04:08 +0800 Subject: [PATCH 11/34] refactor(java): move JDK access utilities to platform internal --- .../src/main/java/org/apache/fory/memory/MemoryUtils.java | 2 +- .../src/main/java/org/apache/fory/platform/UnsafeOps.java | 2 +- .../{util/unsafe => platform/internal}/DefineClass.java | 2 +- .../fory/{util/unsafe => platform/internal}/_JDKAccess.java | 2 +- .../fory/{util/unsafe => platform/internal}/_Lookup.java | 2 +- .../main/java/org/apache/fory/reflect/FieldAccessor.java | 2 +- .../main/java/org/apache/fory/reflect/ObjectCreators.java | 2 +- .../main/java/org/apache/fory/reflect/ReflectionUtils.java | 2 +- .../org/apache/fory/serializer/ExceptionSerializers.java | 2 +- .../java/org/apache/fory/serializer/LambdaSerializer.java | 2 +- .../org/apache/fory/serializer/ObjectStreamSerializer.java | 2 +- .../apache/fory/serializer/ReplaceResolveSerializer.java | 2 +- .../apache/fory/serializer/SerializedLambdaSerializer.java | 2 +- .../main/java/org/apache/fory/serializer/Serializers.java | 2 +- .../java/org/apache/fory/serializer/StringSerializer.java | 2 +- .../fory/serializer/collection/CollectionSerializers.java | 2 +- .../collection/ImmutableCollectionSerializers.java | 2 +- .../main/java/org/apache/fory/util/ClassLoaderUtils.java | 2 +- .../main/java/org/apache/fory/util/DefaultValueUtils.java | 2 +- .../main/java/org/apache/fory/util/function/Functions.java | 2 +- .../main/java/org/apache/fory/util/record/RecordUtils.java | 2 +- .../org.apache.fory/fory-core/native-image.properties | 6 +++--- java/fory-core/src/test/java/org/apache/fory/TestUtils.java | 2 +- .../org/apache/fory/collection/MultiKeyWeakMapTest.java | 2 +- .../{util/unsafe => platform/internal}/DefineClassTest.java | 2 +- .../{util/unsafe => platform/internal}/JDKAccessTest.java | 2 +- .../org/apache/fory/serializer/StringSerializerTest.java | 2 +- .../fory/extension/serializer/ProtobufSerializer.java | 2 +- .../apache/fory/format/type/CustomTypeEncoderRegistry.java | 2 +- .../org/apache/fory/serializer/scala/RangeSerializer.scala | 2 +- 30 files changed, 32 insertions(+), 32 deletions(-) rename java/fory-core/src/main/java/org/apache/fory/{util/unsafe => platform/internal}/DefineClass.java (98%) rename java/fory-core/src/main/java/org/apache/fory/{util/unsafe => platform/internal}/_JDKAccess.java (99%) rename java/fory-core/src/main/java/org/apache/fory/{util/unsafe => platform/internal}/_Lookup.java (99%) rename java/fory-core/src/test/java/org/apache/fory/{util/unsafe => platform/internal}/DefineClassTest.java (98%) rename java/fory-core/src/test/java/org/apache/fory/{util/unsafe => platform/internal}/JDKAccessTest.java (99%) 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 9194937c4e..cf626123b1 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,7 +23,7 @@ import java.io.ByteArrayOutputStream; import java.nio.ByteBuffer; import org.apache.fory.platform.AndroidSupport; -import org.apache.fory.util.unsafe._JDKAccess; +import org.apache.fory.platform.internal._JDKAccess; /** Memory utils for fory. */ public class MemoryUtils { 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 index 7ca7d09bd0..8e214946bc 100644 --- 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 @@ -22,8 +22,8 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; import org.apache.fory.annotation.Internal; +import org.apache.fory.platform.internal._JDKAccess; 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. 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 98% 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..c47ffa9d44 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; diff --git a/java/fory-core/src/main/java/org/apache/fory/util/unsafe/_JDKAccess.java b/java/fory-core/src/main/java/org/apache/fory/platform/internal/_JDKAccess.java similarity index 99% rename from java/fory-core/src/main/java/org/apache/fory/util/unsafe/_JDKAccess.java rename to java/fory-core/src/main/java/org/apache/fory/platform/internal/_JDKAccess.java index 4787acc98d..514a8a9d57 100644 --- a/java/fory-core/src/main/java/org/apache/fory/util/unsafe/_JDKAccess.java +++ b/java/fory-core/src/main/java/org/apache/fory/platform/internal/_JDKAccess.java @@ -17,7 +17,7 @@ * under the License. */ -package org.apache.fory.util.unsafe; +package org.apache.fory.platform.internal; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; 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 2ff89fc529..b4919e5da2 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 @@ -38,6 +38,7 @@ 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; @@ -46,7 +47,6 @@ import org.apache.fory.util.function.ToFloatFunction; import org.apache.fory.util.function.ToShortFunction; import org.apache.fory.util.record.RecordUtils; -import org.apache.fory.util.unsafe._JDKAccess; /** Field accessor for primitive types and object types. */ @SuppressWarnings({"unchecked", "rawtypes"}) 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..065a82b237 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 @@ -30,8 +30,8 @@ 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.util.record.RecordUtils; -import org.apache.fory.util.unsafe._JDKAccess; /** * Factory class for creating and caching {@link ObjectCreator} instances. 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 e09b3361bd..de19e8e262 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 @@ -53,11 +53,11 @@ 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 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 e648da8716..4c32c84e94 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 @@ -42,12 +42,12 @@ 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.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"}) 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/ObjectStreamSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/ObjectStreamSerializer.java index fabaa64afd..a5f1fe852a 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 @@ -62,6 +62,7 @@ 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.ObjectCreator; import org.apache.fory.reflect.ObjectCreators; import org.apache.fory.reflect.ReflectionUtils; @@ -74,7 +75,6 @@ import org.apache.fory.type.Types; import org.apache.fory.util.ExceptionUtils; import org.apache.fory.util.Preconditions; -import org.apache.fory.util.unsafe._JDKAccess; /** * Implement jdk custom serialization only if following conditions are met: 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..03d6492efa 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 @@ -37,13 +37,13 @@ import org.apache.fory.logging.LoggerFactory; import org.apache.fory.memory.MemoryBuffer; import org.apache.fory.platform.AndroidSupport; +import org.apache.fory.platform.internal._JDKAccess; 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.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 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..ec8c3a604e 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 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..0f534f79bd 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 @@ -51,6 +51,7 @@ import org.apache.fory.meta.TypeDef; import org.apache.fory.platform.AndroidSupport; import org.apache.fory.platform.GraalvmSupport; +import org.apache.fory.platform.internal._JDKAccess; import org.apache.fory.reflect.ReflectionUtils; import org.apache.fory.resolver.ClassResolver; import org.apache.fory.resolver.TypeInfo; @@ -66,7 +67,6 @@ 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"}) 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 5d423a44fb..f2fcac8805 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 @@ -45,11 +45,11 @@ import org.apache.fory.platform.AndroidSupport; import org.apache.fory.platform.JdkVersion; import org.apache.fory.platform.UnsafeOps; +import org.apache.fory.platform.internal._JDKAccess; import org.apache.fory.util.MathUtils; import org.apache.fory.util.Preconditions; import org.apache.fory.util.StringEncodingUtils; import org.apache.fory.util.StringUtils; -import org.apache.fory.util.unsafe._JDKAccess; /** * String serializer based on {@link sun.misc.Unsafe} and {@link MethodHandle} for speed. 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 853c9071f9..16a420fd97 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 @@ -60,6 +60,7 @@ import org.apache.fory.memory.MemoryUtils; 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.ReflectionUtils; import org.apache.fory.resolver.ClassResolver; @@ -71,7 +72,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 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..a5b4dc539e 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 @@ -34,9 +34,9 @@ import org.apache.fory.memory.MemoryBuffer; import org.apache.fory.platform.AndroidSupport; 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"}) 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 a6713e8717..59c3088862 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,12 +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.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. 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/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..9ffc90564f 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 @@ -534,9 +534,9 @@ Args=--initialize-at-build-time=org.apache.fory.annotation.ForyField$Dynamic,\ org.apache.fory.util.record.RecordUtils$2,\ org.apache.fory.util.record.RecordUtils$3,\ org.apache.fory.util.record.RecordUtils,\ - org.apache.fory.util.unsafe._JDKAccess$1,\ - org.apache.fory.util.unsafe._JDKAccess,\ - org.apache.fory.util.unsafe._Lookup,\ + org.apache.fory.platform.internal._JDKAccess$1,\ + 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/TestUtils.java b/java/fory-core/src/test/java/org/apache/fory/TestUtils.java index 4ea85a98ad..2a2e5f1e19 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 @@ -32,10 +32,10 @@ import org.apache.fory.collection.Tuple3; import org.apache.fory.meta.TypeDef; import org.apache.fory.platform.UnsafeOps; +import org.apache.fory.platform.internal._JDKAccess; import org.apache.fory.reflect.FieldAccessor; 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. */ 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/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/serializer/StringSerializerTest.java b/java/fory-core/src/test/java/org/apache/fory/serializer/StringSerializerTest.java index b621677bad..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,9 +36,9 @@ import org.apache.fory.memory.MemoryBuffer; import org.apache.fory.memory.MemoryUtils; import org.apache.fory.platform.JdkVersion; +import org.apache.fory.platform.internal._JDKAccess; import org.apache.fory.util.MathUtils; import org.apache.fory.util.StringUtils; -import org.apache.fory.util.unsafe._JDKAccess; import org.testng.Assert; import org.testng.SkipException; import org.testng.annotations.DataProvider; 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/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/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 From 26dec9d6a03d72c717bb3bc4e8c15e4a8d81bb14 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 23 May 2026 23:51:51 +0800 Subject: [PATCH 12/34] fix(java): add JDK25 unsafe replacements --- .../org/apache/fory/platform/UnsafeOps.java | 419 ++++++++++++++++ .../fory/platform/internal/_JDKAccess.java | 470 ++++++++++++++++++ .../fory/platform/internal/_Lookup.java | 69 +++ 3 files changed, 958 insertions(+) create mode 100644 java/fory-core/src/main/java25/org/apache/fory/platform/UnsafeOps.java create mode 100644 java/fory-core/src/main/java25/org/apache/fory/platform/internal/_JDKAccess.java create mode 100644 java/fory-core/src/main/java25/org/apache/fory/platform/internal/_Lookup.java diff --git a/java/fory-core/src/main/java25/org/apache/fory/platform/UnsafeOps.java b/java/fory-core/src/main/java25/org/apache/fory/platform/UnsafeOps.java new file mode 100644 index 0000000000..ce00b9b6c7 --- /dev/null +++ b/java/fory-core/src/main/java25/org/apache/fory/platform/UnsafeOps.java @@ -0,0 +1,419 @@ +/* + * 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.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.lang.reflect.Field; +import java.nio.ByteOrder; +import org.apache.fory.annotation.Internal; +import org.apache.fory.platform.internal._JDKAccess; +import sun.misc.Unsafe; + +/** A utility class for array memory operations on JDK25+. */ +@Internal +@SuppressWarnings("restriction") +public final class UnsafeOps { + @SuppressWarnings("restriction") + public static final Unsafe UNSAFE = _JDKAccess.UNSAFE; + + public static final int BOOLEAN_ARRAY_OFFSET = 0; + public static final int BYTE_ARRAY_OFFSET = 0; + public static final int CHAR_ARRAY_OFFSET = 0; + public static final int SHORT_ARRAY_OFFSET = 0; + public static final int INT_ARRAY_OFFSET = 0; + public static final int LONG_ARRAY_OFFSET = 0; + public static final int FLOAT_ARRAY_OFFSET = 0; + public static final int DOUBLE_ARRAY_OFFSET = 0; + private static final boolean BIG_ENDIAN = ByteOrder.nativeOrder() == ByteOrder.BIG_ENDIAN; + private static final VarHandle BYTE_ARRAY_CHAR = + MethodHandles.byteArrayViewVarHandle(char[].class, ByteOrder.nativeOrder()); + private static final VarHandle BYTE_ARRAY_SHORT = + MethodHandles.byteArrayViewVarHandle(short[].class, ByteOrder.nativeOrder()); + private static final VarHandle BYTE_ARRAY_INT = + MethodHandles.byteArrayViewVarHandle(int[].class, ByteOrder.nativeOrder()); + private static final VarHandle BYTE_ARRAY_LONG = + MethodHandles.byteArrayViewVarHandle(long[].class, ByteOrder.nativeOrder()); + private static final VarHandle BYTE_ARRAY_FLOAT = + MethodHandles.byteArrayViewVarHandle(float[].class, ByteOrder.nativeOrder()); + private static final VarHandle BYTE_ARRAY_DOUBLE = + MethodHandles.byteArrayViewVarHandle(double[].class, ByteOrder.nativeOrder()); + private static final boolean unaligned; + + private UnsafeOps() {} + + static { + String arch = System.getProperty("os.arch", ""); + if ("ppc64le".equals(arch) || "ppc64".equals(arch) || "s390x".equals(arch)) { + unaligned = true; + } else { + unaligned = arch.matches("^(i[3-6]86|x86(_64)?|x64|amd64|aarch64)$"); + } + } + + /** + * Returns true when the underlying system is known to support unaligned access. JDK25 array + * accessors do not use Unsafe, but callers keep this gate for vectorized array scans. + */ + public static boolean unaligned() { + return unaligned; + } + + public static long objectFieldOffset(Field f) { + throw unsupportedObjectMemory(); + } + + public static int getInt(Object object, long offset) { + if (object instanceof byte[]) { + return (int) BYTE_ARRAY_INT.get((byte[]) object, toIntIndex(offset)); + } + return getIntFromArray(object, offset); + } + + public static void putInt(Object object, long offset, int value) { + if (object instanceof byte[]) { + BYTE_ARRAY_INT.set((byte[]) object, toIntIndex(offset), value); + return; + } + putIntToArray(object, offset, value); + } + + public static boolean getBoolean(Object object, long offset) { + if (object instanceof boolean[]) { + return ((boolean[]) object)[toIntIndex(offset)]; + } + return getByte(object, offset) != 0; + } + + public static void putBoolean(Object object, long offset, boolean value) { + if (object instanceof boolean[]) { + ((boolean[]) object)[toIntIndex(offset)] = value; + return; + } + putByte(object, offset, value ? (byte) 1 : (byte) 0); + } + + public static byte getByte(Object object, long offset) { + return getArrayByte(object, offset); + } + + public static void putByte(Object object, long offset, byte value) { + putArrayByte(object, offset, value); + } + + public static short getShort(Object object, long offset) { + if (object instanceof byte[]) { + return (short) BYTE_ARRAY_SHORT.get((byte[]) object, toIntIndex(offset)); + } + return (short) getIntN(object, offset, Short.BYTES); + } + + public static void putShort(Object object, long offset, short value) { + if (object instanceof byte[]) { + BYTE_ARRAY_SHORT.set((byte[]) object, toIntIndex(offset), value); + return; + } + putIntN(object, offset, value, Short.BYTES); + } + + public static char getChar(Object obj, long offset) { + if (obj instanceof byte[]) { + return (char) BYTE_ARRAY_CHAR.get((byte[]) obj, toIntIndex(offset)); + } + return (char) getIntN(obj, offset, Character.BYTES); + } + + public static void putChar(Object obj, long offset, char value) { + if (obj instanceof byte[]) { + BYTE_ARRAY_CHAR.set((byte[]) obj, toIntIndex(offset), value); + return; + } + putIntN(obj, offset, value, Character.BYTES); + } + + public static long getLong(Object object, long offset) { + if (object instanceof byte[]) { + return (long) BYTE_ARRAY_LONG.get((byte[]) object, toIntIndex(offset)); + } + long value = 0; + if (BIG_ENDIAN) { + for (int i = 0; i < Long.BYTES; i++) { + value = (value << Byte.SIZE) | (getArrayByte(object, offset + i) & 0xffL); + } + } else { + for (int i = Long.BYTES - 1; i >= 0; i--) { + value = (value << Byte.SIZE) | (getArrayByte(object, offset + i) & 0xffL); + } + } + return value; + } + + public static void putLong(Object object, long offset, long value) { + if (object instanceof byte[]) { + BYTE_ARRAY_LONG.set((byte[]) object, toIntIndex(offset), value); + return; + } + if (BIG_ENDIAN) { + for (int i = Long.BYTES - 1; i >= 0; i--) { + putArrayByte(object, offset + i, (byte) value); + value >>>= Byte.SIZE; + } + } else { + for (int i = 0; i < Long.BYTES; i++) { + putArrayByte(object, offset + i, (byte) value); + value >>>= Byte.SIZE; + } + } + } + + public static float getFloat(Object object, long offset) { + if (object instanceof byte[]) { + return (float) BYTE_ARRAY_FLOAT.get((byte[]) object, toIntIndex(offset)); + } + return Float.intBitsToFloat(getInt(object, offset)); + } + + public static void putFloat(Object object, long offset, float value) { + if (object instanceof byte[]) { + BYTE_ARRAY_FLOAT.set((byte[]) object, toIntIndex(offset), value); + return; + } + putInt(object, offset, Float.floatToRawIntBits(value)); + } + + public static double getDouble(Object object, long offset) { + if (object instanceof byte[]) { + return (double) BYTE_ARRAY_DOUBLE.get((byte[]) object, toIntIndex(offset)); + } + return Double.longBitsToDouble(getLong(object, offset)); + } + + public static void putDouble(Object object, long offset, double value) { + if (object instanceof byte[]) { + BYTE_ARRAY_DOUBLE.set((byte[]) object, toIntIndex(offset), value); + return; + } + putLong(object, offset, Double.doubleToRawLongBits(value)); + } + + public static Object getObject(Object o, long offset) { + throw unsupportedObjectMemory(); + } + + public static void putObject(Object object, long offset, Object value) { + throw unsupportedObjectMemory(); + } + + public static void copyMemory( + Object src, long srcOffset, Object dst, long dstOffset, long length) { + if (src == null || dst == null) { + throw unsupportedNativeMemory(); + } + if (length < 0) { + throw new IllegalArgumentException("length must be non-negative: " + length); + } + int len = toIntLength(length); + if (src instanceof byte[] && dst instanceof byte[]) { + System.arraycopy((byte[]) src, toIntIndex(srcOffset), (byte[]) dst, toIntIndex(dstOffset), len); + return; + } + if (!isPrimitiveArray(src) || !isPrimitiveArray(dst)) { + throw unsupportedObjectMemory(); + } + if (src == dst && srcOffset < dstOffset && dstOffset < srcOffset + length) { + for (long i = length - 1; i >= 0; i--) { + putArrayByte(dst, dstOffset + i, getArrayByte(src, srcOffset + i)); + } + } else { + for (long i = 0; i < length; i++) { + putArrayByte(dst, dstOffset + i, getArrayByte(src, srcOffset + i)); + } + } + } + + /** Create an instance of type. This method does not call constructor. */ + public static T newInstance(Class type) { + throw new UnsupportedOperationException( + "Constructor-bypassing allocation is unsupported on JDK25 without sun.misc.Unsafe"); + } + + private static int getIntFromArray(Object object, long offset) { + return getIntN(object, offset, Integer.BYTES); + } + + private static void putIntToArray(Object object, long offset, int value) { + putIntN(object, offset, value, Integer.BYTES); + } + + private static int getIntN(Object object, long offset, int bytes) { + int value = 0; + if (BIG_ENDIAN) { + for (int i = 0; i < bytes; i++) { + value = (value << Byte.SIZE) | (getArrayByte(object, offset + i) & 0xff); + } + } else { + for (int i = bytes - 1; i >= 0; i--) { + value = (value << Byte.SIZE) | (getArrayByte(object, offset + i) & 0xff); + } + } + return value; + } + + private static void putIntN(Object object, long offset, int value, int bytes) { + if (BIG_ENDIAN) { + for (int i = bytes - 1; i >= 0; i--) { + putArrayByte(object, offset + i, (byte) value); + value >>>= Byte.SIZE; + } + } else { + for (int i = 0; i < bytes; i++) { + putArrayByte(object, offset + i, (byte) value); + value >>>= Byte.SIZE; + } + } + } + + private static byte getArrayByte(Object object, long offset) { + checkOffset(offset); + if (object instanceof byte[]) { + return ((byte[]) object)[toIntIndex(offset)]; + } else if (object instanceof boolean[]) { + return ((boolean[]) object)[toIntIndex(offset)] ? (byte) 1 : (byte) 0; + } else if (object instanceof char[]) { + return getIntByte( + ((char[]) object)[toIntIndex(offset / Character.BYTES)], offset, Character.BYTES); + } else if (object instanceof short[]) { + return getIntByte(((short[]) object)[toIntIndex(offset / Short.BYTES)], offset, Short.BYTES); + } else if (object instanceof int[]) { + return getIntByte( + ((int[]) object)[toIntIndex(offset / Integer.BYTES)], offset, Integer.BYTES); + } else if (object instanceof long[]) { + return getLongByte(((long[]) object)[toIntIndex(offset / Long.BYTES)], offset); + } else if (object instanceof float[]) { + int value = Float.floatToRawIntBits(((float[]) object)[toIntIndex(offset / Float.BYTES)]); + return getIntByte(value, offset, Float.BYTES); + } else if (object instanceof double[]) { + long value = + Double.doubleToRawLongBits(((double[]) object)[toIntIndex(offset / Double.BYTES)]); + return getLongByte(value, offset); + } + throw unsupportedObjectMemory(); + } + + private static void putArrayByte(Object object, long offset, byte value) { + checkOffset(offset); + if (object instanceof byte[]) { + ((byte[]) object)[toIntIndex(offset)] = value; + } else if (object instanceof boolean[]) { + ((boolean[]) object)[toIntIndex(offset)] = value != 0; + } else if (object instanceof char[]) { + char[] array = (char[]) object; + int index = toIntIndex(offset / Character.BYTES); + array[index] = (char) setIntByte(array[index], offset, value, Character.BYTES); + } else if (object instanceof short[]) { + short[] array = (short[]) object; + int index = toIntIndex(offset / Short.BYTES); + array[index] = (short) setIntByte(array[index], offset, value, Short.BYTES); + } else if (object instanceof int[]) { + int[] array = (int[]) object; + int index = toIntIndex(offset / Integer.BYTES); + array[index] = setIntByte(array[index], offset, value, Integer.BYTES); + } else if (object instanceof long[]) { + long[] array = (long[]) object; + int index = toIntIndex(offset / Long.BYTES); + array[index] = setLongByte(array[index], offset, value); + } else if (object instanceof float[]) { + float[] array = (float[]) object; + int index = toIntIndex(offset / Float.BYTES); + int bits = Float.floatToRawIntBits(array[index]); + array[index] = Float.intBitsToFloat(setIntByte(bits, offset, value, Float.BYTES)); + } else if (object instanceof double[]) { + double[] array = (double[]) object; + int index = toIntIndex(offset / Double.BYTES); + long bits = Double.doubleToRawLongBits(array[index]); + array[index] = Double.longBitsToDouble(setLongByte(bits, offset, value)); + } else { + throw unsupportedObjectMemory(); + } + } + + private static byte getIntByte(int value, long offset, int width) { + int shift = byteShift(offset, width); + return (byte) (value >>> shift); + } + + private static byte getLongByte(long value, long offset) { + int shift = byteShift(offset, Long.BYTES); + return (byte) (value >>> shift); + } + + private static int setIntByte(int oldValue, long offset, byte value, int width) { + int shift = byteShift(offset, width); + int mask = 0xff << shift; + return (oldValue & ~mask) | ((value & 0xff) << shift); + } + + private static long setLongByte(long oldValue, long offset, byte value) { + int shift = byteShift(offset, Long.BYTES); + long mask = 0xffL << shift; + return (oldValue & ~mask) | ((long) (value & 0xff) << shift); + } + + private static int byteShift(long offset, int width) { + int byteIndex = (int) Math.floorMod(offset, width); + return (BIG_ENDIAN ? width - 1 - byteIndex : byteIndex) * Byte.SIZE; + } + + private static boolean isPrimitiveArray(Object object) { + Class cls = object.getClass(); + return cls.isArray() && cls.getComponentType().isPrimitive(); + } + + 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 toIntLength(long length) { + if (length > Integer.MAX_VALUE) { + throw new IndexOutOfBoundsException("length out of int range: " + length); + } + return (int) length; + } + + private static void checkOffset(long offset) { + if (offset < 0) { + throw new IndexOutOfBoundsException("offset must be non-negative: " + offset); + } + } + + private static UnsupportedOperationException unsupportedObjectMemory() { + return new UnsupportedOperationException( + "Object field and reference-offset memory access is unsupported on JDK25 without " + + "sun.misc.Unsafe"); + } + + private static UnsupportedOperationException unsupportedNativeMemory() { + return new UnsupportedOperationException( + "Raw native-address memory access is unsupported on JDK25 without sun.misc.Unsafe"); + } +} 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..7bdb2ac158 --- /dev/null +++ b/java/fory-core/src/main/java25/org/apache/fory/platform/internal/_JDKAccess.java @@ -0,0 +1,470 @@ +/* + * 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.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.invoke.VarHandle; +import java.lang.reflect.Field; +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.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 Class _INNER_UNSAFE_CLASS = null; + public static final Object _INNER_UNSAFE = null; + + 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; + private static final long STRING_VALUE_FIELD_OFFSET = -1; + private static final long STRING_COUNT_FIELD_OFFSET = -1; + private static final long STRING_OFFSET_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; + + 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; + } + FieldHandles handles = initFieldHandles(valueField.getType(), countField, offsetField); + JDK_INTERNAL_FIELD_ACCESS = handles != null; + STRING_VALUE_HANDLE = handles == null ? null : handles.stringValue; + STRING_CODER_HANDLE = handles == null ? null : handles.stringCoder; + STRING_COUNT_HANDLE = handles == null ? null : handles.stringCount; + STRING_OFFSET_HANDLE = handles == null ? null : handles.stringOffset; + BAS_BUF_HANDLE = handles == null ? null : handles.basBuf; + BAS_COUNT_HANDLE = handles == null ? null : handles.basCount; + BIS_BUF_HANDLE = handles == null ? null : handles.bisBuf; + BIS_POS_HANDLE = handles == null ? null : handles.bisPos; + BIS_COUNT_HANDLE = handles == null ? null : handles.bisCount; + } 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 FieldHandles initFieldHandles( + Class stringValueType, Field countField, Field offsetField) { + try { + Lookup stringLookup = MethodHandles.privateLookupIn(String.class, MethodHandles.lookup()); + VarHandle stringValue = stringLookup.findVarHandle(String.class, "value", stringValueType); + VarHandle stringCoder = + STRING_VALUE_FIELD_IS_BYTES + ? stringLookup.findVarHandle(String.class, "coder", byte.class) + : null; + VarHandle stringCount = + countField == null ? null : stringLookup.findVarHandle(String.class, "count", int.class); + VarHandle stringOffset = + offsetField == null + ? null + : stringLookup.findVarHandle(String.class, "offset", int.class); + Lookup basLookup = + MethodHandles.privateLookupIn(ByteArrayOutputStream.class, MethodHandles.lookup()); + Lookup bisLookup = + MethodHandles.privateLookupIn(ByteArrayInputStream.class, MethodHandles.lookup()); + return new FieldHandles( + stringValue, + stringCoder, + stringCount, + stringOffset, + 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 class FieldHandles { + private final VarHandle stringValue; + private final VarHandle stringCoder; + private final VarHandle stringCount; + private final VarHandle stringOffset; + private final VarHandle basBuf; + private final VarHandle basCount; + private final VarHandle bisBuf; + private final VarHandle bisPos; + private final VarHandle bisCount; + + private FieldHandles( + VarHandle stringValue, + VarHandle stringCoder, + VarHandle stringCount, + VarHandle stringOffset, + VarHandle basBuf, + VarHandle basCount, + VarHandle bisBuf, + VarHandle bisPos, + VarHandle bisCount) { + this.stringValue = stringValue; + this.stringCoder = stringCoder; + this.stringCount = stringCount; + this.stringOffset = stringOffset; + this.basBuf = basBuf; + this.basCount = basCount; + this.bisBuf = bisBuf; + this.bisPos = bisPos; + this.bisCount = bisCount; + } + } + + public static Object getStringValue(String value) { + checkInternalFieldAccess("String.value"); + return STRING_VALUE_HANDLE.get(value); + } + + public static byte getStringCoder(String value) { + checkInternalFieldAccess("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) { + checkInternalFieldAccess("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) { + checkInternalFieldAccess("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 void wrap(ByteArrayOutputStream stream, MemoryBuffer buffer) { + Preconditions.checkNotNull(stream); + checkInternalFieldAccess("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); + checkInternalFieldAccess("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); + checkInternalFieldAccess("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); + } + + private static void checkInternalFieldAccess(String target) { + if (!JDK_INTERNAL_FIELD_ACCESS) { + throw new UnsupportedOperationException( + target + + " private access is unavailable; open java.base/java.lang and java.base/java.io " + + "to org.apache.fory.core"); + } + } + + 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 (ClassNotFoundException | NoClassDefFoundError e) { + return makeGetterFunction(lookup, handle, Object.class); + } 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); + } +} 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..0d3a669321 --- /dev/null +++ b/java/fory-core/src/main/java25/org/apache/fory/platform/internal/_Lookup.java @@ -0,0 +1,69 @@ +/* + * 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 { + 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"; + } +} From 0947efd2cdfc9e77154c34d617926aeb960b5383 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 23 May 2026 23:59:21 +0800 Subject: [PATCH 13/34] feat(java): add JDK25 field accessor --- .../apache/fory/reflect/FieldAccessor.java | 1341 +++++++++++++++++ 1 file changed, 1341 insertions(+) create mode 100644 java/fory-core/src/main/java25/org/apache/fory/reflect/FieldAccessor.java 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..3f0e9a2ff7 --- /dev/null +++ b/java/fory-core/src/main/java25/org/apache/fory/reflect/FieldAccessor.java @@ -0,0 +1,1341 @@ +/* + * 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.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 { + protected final Field field; + protected final long fieldOffset; + + public FieldAccessor(Field field) { + this(field, -1); + } + + protected FieldAccessor(Field field, long fieldOffset) { + this.field = field; + this.fieldOffset = fieldOffset; + Preconditions.checkNotNull(field); + } + + public abstract Object get(Object obj); + + public void set(Object obj, Object value) { + throw new UnsupportedOperationException("Unsupported for field " + field); + } + + 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 void putObject(Object targetObject, Object object) { + set(targetObject, object); + } + + public 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()) { + return new GeneratedAccessor(field); + } + 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) { + MethodHandles.Lookup lookup = privateLookup(field); + try { + if (Modifier.isStatic(field.getModifiers())) { + return lookup.findStaticVarHandle( + field.getDeclaringClass(), field.getName(), field.getType()); + } + return lookup.findVarHandle(field.getDeclaringClass(), field.getName(), field.getType()); + } catch (IllegalAccessException e) { + throw accessFailure(field, e); + } catch (NoSuchFieldException e) { + throw new IllegalStateException("Failed to create VarHandle for field " + field, e); + } + } + + private static MethodHandle recordGetter(Field field) { + MethodHandles.Lookup lookup = privateLookup(field); + try { + return lookup.findVirtual( + field.getDeclaringClass(), field.getName(), MethodType.methodType(field.getType())); + } catch (IllegalAccessException e) { + throw accessFailure(field, e); + } catch (NoSuchMethodException e) { + throw new IllegalStateException("Failed to find record accessor for field " + field, e); + } + } + + private static MethodHandles.Lookup privateLookup(Field field) { + Class declaringClass = field.getDeclaringClass(); + try { + return MethodHandles.privateLookupIn(declaringClass, MethodHandles.lookup()); + } catch (IllegalAccessException e) { + throw accessFailure(field, e); + } + } + + 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(); + return new IllegalStateException( + "Cannot access field " + + field + + " because package " + + packageName + + " in module " + + moduleName(targetModule) + + " is not open to " + + moduleName(FieldAccessor.class.getModule()), + 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 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; + + VarHandleAccessor(Field field) { + super(field, -1); + handle = fieldHandle(field); + isStatic = Modifier.isStatic(field.getModifiers()); + } + } + + /** 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) { + try { + if (isStatic) { + handle.set(value); + } else { + checkObj(obj); + handle.set(obj, value); + } + } catch (UnsupportedOperationException e) { + throw unsupportedWrite(field, 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) { + try { + if (isStatic) { + handle.set(value); + } else { + checkObj(obj); + handle.set(obj, value); + } + } catch (UnsupportedOperationException e) { + throw unsupportedWrite(field, 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) { + try { + if (isStatic) { + handle.set(value); + } else { + checkObj(obj); + handle.set(obj, value); + } + } catch (UnsupportedOperationException e) { + throw unsupportedWrite(field, 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) { + try { + if (isStatic) { + handle.set(value); + } else { + checkObj(obj); + handle.set(obj, value); + } + } catch (UnsupportedOperationException e) { + throw unsupportedWrite(field, 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) { + try { + if (isStatic) { + handle.set(value); + } else { + checkObj(obj); + handle.set(obj, value); + } + } catch (UnsupportedOperationException e) { + throw unsupportedWrite(field, 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) { + try { + if (isStatic) { + handle.set(value); + } else { + checkObj(obj); + handle.set(obj, value); + } + } catch (UnsupportedOperationException e) { + throw unsupportedWrite(field, 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) { + try { + if (isStatic) { + handle.set(value); + } else { + checkObj(obj); + handle.set(obj, value); + } + } catch (UnsupportedOperationException e) { + throw unsupportedWrite(field, 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) { + try { + if (isStatic) { + handle.set(value); + } else { + checkObj(obj); + handle.set(obj, value); + } + } catch (UnsupportedOperationException e) { + throw unsupportedWrite(field, 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) { + try { + if (isStatic) { + handle.set(value); + } else { + checkObj(obj); + handle.set(obj, value); + } + } catch (UnsupportedOperationException e) { + throw unsupportedWrite(field, 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) { + try { + if (isStatic) { + handle.set(value); + } else { + checkObj(obj); + handle.set(obj, value); + } + } catch (UnsupportedOperationException e) { + throw unsupportedWrite(field, 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) { + try { + if (isStatic) { + handle.set(value); + } else { + checkObj(obj); + handle.set(obj, value); + } + } catch (UnsupportedOperationException e) { + throw unsupportedWrite(field, 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) { + try { + if (isStatic) { + handle.set(value); + } else { + checkObj(obj); + handle.set(obj, value); + } + } catch (UnsupportedOperationException e) { + throw unsupportedWrite(field, 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) { + try { + if (isStatic) { + handle.set(value); + } else { + checkObj(obj); + handle.set(obj, value); + } + } catch (UnsupportedOperationException e) { + throw unsupportedWrite(field, 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) { + try { + if (isStatic) { + handle.set(value); + } else { + checkObj(obj); + handle.set(obj, value); + } + } catch (UnsupportedOperationException e) { + throw unsupportedWrite(field, 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) { + try { + if (isStatic) { + handle.set(value); + } else { + checkObj(obj); + handle.set(obj, value); + } + } catch (UnsupportedOperationException e) { + throw unsupportedWrite(field, 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) { + try { + if (isStatic) { + handle.set(value); + } else { + checkObj(obj); + handle.set(obj, value); + } + } catch (UnsupportedOperationException e) { + throw unsupportedWrite(field, 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) { + try { + if (isStatic) { + handle.set(value); + } else { + checkObj(obj); + handle.set(obj, value); + } + } catch (UnsupportedOperationException e) { + throw unsupportedWrite(field, 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) { + try { + if (isStatic) { + handle.set(value); + } else { + checkObj(obj); + handle.set(obj, value); + } + } catch (UnsupportedOperationException e) { + throw unsupportedWrite(field, e); + } catch (RuntimeException e) { + throw accessorFailure(field, e); + } + } + } +} From 1ed30ddb0cd107f62c0ee5e497cf8b8da69ff25b Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 24 May 2026 14:16:03 +0800 Subject: [PATCH 14/34] feat(java): add JDK25 zero-unsafe runtime path --- .github/workflows/release-java-snapshot.yaml | 6 +- benchmarks/java/pom.xml | 152 + .../fory/benchmark/Jdk25MrJarCheck.java | 50 + .../fory/benchmark/NewJava11StringSuite.java | 20 +- .../apache/fory/benchmark/NewStringSuite.java | 12 +- ci/deploy.sh | 11 + ci/run_ci.sh | 114 +- ci/tasks/java.py | 135 +- docs/guide/java/troubleshooting.md | 44 + integration_tests/graalvm_tests/pom.xml | 33 + .../fory/graalvm/FeatureTestExample.java | 2 + .../java/org/apache/fory/graalvm/Foo.java | 2 + .../fory/graalvm/ObjectStreamExample.java | 12 +- .../jdk_compatibility_tests/.gitignore | 4 + .../jdk_compatibility_tests/pom.xml | 33 + integration_tests/jpms_tests/pom.xml | 33 + .../jpms_tests/src/main/java/module-info.java | 4 + .../model/PrivateFieldBean.java | 32 + .../PublicSerializerValue.java | 28 + .../PublicSerializerValueSerializer.java | 41 + .../JpmsFieldAccessorTest.java | 50 + java/fory-core/pom.xml | 260 + .../src/main/java/org/apache/fory/Fory.java | 2 +- .../org/apache/fory/builder/CodecBuilder.java | 10 +- .../fory/builder/CompatibleCodecBuilder.java | 91 +- .../fory/builder/ObjectCodecBuilder.java | 380 +- .../org/apache/fory/collection/MapEntry.java | 2 + .../org/apache/fory/context/CopyContext.java | 23 +- .../org/apache/fory/context/MapRefReader.java | 86 +- .../org/apache/fory/context/ReadContext.java | 30 + .../org/apache/fory/context/RefReader.java | 29 + .../org/apache/fory/memory/MemoryUtils.java | 22 +- .../java/org/apache/fory/meta/TypeDef.java | 3 - .../org/apache/fory/platform/UnsafeOps.java | 6 + .../fory/platform/internal/DefineClass.java | 5 + .../fory/platform/internal/_JDKAccess.java | 216 +- .../apache/fory/reflect/FieldAccessor.java | 108 +- .../apache/fory/reflect/ObjectCreator.java | 32 + .../apache/fory/reflect/ObjectCreators.java | 442 +- .../apache/fory/reflect/ReflectionUtils.java | 31 - .../apache/fory/resolver/ClassResolver.java | 73 +- .../serializer/AbstractObjectSerializer.java | 361 +- .../CompatibleLayerSerializerBase.java | 25 +- .../fory/serializer/CompatibleSerializer.java | 234 +- .../fory/serializer/ExceptionSerializers.java | 64 +- .../FinalFieldReplaceResolveSerializer.java | 4 +- .../fory/serializer/JavaSerializer.java | 33 +- .../fory/serializer/JdkProxySerializer.java | 4 +- .../fory/serializer/ObjectSerializer.java | 141 +- .../serializer/ObjectStreamSerializer.java | 109 +- .../serializer/ReplaceResolveSerializer.java | 92 +- .../apache/fory/serializer/Serializers.java | 89 + .../fory/serializer/StringSerializer.java | 157 +- .../apache/fory/serializer/URLSerializer.java | 15 +- .../collection/ChildContainerSerializers.java | 24 +- .../collection/CollectionSerializers.java | 60 +- .../GuavaCollectionSerializers.java | 206 +- .../ImmutableCollectionSerializers.java | 18 +- .../serializer/collection/MapSerializers.java | 5 +- .../collection/SubListSerializers.java | 2 +- .../collection/SynchronizedSerializers.java | 12 +- .../collection/UnmodifiableSerializers.java | 12 +- .../java/org/apache/fory/type/BFloat16.java | 2 + .../org/apache/fory/type/BFloat16Array.java | 6 + .../java/org/apache/fory/type/Float16.java | 2 + .../org/apache/fory/type/Float16Array.java | 6 + .../org/apache/fory/type/unsigned/UInt16.java | 2 + .../org/apache/fory/type/unsigned/UInt32.java | 2 + .../org/apache/fory/type/unsigned/UInt64.java | 2 + .../org/apache/fory/type/unsigned/UInt8.java | 2 + .../apache/fory/util/DefaultValueUtils.java | 108 +- .../org/apache/fory/builder/CodecBuilder.java | 732 +++ .../fory/builder/ObjectCodecBuilder.java | 1465 ++++++ .../org/apache/fory/memory/MemoryBuffer.java | 4521 +++++++++++++++++ .../org/apache/fory/platform/UnsafeOps.java | 80 +- .../fory/platform/internal/DefineClass.java | 110 + .../fory/platform/internal/_JDKAccess.java | 519 +- .../fory/platform/internal/_Lookup.java | 7 +- .../apache/fory/reflect/FieldAccessor.java | 267 +- .../reflect/HiddenFieldAccessorFactory.java | 559 ++ .../SerializedLambdaSerializer.java | 216 + .../apache/fory/serializer/Serializers.java | 865 ++++ .../fory/serializer/SlicedStringUtil.java | 285 ++ .../fory/serializer/StringSerializer.java | 1166 +++++ .../fory-core/native-image.properties | 13 +- .../test/java/org/apache/fory/ForyTest.java | 55 +- .../pkgprivate/PackagePrivateMapKeyTest.java | 23 +- .../apache/fory/memory/MemoryBufferTest.java | 64 +- .../org/apache/fory/meta/TypeDefTest.java | 3 - .../fory/reflect/FieldAccessorTest.java | 35 + .../fory/resolver/DisallowedListTest.java | 6 +- .../fory/serializer/ArraySerializersTest.java | 8 + .../fory/serializer/DuplicateFieldsTest.java | 80 + ...inalFieldReplaceResolveSerializerTest.java | 2 - .../fory/serializer/ObjectSerializerTest.java | 463 ++ .../ObjectStreamSerializerTest.java | 2 +- .../ReplaceResolveSerializerTest.java | 2 + .../fory/serializer/URLSerializerTest.java | 2 +- .../collection/CollectionSerializersTest.java | 19 +- .../collection/MapSerializersTest.java | 46 +- java/fory-format/pom.xml | 15 +- .../fory/format/vectorized/ArrowUtils.java | 80 +- .../format/vectorized/ArrowTestSupport.java | 35 + .../format/vectorized/ArrowUtilsTest.java | 1 + .../format/vectorized/ArrowWriterTest.java | 2 + .../ImmutableCollectionSerializersTest.java | 8 +- .../apache/fory/test/bean/AccessBeans.java | 38 +- .../GuavaCollectionSerializersTest.java | 2 + .../org/apache/fory/test/FastJsonTest.java | 2 + .../fory/test/ReadResolveCircularTest.java | 9 +- java/pom.xml | 32 + 111 files changed, 15715 insertions(+), 592 deletions(-) create mode 100644 benchmarks/java/src/main/java/org/apache/fory/benchmark/Jdk25MrJarCheck.java create mode 100644 integration_tests/jdk_compatibility_tests/.gitignore create mode 100644 integration_tests/jpms_tests/src/main/java/org/apache/fory/integration_tests/model/PrivateFieldBean.java create mode 100644 integration_tests/jpms_tests/src/main/java/org/apache/fory/integration_tests/publicserializer/PublicSerializerValue.java create mode 100644 integration_tests/jpms_tests/src/main/java/org/apache/fory/integration_tests/publicserializer/PublicSerializerValueSerializer.java create mode 100644 integration_tests/jpms_tests/src/test/java/org/apache/fory/integration_tests/JpmsFieldAccessorTest.java create mode 100644 java/fory-core/src/main/java25/org/apache/fory/builder/CodecBuilder.java create mode 100644 java/fory-core/src/main/java25/org/apache/fory/builder/ObjectCodecBuilder.java create mode 100644 java/fory-core/src/main/java25/org/apache/fory/memory/MemoryBuffer.java create mode 100644 java/fory-core/src/main/java25/org/apache/fory/platform/internal/DefineClass.java create mode 100644 java/fory-core/src/main/java25/org/apache/fory/reflect/HiddenFieldAccessorFactory.java create mode 100644 java/fory-core/src/main/java25/org/apache/fory/serializer/SerializedLambdaSerializer.java create mode 100644 java/fory-core/src/main/java25/org/apache/fory/serializer/Serializers.java create mode 100644 java/fory-core/src/main/java25/org/apache/fory/serializer/SlicedStringUtil.java create mode 100644 java/fory-core/src/main/java25/org/apache/fory/serializer/StringSerializer.java create mode 100644 java/fory-format/src/test/java/org/apache/fory/format/vectorized/ArrowTestSupport.java 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..07b793301b 100644 --- a/benchmarks/java/pom.xml +++ b/benchmarks/java/pom.xml @@ -236,6 +236,141 @@ + + jdk25-benchmark-mrjar-check + + [25,) + + + + + org.apache.maven.plugins + maven-antrun-plugin + + + verify-benchmark-mrjar + package + + run + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -266,6 +401,7 @@ + true org.apache.fory.benchmark @@ -287,6 +423,10 @@ org.openjdk.jmh.Main + + true + org.apache.fory.benchmark + @@ -298,6 +438,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/Jdk25MrJarCheck.java b/benchmarks/java/src/main/java/org/apache/fory/benchmark/Jdk25MrJarCheck.java new file mode 100644 index 0000000000..857b668e04 --- /dev/null +++ b/benchmarks/java/src/main/java/org/apache/fory/benchmark/Jdk25MrJarCheck.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.benchmark; + +import org.apache.fory.memory.MemoryBuffer; +import org.apache.fory.platform.UnsafeOps; +import org.apache.fory.platform.internal._JDKAccess; +import org.apache.fory.reflect.FieldAccessor; +import org.apache.fory.serializer.StringSerializer; + +/** 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); + verifyClass(UnsafeOps.class); + verifyClass(_JDKAccess.class); + verifyClass(FieldAccessor.class); + verifyClass(StringSerializer.class); + if (_JDKAccess.UNSAFE != null) { + throw new IllegalStateException("JDK25 benchmark jar loaded Unsafe-backed _JDKAccess"); + } + } + + 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/NewJava11StringSuite.java b/benchmarks/java/src/main/java/org/apache/fory/benchmark/NewJava11StringSuite.java index 38a380a6dd..c4fcde43e1 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 @@ -23,7 +23,6 @@ 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; @@ -37,16 +36,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[]) UnsafeOps.getObject(str, fieldOffset(String.class, "value")); + coder = UnsafeOps.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 +54,14 @@ public class NewJava11StringSuite { stringSerializer.writeString(buffer, str); } + private static long fieldOffset(Class type, String fieldName) { + try { + return UnsafeOps.objectFieldOffset(type.getDeclaredField(fieldName)); + } catch (NoSuchFieldException e) { + throw new IllegalStateException(e); + } + } + // @Benchmark public Object createJDK11StringByCopyStr() { return new String(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..dfbad5136a 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 @@ -20,7 +20,6 @@ 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; @@ -41,10 +40,17 @@ 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 UnsafeOps.objectFieldOffset(type.getDeclaredField(fieldName)); + } catch (NoSuchFieldException e) { + throw new IllegalStateException(e); + } + } + // @Benchmark public Object createJDK8StringByUnsafe() { String str = new String(stubStr); 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..dfe1611726 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,97 @@ 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.nio=${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 +199,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..cf8bb10041 100644 --- a/ci/tasks/java.py +++ b/ci/tasks/java.py @@ -76,6 +76,93 @@ 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.nio={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 +254,23 @@ def run_jdk17_plus(java_version="17"): """Run Java 17+ tests.""" logging.info(f"Executing fory java tests with Java {java_version}") common.exec_cmd("java -version") - os.environ["JDK_JAVA_OPTIONS"] = ( + jdk_options = [ "--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED" - ) + ] + if java_version == "25": + jdk_options.extend(jdk25_deny_options()) + jdk_options.extend(jdk25_javac_options()) + os.environ["JDK_JAVA_OPTIONS"] = " ".join(jdk_options) common.cd_project_subdir("java") - common.exec_cmd("mvn -T10 --batch-mode --no-transfer-progress install") + 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 +301,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 +309,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 +322,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 +332,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 +346,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 +373,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..f4837e211c 100644 --- a/docs/guide/java/troubleshooting.md +++ b/docs/guide/java/troubleshooting.md @@ -148,6 +148,50 @@ 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, 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 and direct-buffer helpers also need JDK-private packages. 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` | +| `ByteArrayInputStream`, `ByteArrayOutputStream`, and Java object-stream metadata | `java.base/java.io` | +| Proxy serializers | `java.base/java.lang.reflect` | +| Direct `ByteBuffer` wrapping | `java.base/java.nio` | + +For example, direct `ByteBuffer` wrapping on the module path requires: + +```bash +--add-opens=java.base/java.nio=ALL-UNNAMED,org.apache.fory.core,org.apache.fory.format +``` + +Normal classes with final instance fields need a constructor that covers those final fields when +Unsafe allocation is denied. Annotate the constructor with +`java.beans.ConstructorProperties`, or compile the class with `-parameters` so Fory can bind +constructor parameters to fields. Non-final fields can still be restored after construction. + +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/integration_tests/graalvm_tests/pom.xml b/integration_tests/graalvm_tests/pom.xml index 38d3e5b50f..a0de283a90 100644 --- a/integration_tests/graalvm_tests/pom.xml +++ b/integration_tests/graalvm_tests/pom.xml @@ -176,6 +176,17 @@ -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.nio=ALL-UNNAMED + -J--add-opens=java.base/java.math=ALL-UNNAMED @@ -201,5 +212,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..1d0899d92f 100644 --- a/integration_tests/jdk_compatibility_tests/pom.xml +++ b/integration_tests/jdk_compatibility_tests/pom.xml @@ -81,4 +81,37 @@ + + + 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.nio=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..e98b2d633c 100644 --- a/integration_tests/jpms_tests/pom.xml +++ b/integration_tests/jpms_tests/pom.xml @@ -76,4 +76,37 @@ + + + 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.nio=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..f5a9a699ea 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,263 @@ + + 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 ed770247a3..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 @@ -563,7 +563,7 @@ public T copy(T obj) { private void serializeToStream(OutputStream outputStream, Consumer function) { MemoryBuffer buf = getBuffer(); - if (MemoryUtils.JDK_INTERNAL_FIELD_ACCESS + 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; 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..5c598b72ce 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 @@ -310,7 +310,7 @@ private Expression reflectAccessField( private Expression unsafeAccessField( Expression inputObject, Class cls, Descriptor descriptor) { String fieldName = descriptor.getName(); - Expression fieldOffsetExpr = getFieldOffset(cls, descriptor); + Expression fieldOffsetExpr = fieldOffsetExpr(cls, descriptor); boolean fieldNullable = fieldNullable(descriptor); if (descriptor.getTypeRef().isPrimitive()) { // ex: UnsafeOps.getFloat(obj, fieldOffset) @@ -333,7 +333,7 @@ private Expression unsafeAccessField( } } - 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`. @@ -351,9 +351,7 @@ private Expression getFieldOffset(Class cls, Descriptor descriptor) { .inline(); }); } else { - long fieldOffset = ReflectionUtils.getFieldOffset(field); - Preconditions.checkArgument(fieldOffset != -1); - return Literal.ofLong(fieldOffset); + return Literal.ofLong(UnsafeOps.objectFieldOffset(field)); } } @@ -426,7 +424,7 @@ private Expression reflectSetField(Expression bean, Field field, Expression valu private Expression unsafeSetField(Expression bean, Descriptor descriptor, Expression value) { TypeRef fieldType = descriptor.getTypeRef(); // 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()); 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..2220a53013 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 @@ -37,6 +37,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 +47,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; @@ -59,8 +61,10 @@ import org.apache.fory.logging.LoggerFactory; import org.apache.fory.meta.TypeDef; import org.apache.fory.platform.UnsafeOps; +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 +97,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 +141,8 @@ public ObjectCodecBuilder(Class beanClass, Fory fory) { buildRecordComponentDefaultValues(); } recordReversedMapping = RecordUtils.buildFieldToComponentMapping(beanClass); + } else { + initConstructorFields(grouper.getSortedDescriptors(), true); } } @@ -147,6 +157,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 ""; @@ -548,12 +674,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(); @@ -582,6 +716,132 @@ public Expression buildDecodeExpression() { 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, @@ -607,6 +867,88 @@ protected Expression createRecord(SortedMap recordComponent return new NewInstance(beanType, params); } + protected Expression createConstructorObject(FieldsArray fieldValues) { + Expression[] params = 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); + } + } + Expression args = new Expression.NewArray(OBJECT_ARRAY_TYPE, params); + ObjectCreators.getObjectCreator(beanClass); // trigger cache and make error raised early + 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 void addNonConstructorFieldSetters( + ListExpression expressions, Expression bean, FieldsArray fieldValues) { + for (Descriptor descriptor : objectCodecOptimizer.descriptorGrouper.getSortedDescriptors()) { + int index = fieldIndexes.get(descriptor); + if (constructorFieldMask[index]) { + continue; + } + 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)); + } + } + + 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)); + } + } + + 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 { private final TreeMap recordValuesMap = new TreeMap<>(); @@ -625,8 +967,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) { diff --git a/java/fory-core/src/main/java/org/apache/fory/collection/MapEntry.java b/java/fory-core/src/main/java/org/apache/fory/collection/MapEntry.java index b0bc8c3337..5ec07a7974 100644 --- a/java/fory-core/src/main/java/org/apache/fory/collection/MapEntry.java +++ b/java/fory-core/src/main/java/org/apache/fory/collection/MapEntry.java @@ -19,6 +19,7 @@ package org.apache.fory.collection; +import java.beans.ConstructorProperties; import java.util.Map; import java.util.Objects; @@ -27,6 +28,7 @@ public class MapEntry implements Map.Entry { private final K key; private V value; + @ConstructorProperties({"key", "value"}) public MapEntry(K key, V value) { this.key = key; this.value = value; 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..6613349580 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,12 +106,52 @@ 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) { return readObjects.get(id); } + 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; + } + /** Returns the object resolved by the last ref header that pointed to an existing instance. */ @Override public Object getReadRef() { @@ -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/MemoryUtils.java b/java/fory-core/src/main/java/org/apache/fory/memory/MemoryUtils.java index cf626123b1..81d35bb4d1 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 @@ -28,10 +28,22 @@ /** 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 java.nio private fields, the JVM must be launched with: - // --add-opens=java.base/java.nio=org.apache.fory.core + // 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 && _JDKAccess.JDK_INTERNAL_FIELD_ACCESS; + public static final boolean JDK_LANG_FIELD_ACCESS = + !AndroidSupport.IS_ANDROID && _JDKAccess.JDK_LANG_FIELD_ACCESS; + public static final boolean JDK_BYTE_ARRAY_STREAM_FIELD_ACCESS = + !AndroidSupport.IS_ANDROID && _JDKAccess.JDK_BYTE_ARRAY_STREAM_FIELD_ACCESS; + public static final boolean JDK_OBJECT_STREAM_FIELD_ACCESS = + !AndroidSupport.IS_ANDROID && _JDKAccess.JDK_OBJECT_STREAM_FIELD_ACCESS; + public static final boolean JDK_COLLECTION_FIELD_ACCESS = + !AndroidSupport.IS_ANDROID && _JDKAccess.JDK_COLLECTION_FIELD_ACCESS; + public static final boolean JDK_CONCURRENT_FIELD_ACCESS = + !AndroidSupport.IS_ANDROID && _JDKAccess.JDK_CONCURRENT_FIELD_ACCESS; + public static final boolean JDK_PROXY_FIELD_ACCESS = + !AndroidSupport.IS_ANDROID && _JDKAccess.JDK_PROXY_FIELD_ACCESS; public static MemoryBuffer buffer(int size) { return wrap(new byte[size]); @@ -103,9 +115,11 @@ public static void wrap(ByteArrayInputStream stream, MemoryBuffer buffer) { } private static void checkByteArrayStreamWrap(String streamType) { - if (!JDK_INTERNAL_FIELD_ACCESS) { + if (!JDK_BYTE_ARRAY_STREAM_FIELD_ACCESS) { throw new UnsupportedOperationException( - streamType + " direct wrapping is not supported on this platform"); + streamType + + " direct wrapping requires JDK internal field access. On JDK25+, open " + + "java.base/java.io to org.apache.fory.core,org.apache.fory.format."); } } 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 86a82e3e37..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,7 +19,6 @@ package org.apache.fory.meta; - import java.io.ObjectStreamClass; import java.io.Serializable; import java.lang.reflect.Field; @@ -36,7 +35,6 @@ import org.apache.fory.logging.Logger; import org.apache.fory.logging.LoggerFactory; import org.apache.fory.memory.MemoryBuffer; -import org.apache.fory.reflect.ReflectionUtils; import org.apache.fory.resolver.ClassResolver; import org.apache.fory.resolver.SharedRegistry; import org.apache.fory.resolver.TypeResolver; @@ -64,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); 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 index 8e214946bc..2dc5456a63 100644 --- 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 @@ -197,6 +197,12 @@ public static void copyMemory( } } + public static Object[] copyObjectArray(Object[] arr) { + Object[] objects = new Object[arr.length]; + System.arraycopy(arr, 0, objects, 0, arr.length); + return objects; + } + /** Create an instance of type. This method don't call constructor. */ public static T newInstance(Class type) { try { diff --git a/java/fory-core/src/main/java/org/apache/fory/platform/internal/DefineClass.java b/java/fory-core/src/main/java/org/apache/fory/platform/internal/DefineClass.java index c47ffa9d44..c340e673a7 100644 --- a/java/fory-core/src/main/java/org/apache/fory/platform/internal/DefineClass.java +++ b/java/fory-core/src/main/java/org/apache/fory/platform/internal/DefineClass.java @@ -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 index 514a8a9d57..e4c9d47ad9 100644 --- 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 @@ -21,6 +21,7 @@ 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; @@ -30,9 +31,11 @@ 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; @@ -63,6 +66,13 @@ public class _JDKAccess { // 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; @@ -79,6 +89,13 @@ public class _JDKAccess { } UNSAFE = unsafe; 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; if (JdkVersion.MAJOR_VERSION >= 11) { try { Field theInternalUnsafeField = Unsafe.class.getDeclaredField("theInternalUnsafe"); @@ -99,9 +116,9 @@ public class _JDKAccess { 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; - private static final long STRING_VALUE_FIELD_OFFSET; - private static final long STRING_COUNT_FIELD_OFFSET; - private static final long STRING_OFFSET_FIELD_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 { try { @@ -150,6 +167,8 @@ private static class StringCoderField { } } + public static final long STRING_CODER_FIELD_OFFSET = StringCoderField.OFFSET; + public static Object getStringValue(String value) { return UNSAFE.getObject(value, STRING_VALUE_FIELD_OFFSET); } @@ -178,6 +197,141 @@ public static Lookup _trustedLookup(Class objectClass) { return lookupCache.get(objectClass, () -> _Lookup._trustedLookup(objectClass)); } + 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 { @@ -227,6 +381,58 @@ public static void wrap(ByteArrayInputStream stream, MemoryBuffer buffer) { 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 { @@ -456,4 +662,8 @@ public static Object addReads(Object thisModule, Object otherModule) { 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/reflect/FieldAccessor.java b/java/fory-core/src/main/java/org/apache/fory/reflect/FieldAccessor.java index b4919e5da2..2eb540cb79 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 @@ -51,24 +51,68 @@ /** Field accessor for primitive types and object types. */ @SuppressWarnings({"unchecked", "rawtypes"}) public abstract class FieldAccessor { + 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 UnsafeOps.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); @@ -77,6 +121,54 @@ 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: + UnsafeOps.putBoolean( + targetObject, fieldOffset, UnsafeOps.getBoolean(sourceObject, fieldOffset)); + return; + case BYTE_ACCESS: + UnsafeOps.putByte(targetObject, fieldOffset, UnsafeOps.getByte(sourceObject, fieldOffset)); + return; + case CHAR_ACCESS: + UnsafeOps.putChar(targetObject, fieldOffset, UnsafeOps.getChar(sourceObject, fieldOffset)); + return; + case SHORT_ACCESS: + UnsafeOps.putShort( + targetObject, fieldOffset, UnsafeOps.getShort(sourceObject, fieldOffset)); + return; + case INT_ACCESS: + UnsafeOps.putInt(targetObject, fieldOffset, UnsafeOps.getInt(sourceObject, fieldOffset)); + return; + case LONG_ACCESS: + UnsafeOps.putLong(targetObject, fieldOffset, UnsafeOps.getLong(sourceObject, fieldOffset)); + return; + case FLOAT_ACCESS: + UnsafeOps.putFloat( + targetObject, fieldOffset, UnsafeOps.getFloat(sourceObject, fieldOffset)); + return; + case DOUBLE_ACCESS: + UnsafeOps.putDouble( + targetObject, fieldOffset, UnsafeOps.getDouble(sourceObject, fieldOffset)); + return; + case OBJECT_ACCESS: + UnsafeOps.putObject( + targetObject, fieldOffset, UnsafeOps.getObject(sourceObject, fieldOffset)); + return; + default: + putObject(targetObject, getObject(sourceObject)); + } + } + + public final void copyObject(Object sourceObject, Object targetObject) { + if (accessKind == OBJECT_ACCESS) { + UnsafeOps.putObject( + targetObject, fieldOffset, UnsafeOps.getObject(sourceObject, fieldOffset)); + } else { + putObject(targetObject, getObject(sourceObject)); + } + } + public Field getField() { return field; } @@ -145,7 +237,7 @@ public void putDouble(Object targetObject, double value) { set(targetObject, value); } - public void putObject(Object targetObject, Object object) { + public final void putObject(Object targetObject, Object object) { // For primitive fields, we must use set() which calls the correct UnsafeOps.putXxx method. // UnsafeOps.putObject writes object references, not primitive values. if (fieldOffset != -1 && !field.getType().isPrimitive()) { @@ -155,7 +247,7 @@ public void putObject(Object targetObject, Object object) { } } - public Object getObject(Object targetObject) { + public final Object getObject(Object targetObject) { // For primitive fields, we must use get() which calls the correct UnsafeOps.getXxx method // and returns the boxed value. UnsafeOps.getObject interprets primitive bytes as object // refs. 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..75f8c64558 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 @@ -37,6 +37,10 @@ */ @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 065a82b237..a9aa8d2da9 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,10 +19,22 @@ 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.Arrays; +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; @@ -31,6 +43,8 @@ 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; /** @@ -90,7 +104,14 @@ 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 (JdkVersion.MAJOR_VERSION >= 25 && hasFinalFields(type)) { + return new ConstructorObjectCreator<>(type); } if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { if (noArgConstructor != null) { @@ -105,6 +126,328 @@ private static ObjectCreator creategetObjectCreator(Class type) { return new DeclaredNoArgCtrObjectCreator<>(type); } + private static boolean hasFinalFields(Class type) { + for (Field field : Descriptor.getFields(type)) { + if (Modifier.isFinal(field.getModifiers())) { + return true; + } + } + return false; + } + + 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<>(); + Set finalFields = 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()); + } + } + if (Modifier.isFinal(field.getModifiers())) { + finalFields.add(field); + } + } + ConstructorMatch best = null; + for (Constructor constructor : type.getDeclaredConstructors()) { + if (constructor.isSynthetic()) { + continue; + } + ConstructorMatch match = + matchConstructor( + type, + (Constructor) constructor, + fieldsByName, + fieldsByNameList, + fieldsById, + duplicateNames, + duplicateIds, + finalFields); + if (match != null && (best == null || match.score > best.score)) { + best = match; + } + } + if (best == null) { + String requirement = + finalFields.isEmpty() + ? "a bindable constructor because no no-arg constructor is available" + : "a constructor covering final fields " + finalFields; + throw new ForyException( + "JDK25 zero-Unsafe mode requires " + + requirement + + " 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, + Set finalFields) { + Field[] fields = + constructorFields( + constructor, fieldsByName, fieldsByNameList, fieldsById, duplicateNames, duplicateIds); + if (fields == null) { + return null; + } + return matchConstructorFields(constructor, finalFields, fields); + } + + private static ConstructorMatch matchConstructorFields( + Constructor constructor, Set finalFields, Field[] fields) { + if (!containsAllFinalFields(finalFields, fields)) { + return null; + } + 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 boolean containsAllFinalFields(Set finalFields, Field[] fields) { + Set selectedFields = new LinkedHashSet<>(Arrays.asList(fields)); + for (Field finalField : finalFields) { + if (selectedFields.contains(finalField)) { + continue; + } + if (coveredBySyntheticField(finalField, selectedFields)) { + continue; + } + return false; + } + return true; + } + + private static boolean coveredBySyntheticField(Field finalField, Set selectedFields) { + if (!finalField.isSynthetic()) { + return false; + } + for (Field selectedField : selectedFields) { + if (selectedField.isSynthetic() + && selectedField.getName().equals(finalField.getName()) + && selectedField.getType() == finalField.getType()) { + return true; + } + } + return false; + } + + 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,20 +477,109 @@ 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); + } } } 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 de19e8e262..05f0e23712 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,8 +51,6 @@ 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; @@ -463,35 +461,6 @@ public static List getFieldValues(Collection fields, Object o) { 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); } 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..24f3d7f472 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; @@ -997,6 +1004,7 @@ private boolean usesNonStructTypeDef(Class cls) { || isMap(cls) || Externalizable.class.isAssignableFrom(cls) || requireJavaSerialization(cls) + || requiresJavaSerializer(cls) || useReplaceResolveSerializer(cls) || Functions.isLambda(cls) || (ScalaTypes.SCALA_AVAILABLE && ReflectionUtils.isScalaSingletonObject(cls)) @@ -1467,6 +1475,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 +1543,12 @@ public Class getSerializerClass(Class cls, boolean code return MapSerializer.class; } } + if (requiresJdkStream(cls)) { + return getDefaultJDKStreamSerializerType(); + } + if (requiresJavaSerializer(cls)) { + return JavaSerializer.class; + } if (isCrossLanguage()) { LOG.warn("Class {} isn't supported for cross-language serialization.", cls); } @@ -1564,6 +1587,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 +1605,9 @@ public Class getObjectSerializerClass( return serializerClass; } } + if (ReflectionUtils.isJdkProxy(cls)) { + return JdkProxySerializer.class; + } Class staticSerializerClass = getStaticGeneratedStructSerializerClass(cls); if (staticSerializerClass != null && shouldPreferStaticGeneratedSerializer(cls)) { @@ -1610,6 +1639,34 @@ 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 requiresJavaSerializer(Class cls) { + if (JdkVersion.MAJOR_VERSION < 25 || !Serializable.class.isAssignableFrom(cls)) { + return false; + } + // Scala products can have final derived fields initialized by the primary constructor but not + // represented as constructor parameters. Keep that compatibility in the isolated JDK stream + // path instead of teaching the generic JDK25 field serializer to ignore final fields. + return ScalaTypes.SCALA_AVAILABLE + && ScalaTypes.isScalaProductType(cls) + && !ReflectionUtils.isScalaSingletonObject(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 +1917,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 58bc955f9d..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,6 +32,7 @@ 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; @@ -62,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; @@ -863,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); @@ -870,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); @@ -882,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(); @@ -898,7 +929,30 @@ private Object[] copyFields(CopyContext copyContext, T originObj) { copyNotPrimitiveField(copyContext, originObj, fieldAccessor, fieldInfo.dispatchId); } } - return RecordUtils.remapping(copyRecordInfo, fieldValues); + 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 { + fieldValues[i] = + copyNotPrimitiveField(copyContext, originObj, fieldAccessor, fieldInfo.dispatchId); + } + } + return fieldValues; } private void copyFields(CopyContext copyContext, T originObj, T newObj) { @@ -925,10 +979,36 @@ public static void copyFields( } } + 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, fieldAccessor, fieldInfo.dispatchId); + } else { + copySetNotPrimitiveField( + copyContext, originObj, newObj, fieldAccessor, fieldInfo.dispatchId); + } + } + } + private static Object copyFieldValue(CopyContext copyContext, Object fieldValue, int dispatchId) { 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: @@ -957,58 +1037,15 @@ 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, FieldAccessor fieldAccessor, int typeId) { - switch (typeId) { - case DispatchId.BOOL: - fieldAccessor.putBoolean(newObj, fieldAccessor.getBoolean(originObj)); - break; - case DispatchId.INT8: - fieldAccessor.putByte(newObj, fieldAccessor.getByte(originObj)); - break; - case DispatchId.UINT8: - fieldAccessor.putInt(newObj, fieldAccessor.getInt(originObj)); - break; - case DispatchId.CHAR: - fieldAccessor.putChar(newObj, fieldAccessor.getChar(originObj)); - break; - case DispatchId.INT16: - fieldAccessor.putShort(newObj, fieldAccessor.getShort(originObj)); - break; - case DispatchId.UINT16: - fieldAccessor.putInt(newObj, fieldAccessor.getInt(originObj)); - break; - case DispatchId.INT32: - case DispatchId.VARINT32: - fieldAccessor.putInt(newObj, fieldAccessor.getInt(originObj)); - break; - case DispatchId.UINT32: - case DispatchId.VAR_UINT32: - fieldAccessor.putLong(newObj, fieldAccessor.getLong(originObj)); - break; - case DispatchId.INT64: - case DispatchId.VARINT64: - case DispatchId.TAGGED_INT64: - case DispatchId.UINT64: - case DispatchId.VAR_UINT64: - case DispatchId.TAGGED_UINT64: - fieldAccessor.putLong(newObj, fieldAccessor.getLong(originObj)); - break; - case DispatchId.FLOAT32: - fieldAccessor.putFloat(newObj, fieldAccessor.getFloat(originObj)); - break; - case DispatchId.FLOAT64: - fieldAccessor.putDouble(newObj, fieldAccessor.getDouble(originObj)); - break; - default: - throw new RuntimeException("Unknown primitive type: " + typeId); - } + fieldAccessor.copy(originObj, newObj); } private static void copySetNotPrimitiveField( @@ -1017,8 +1054,12 @@ private static void copySetNotPrimitiveField( Object newObj, FieldAccessor fieldAccessor, int typeId) { + if (isCopyByReference(typeId)) { + fieldAccessor.copyObject(originObj, newObj); + return; + } Object fieldValue = fieldAccessor.getObject(originObj); - fieldAccessor.putObject(newObj, copyFieldValue(copyContext, fieldValue, typeId)); + fieldAccessor.putObject(newObj, fieldValue == null ? null : copyContext.copyObject(fieldValue)); } private Object copyPrimitiveField(Object targetObject, FieldAccessor fieldAccessor, int typeId) { @@ -1108,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..d5e548fb45 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,6 +33,7 @@ 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.UnsafeOps; import org.apache.fory.reflect.FieldAccessor; import org.apache.fory.resolver.ClassResolver; @@ -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,7 +263,9 @@ 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); // Set default values for missing fields in Scala case classes @@ -240,6 +273,34 @@ private T newInstance() { 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 4c32c84e94..6cdeab3b7a 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 @@ -53,9 +53,26 @@ @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; @@ -70,17 +87,18 @@ public ExceptionSerializer(TypeResolver typeResolver, Class type) { this.typeResolver = typeResolver; messageConstructor = getOptionalMessageConstructor(type); objectCreator = - messageConstructor == null && MemoryUtils.JDK_INTERNAL_FIELD_ACCESS + messageConstructor == null && MemoryUtils.JDK_LANG_FIELD_ACCESS ? createThrowableObjectCreator(type) : null; slotsSerializers = buildSlotsSerializers(typeResolver, type); - if (!MemoryUtils.JDK_INTERNAL_FIELD_ACCESS + 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 @@ -111,7 +129,7 @@ public void write(WriteContext writeContext, T value) { public T read(ReadContext readContext) { Serializer[] slotsSerializers = getSlotsSerializers(); StackTraceElement[] stackTrace = (StackTraceElement[]) readContext.readRef(); - if (!MemoryUtils.JDK_INTERNAL_FIELD_ACCESS) { + if (!MemoryUtils.JDK_LANG_FIELD_ACCESS) { return readAndroidThrowableWithoutDetailMessageField( readContext, stackTrace, slotsSerializers); } @@ -137,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) { @@ -152,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); @@ -224,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 @@ -350,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); } @@ -419,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(); @@ -429,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); diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/FinalFieldReplaceResolveSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/FinalFieldReplaceResolveSerializer.java index 92ca339219..40bdb8b535 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/FinalFieldReplaceResolveSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/FinalFieldReplaceResolveSerializer.java @@ -41,13 +41,13 @@ public FinalFieldReplaceResolveSerializer(TypeResolver typeResolver, Class type) @Override protected void writeObject( WriteContext writeContext, Object value, MethodInfoCache jdkMethodInfoCache) { - jdkMethodInfoCache.objectSerializer.write(writeContext, value); + jdkMethodInfoCache.objectSerializer().write(writeContext, value); } @Override protected Object readObject(ReadContext readContext) { MethodInfoCache jdkMethodInfoCache = getMethodInfoCache(type); - Object o = jdkMethodInfoCache.objectSerializer.read(readContext); + Object o = jdkMethodInfoCache.objectSerializer().read(readContext); ReplaceResolveInfo replaceResolveInfo = jdkMethodInfoCache.info; if (replaceResolveInfo.readResolveMethod == null) { return o; diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/JavaSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/JavaSerializer.java index eb0497dc28..5ff4ab37c6 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/JavaSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/JavaSerializer.java @@ -19,6 +19,8 @@ package org.apache.fory.serializer; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.Externalizable; import java.io.IOException; import java.io.ObjectInputStream; @@ -30,6 +32,7 @@ import java.nio.ByteBuffer; import org.apache.fory.Fory; import org.apache.fory.collection.ClassValueCache; +import org.apache.fory.context.CopyContext; import org.apache.fory.context.ReadContext; import org.apache.fory.context.WriteContext; import org.apache.fory.io.ClassLoaderObjectInputStream; @@ -51,14 +54,16 @@ *

When a serializer not found and {@link ClassResolver#requireJavaSerialization(Class)} return * true, this serializer will be used. */ -@SuppressWarnings({"rawtypes", "unchecked"}) -public class JavaSerializer extends AbstractObjectSerializer { +@SuppressWarnings({"unchecked"}) +public class JavaSerializer extends Serializer { private static final Logger LOG = LoggerFactory.getLogger(JavaSerializer.class); + private final TypeResolver typeResolver; private final MemoryBufferObjectInput objectInput; private final MemoryBufferObjectOutput objectOutput; public JavaSerializer(TypeResolver typeResolver, Class cls) { - super(typeResolver, cls); + super(typeResolver.getConfig(), (Class) cls); + this.typeResolver = typeResolver; // TODO(chgaokunyang) enable this check when ObjectSerializer is implemented. // Preconditions.checkArgument(ClassResolver.requireJavaSerialization(cls)); if (cls != SerializedLambda.class) { @@ -69,8 +74,8 @@ public JavaSerializer(TypeResolver typeResolver, Class cls) { Serializer.class.getName(), Externalizable.class.getName()); } - objectInput = new MemoryBufferObjectInput(config, null); - objectOutput = new MemoryBufferObjectOutput(config, null); + objectInput = new MemoryBufferObjectInput(typeResolver.getConfig(), null); + objectOutput = new MemoryBufferObjectOutput(typeResolver.getConfig(), null); } @Override @@ -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 aaf1da9156..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 @@ -117,7 +117,7 @@ public Object copy(CopyContext copyContext, Object value) { Preconditions.checkNotNull(copyHandler); return Proxy.newProxyInstance(typeResolver.getClassLoader(), interfaces, copyHandler); } - if (!MemoryUtils.JDK_INTERNAL_FIELD_ACCESS) { + if (!MemoryUtils.JDK_PROXY_FIELD_ACCESS) { DeferredInvocationHandler deferredHandler = new DeferredInvocationHandler(); Object proxy = Proxy.newProxyInstance(typeResolver.getClassLoader(), interfaces, deferredHandler); @@ -143,7 +143,7 @@ public Object read(ReadContext readContext) { unwrapInvocationHandler((InvocationHandler) readContext.readRef()); return Proxy.newProxyInstance(typeResolver.getClassLoader(), interfaces, invocationHandler); } - if (!MemoryUtils.JDK_INTERNAL_FIELD_ACCESS) { + if (!MemoryUtils.JDK_PROXY_FIELD_ACCESS) { DeferredInvocationHandler deferredHandler = new DeferredInvocationHandler(); Object proxy = Proxy.newProxyInstance(typeResolver.getClassLoader(), interfaces, deferredHandler); 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 a5f1fe852a..09b34f4318 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,20 +58,22 @@ 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; @@ -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,13 +224,35 @@ 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 + 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 (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); + } else if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { return new ObjectCreators.UnsafeObjectCreator<>(type); } else { // In regular JVM, use the standard object creator @@ -227,8 +260,44 @@ private static ObjectCreator createObjectCreatorForGraalVM(Class 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 +350,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 +471,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 +690,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/ReplaceResolveSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/ReplaceResolveSerializer.java index 03d6492efa..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,9 +36,11 @@ 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.platform.JdkVersion; import org.apache.fory.platform.internal._JDKAccess; -import org.apache.fory.reflect.ReflectionUtils; +import org.apache.fory.reflect.ObjectCreators; import org.apache.fory.resolver.ClassResolver; import org.apache.fory.resolver.TypeInfo; import org.apache.fory.resolver.TypeResolver; @@ -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/Serializers.java b/java/fory-core/src/main/java/org/apache/fory/serializer/Serializers.java index 0f534f79bd..2cca216fba 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,10 +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; @@ -501,6 +504,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 +816,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/StringSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/StringSerializer.java index f2fcac8805..8e9fa43965 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,15 +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.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; @@ -43,7 +36,6 @@ 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.platform.internal._JDKAccess; import org.apache.fory.util.MathUtils; @@ -52,7 +44,7 @@ import org.apache.fory.util.StringUtils; /** - * 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 @@ -64,28 +56,35 @@ public final class StringSerializer extends ImmutableSerializer { private static final boolean 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; + 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; static { if (!jdkInternalFieldAccess()) { 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 { STRING_VALUE_FIELD_IS_CHARS = _JDKAccess.STRING_VALUE_FIELD_IS_CHARS; STRING_VALUE_FIELD_IS_BYTES = _JDKAccess.STRING_VALUE_FIELD_IS_BYTES; + STRING_VALUE_FIELD_OFFSET = _JDKAccess.STRING_VALUE_FIELD_OFFSET; STRING_HAS_COUNT_OFFSET = _JDKAccess.STRING_HAS_COUNT_OFFSET; + STRING_COUNT_FIELD_OFFSET = _JDKAccess.STRING_COUNT_FIELD_OFFSET; + STRING_OFFSET_FIELD_OFFSET = _JDKAccess.STRING_OFFSET_FIELD_OFFSET; } } private static boolean jdkInternalFieldAccess() { - return !AndroidSupport.IS_ANDROID && _JDKAccess.JDK_INTERNAL_FIELD_ACCESS; + return !AndroidSupport.IS_ANDROID && _JDKAccess.JDK_STRING_FIELD_ACCESS; } private final boolean compressString; @@ -427,19 +426,19 @@ private static boolean isLatin(char[] chars) { } private static Object getStringValue(String value) { - return _JDKAccess.getStringValue(value); + return UnsafeOps.getObject(value, STRING_VALUE_FIELD_OFFSET); } private static byte getStringCoder(String value) { - return _JDKAccess.getStringCoder(value); + return UnsafeOps.getByte(value, _JDKAccess.STRING_CODER_FIELD_OFFSET); } private static int getStringOffset(String value) { - return _JDKAccess.getStringOffset(value); + return UnsafeOps.getInt(value, STRING_OFFSET_FIELD_OFFSET); } private static int getStringCount(String value) { - return _JDKAccess.getStringCount(value); + return UnsafeOps.getInt(value, STRING_COUNT_FIELD_OFFSET); } @CodegenInvoke @@ -885,24 +884,11 @@ private void writeBytesUTF8PerfOptimized(MemoryBuffer buffer, byte[] bytes) { } } - private static final MethodHandles.Lookup STRING_LOOK_UP = - jdkInternalFieldAccess() ? _JDKAccess._trustedLookup(String.class) : null; - private static final BiFunction CHARS_STRING_ZERO_COPY_CTR = - jdkInternalFieldAccess() ? getCharsStringZeroCopyCtr() : null; - private static final BiFunction BYTES_STRING_ZERO_COPY_CTR = - jdkInternalFieldAccess() ? getBytesStringZeroCopyCtr() : null; - private static final Function LATIN_BYTES_STRING_ZERO_COPY_CTR = - jdkInternalFieldAccess() ? getLatinBytesStringZeroCopyCtr() : null; - public static String newCharsStringZeroCopy(char[] data) { if (!jdkInternalFieldAccess()) { 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); + return _JDKAccess.newCharsStringZeroCopy(data); } private static String newCharsStringSlow(char[] data) { @@ -915,27 +901,7 @@ public static String newBytesStringZeroCopy(byte coder, byte[] data) { if (!jdkInternalFieldAccess()) { 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); - } + return _JDKAccess.newBytesStringZeroCopy(coder, data); } private static String newBytesStringSlow(byte coder, byte[] data) { @@ -952,95 +918,6 @@ private static String newBytesStringSlow(byte coder, byte[] data) { } } - 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; - } - } - private static void writeCharsUTF16BEToHeap( char[] chars, int arrIndex, int numBytes, byte[] targetArray) { // Write to heap memory then copy is 250% faster than unsafe write to direct memory. 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 16a420fd97..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; @@ -59,8 +58,7 @@ import org.apache.fory.memory.MemoryBuffer; import org.apache.fory.memory.MemoryUtils; import org.apache.fory.platform.GraalvmSupport; -import org.apache.fory.platform.UnsafeOps; -import org.apache.fory.platform.internal._JDKAccess; +import org.apache.fory.platform.JdkVersion; import org.apache.fory.reflect.FieldAccessor; import org.apache.fory.reflect.ReflectionUtils; import org.apache.fory.resolver.ClassResolver; @@ -162,7 +160,7 @@ public void write(WriteContext writeContext, List value) { super.write(writeContext, value); } else { Object[] array = - !MemoryUtils.JDK_INTERNAL_FIELD_ACCESS || GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE + !MemoryUtils.JDK_COLLECTION_FIELD_ACCESS || GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE ? value.toArray() : (Object[]) ArrayAccess.ACCESSOR.getObject(value); writeContext.writeRef(array); @@ -554,17 +552,27 @@ public static final class SetFromMapSerializer extends CollectionSerializer type = Class.forName("java.util.Collections$SetFromMap"); Field mapField = type.getDeclaredField("m"); MAP_ACCESSOR = FieldAccessor.createAccessor(mapField); - MethodHandles.Lookup lookup = _JDKAccess._trustedLookup(type); - M_SETTER = lookup.findSetter(type, "m", Map.class); - S_SETTER = lookup.findSetter(type, "s", Set.class); + } 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 (!MemoryUtils.JDK_INTERNAL_FIELD_ACCESS || 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,7 +619,7 @@ public Collection newCollection(ReadContext readContext) { @Override public Collection newCollection(CopyContext copyContext, Collection originCollection) { assert !config.isXlang(); - if (!MemoryUtils.JDK_INTERNAL_FIELD_ACCESS || 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 = @@ -624,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 (!MemoryUtils.JDK_INTERNAL_FIELD_ACCESS || 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); @@ -638,6 +647,17 @@ public Collection onCollectionWrite(WriteContext writeContext, Set 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); @@ -878,7 +898,7 @@ public ArrayBlockingQueueSerializer(TypeResolver typeResolver, Class 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 a5b4dc539e..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,7 +32,7 @@ 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; @@ -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 b94fd6f366..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 @@ -359,7 +359,7 @@ public EnumMapSerializer(TypeResolver typeResolver) { @Override public Map onMapWrite(WriteContext writeContext, EnumMap value) { MemoryBuffer buffer = writeContext.getBuffer(); - if (!MemoryUtils.JDK_INTERNAL_FIELD_ACCESS && value.isEmpty()) { + if (!MemoryUtils.JDK_COLLECTION_FIELD_ACCESS && value.isEmpty()) { buffer.writeByte(JAVA_SERIALIZED_EMPTY_ENUM_MAP); getJavaSerializer().write(writeContext, value); return value; @@ -384,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; } 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 fab1967e4c..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 @@ -222,7 +222,7 @@ private ViewFields( } private static ViewFields create(Class type) { - if (!MemoryUtils.JDK_INTERNAL_FIELD_ACCESS || Stub.class.isAssignableFrom(type)) { + if (!MemoryUtils.JDK_COLLECTION_FIELD_ACCESS || Stub.class.isAssignableFrom(type)) { return null; } Class cls = type; 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 b7f03987f7..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 @@ -93,7 +93,7 @@ public SynchronizedCollectionSerializer( @Override public void write(WriteContext writeContext, Collection object) { Preconditions.checkArgument(object.getClass() == type); - if (!MemoryUtils.JDK_INTERNAL_FIELD_ACCESS) { + if (!MemoryUtils.JDK_COLLECTION_FIELD_ACCESS) { synchronized (object) { Collection source; if (object instanceof SortedSet) { @@ -123,7 +123,7 @@ public Collection read(ReadContext readContext) { @Override public Collection copy(CopyContext copyContext, Collection object) { - if (!MemoryUtils.JDK_INTERNAL_FIELD_ACCESS) { + if (!MemoryUtils.JDK_COLLECTION_FIELD_ACCESS) { synchronized (object) { Collection mutableSource; if (object instanceof SortedSet) { @@ -159,7 +159,7 @@ public SynchronizedMapSerializer( @Override public void write(WriteContext writeContext, Map object) { Preconditions.checkArgument(object.getClass() == type); - if (!MemoryUtils.JDK_INTERNAL_FIELD_ACCESS) { + if (!MemoryUtils.JDK_COLLECTION_FIELD_ACCESS) { synchronized (object) { Map source; if (object instanceof SortedMap) { @@ -181,7 +181,7 @@ public void write(WriteContext writeContext, Map object) { @Override public Map copy(CopyContext copyContext, Map originMap) { - if (!MemoryUtils.JDK_INTERNAL_FIELD_ACCESS) { + if (!MemoryUtils.JDK_COLLECTION_FIELD_ACCESS) { synchronized (originMap) { Map mutableSource; if (originMap instanceof SortedMap) { @@ -223,7 +223,7 @@ private static Serializer createSerializer( typeResolver, factory.f0, factory.f1, - MemoryUtils.JDK_INTERNAL_FIELD_ACCESS + MemoryUtils.JDK_COLLECTION_FIELD_ACCESS ? SourceAccessors.SOURCE_COLLECTION_ACCESSOR : null); } else { @@ -231,7 +231,7 @@ private static Serializer createSerializer( typeResolver, factory.f0, factory.f1, - MemoryUtils.JDK_INTERNAL_FIELD_ACCESS ? SourceAccessors.SOURCE_MAP_ACCESSOR : null); + 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 4b0cd0372c..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 @@ -93,7 +93,7 @@ public UnmodifiableCollectionSerializer( @Override public void write(WriteContext writeContext, Collection value) { Preconditions.checkArgument(value.getClass() == type); - if (!MemoryUtils.JDK_INTERNAL_FIELD_ACCESS) { + if (!MemoryUtils.JDK_COLLECTION_FIELD_ACCESS) { Collection source; if (value instanceof SortedSet) { source = new TreeSet(((SortedSet) value).comparator()); @@ -116,7 +116,7 @@ public Collection read(ReadContext readContext) { @Override public Collection copy(CopyContext copyContext, Collection object) { - if (!MemoryUtils.JDK_INTERNAL_FIELD_ACCESS) { + if (!MemoryUtils.JDK_COLLECTION_FIELD_ACCESS) { Collection mutableSource; if (object instanceof SortedSet) { Object comparator = copyContext.copyObject(((SortedSet) object).comparator()); @@ -149,7 +149,7 @@ public UnmodifiableMapSerializer( @Override public void write(WriteContext writeContext, Map value) { Preconditions.checkArgument(value.getClass() == type); - if (!MemoryUtils.JDK_INTERNAL_FIELD_ACCESS) { + if (!MemoryUtils.JDK_COLLECTION_FIELD_ACCESS) { Map source; if (value instanceof SortedMap) { source = new TreeMap(((SortedMap) value).comparator()); @@ -165,7 +165,7 @@ public void write(WriteContext writeContext, Map value) { @Override public Map copy(CopyContext copyContext, Map originMap) { - if (!MemoryUtils.JDK_INTERNAL_FIELD_ACCESS) { + if (!MemoryUtils.JDK_COLLECTION_FIELD_ACCESS) { Map mutableSource; if (originMap instanceof SortedMap) { Object comparator = copyContext.copyObject(((SortedMap) originMap).comparator()); @@ -203,7 +203,7 @@ private static Serializer createSerializer( typeResolver, factory.f0, factory.f1, - MemoryUtils.JDK_INTERNAL_FIELD_ACCESS + MemoryUtils.JDK_COLLECTION_FIELD_ACCESS ? SourceAccessors.SOURCE_COLLECTION_ACCESSOR : null); } else { @@ -211,7 +211,7 @@ private static Serializer createSerializer( typeResolver, factory.f0, factory.f1, - MemoryUtils.JDK_INTERNAL_FIELD_ACCESS ? SourceAccessors.SOURCE_MAP_ACCESSOR : null); + MemoryUtils.JDK_COLLECTION_FIELD_ACCESS ? SourceAccessors.SOURCE_MAP_ACCESSOR : null); } } diff --git a/java/fory-core/src/main/java/org/apache/fory/type/BFloat16.java b/java/fory-core/src/main/java/org/apache/fory/type/BFloat16.java index fb0d905ffb..b1e0f2b67e 100644 --- a/java/fory-core/src/main/java/org/apache/fory/type/BFloat16.java +++ b/java/fory-core/src/main/java/org/apache/fory/type/BFloat16.java @@ -19,6 +19,7 @@ package org.apache.fory.type; +import java.beans.ConstructorProperties; import java.io.Serializable; /** @@ -69,6 +70,7 @@ public final class BFloat16 extends Number implements Comparable, Seri private final short bits; + @ConstructorProperties("bits") private BFloat16(short bits) { this.bits = bits; } 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..538b7095bb 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 @@ -19,6 +19,7 @@ package org.apache.fory.type; +import java.beans.ConstructorProperties; import java.io.Serializable; import java.util.Arrays; import java.util.Iterator; @@ -45,6 +46,11 @@ public BFloat16Array(BFloat16[] values) { } } + @ConstructorProperties("bits") + 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/Float16.java b/java/fory-core/src/main/java/org/apache/fory/type/Float16.java index c76eaa362c..562df4d180 100644 --- a/java/fory-core/src/main/java/org/apache/fory/type/Float16.java +++ b/java/fory-core/src/main/java/org/apache/fory/type/Float16.java @@ -19,6 +19,7 @@ package org.apache.fory.type; +import java.beans.ConstructorProperties; import java.io.Serializable; public final class Float16 extends Number implements Comparable, Serializable { @@ -61,6 +62,7 @@ public final class Float16 extends Number implements Comparable, Serial private final short bits; + @ConstructorProperties("bits") private Float16(short bits) { this.bits = 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..f94e618f72 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 @@ -19,6 +19,7 @@ package org.apache.fory.type; +import java.beans.ConstructorProperties; import java.io.Serializable; import java.util.Arrays; import java.util.Iterator; @@ -45,6 +46,11 @@ public Float16Array(Float16[] values) { } } + @ConstructorProperties("bits") + private Float16Array(short[] bits) { + this.bits = bits; + } + private Float16Array(short[] bits, boolean copy) { this.bits = copy ? Arrays.copyOf(bits, bits.length) : bits; } diff --git a/java/fory-core/src/main/java/org/apache/fory/type/unsigned/UInt16.java b/java/fory-core/src/main/java/org/apache/fory/type/unsigned/UInt16.java index cd57b1dab1..320260816c 100644 --- a/java/fory-core/src/main/java/org/apache/fory/type/unsigned/UInt16.java +++ b/java/fory-core/src/main/java/org/apache/fory/type/unsigned/UInt16.java @@ -19,6 +19,7 @@ package org.apache.fory.type.unsigned; +import java.beans.ConstructorProperties; import java.io.Serializable; /** @@ -39,6 +40,7 @@ public final class UInt16 implements Comparable, Serializable { private final short data; + @ConstructorProperties("data") public UInt16(short data) { this.data = data; } diff --git a/java/fory-core/src/main/java/org/apache/fory/type/unsigned/UInt32.java b/java/fory-core/src/main/java/org/apache/fory/type/unsigned/UInt32.java index 2a1a4999fd..3f8052e69a 100644 --- a/java/fory-core/src/main/java/org/apache/fory/type/unsigned/UInt32.java +++ b/java/fory-core/src/main/java/org/apache/fory/type/unsigned/UInt32.java @@ -19,6 +19,7 @@ package org.apache.fory.type.unsigned; +import java.beans.ConstructorProperties; import java.io.Serializable; /** @@ -37,6 +38,7 @@ public final class UInt32 implements Comparable, Serializable { private final int data; + @ConstructorProperties("data") public UInt32(int data) { this.data = data; } diff --git a/java/fory-core/src/main/java/org/apache/fory/type/unsigned/UInt64.java b/java/fory-core/src/main/java/org/apache/fory/type/unsigned/UInt64.java index d5c7267c9f..3caff91f83 100644 --- a/java/fory-core/src/main/java/org/apache/fory/type/unsigned/UInt64.java +++ b/java/fory-core/src/main/java/org/apache/fory/type/unsigned/UInt64.java @@ -19,6 +19,7 @@ package org.apache.fory.type.unsigned; +import java.beans.ConstructorProperties; import java.io.Serializable; /** @@ -36,6 +37,7 @@ public final class UInt64 implements Comparable, Serializable { private final long data; + @ConstructorProperties("data") public UInt64(long data) { this.data = data; } diff --git a/java/fory-core/src/main/java/org/apache/fory/type/unsigned/UInt8.java b/java/fory-core/src/main/java/org/apache/fory/type/unsigned/UInt8.java index b4183108f1..70eb002787 100644 --- a/java/fory-core/src/main/java/org/apache/fory/type/unsigned/UInt8.java +++ b/java/fory-core/src/main/java/org/apache/fory/type/unsigned/UInt8.java @@ -19,6 +19,7 @@ package org.apache.fory.type.unsigned; +import java.beans.ConstructorProperties; import java.io.Serializable; /** @@ -39,6 +40,7 @@ public final class UInt8 implements Comparable, Serializable { private final byte data; + @ConstructorProperties("data") public UInt8(byte data) { this.data = data; } 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 59c3088862..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 @@ -85,6 +85,10 @@ public FieldAccessor getFieldAccessor() { return fieldAccessor; } + public Class getDeclaringClass() { + return fieldAccessor == null ? null : fieldAccessor.getField().getDeclaringClass(); + } + public int getDispatchId() { return dispatchId; } @@ -414,42 +418,82 @@ 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(); - 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); - } + 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) { return getScalaDefaultValueSupport().getDefaultValue(cls, fieldName); } diff --git a/java/fory-core/src/main/java25/org/apache/fory/builder/CodecBuilder.java b/java/fory-core/src/main/java25/org/apache/fory/builder/CodecBuilder.java new file mode 100644 index 0000000000..b74b523c0f --- /dev/null +++ b/java/fory-core/src/main/java25/org/apache/fory/builder/CodecBuilder.java @@ -0,0 +1,732 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fory.builder; + +import static org.apache.fory.codegen.Expression.Invoke.inlineInvoke; +import static org.apache.fory.type.TypeUtils.CLASS_TYPE; +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_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; +import static org.apache.fory.type.TypeUtils.PRIMITIVE_VOID_TYPE; +import static org.apache.fory.type.TypeUtils.getRawType; + +import java.lang.invoke.MethodHandle; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Collectors; +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; +import org.apache.fory.codegen.Expression.Literal; +import org.apache.fory.codegen.Expression.Reference; +import org.apache.fory.codegen.Expression.StaticInvoke; +import org.apache.fory.collection.Tuple2; +import org.apache.fory.memory.MemoryBuffer; +import org.apache.fory.memory.NativeByteOrder; +import org.apache.fory.platform.GraalvmSupport; +import org.apache.fory.platform.UnsafeOps; +import org.apache.fory.reflect.FieldAccessor; +import org.apache.fory.reflect.ObjectCreator; +import org.apache.fory.reflect.ObjectCreators; +import org.apache.fory.reflect.ReflectionUtils; +import org.apache.fory.reflect.TypeRef; +import org.apache.fory.resolver.TypeInfo; +import org.apache.fory.resolver.TypeInfoHolder; +import org.apache.fory.type.Descriptor; +import org.apache.fory.util.Preconditions; +import org.apache.fory.util.StringUtils; +import org.apache.fory.util.function.Functions; +import org.apache.fory.util.record.RecordComponent; +import org.apache.fory.util.record.RecordUtils; + +/** + * Base builder for generating code to serialize java bean in row-format or object stream format. + * + *
    + * This builder has following requirements for the class of java bean: + *
  • public + *
  • For instance inner class, ignore outer class field. + *
  • For instance inner class, deserialized outer class field is null + *
+ */ +@SuppressWarnings("UnstableApiUsage") +public abstract class CodecBuilder { + protected static final String ROOT_OBJECT_NAME = "_f_obj"; + static TypeRef objectArrayTypeRef = TypeRef.of(Object[].class); + static TypeRef bufferTypeRef = TypeRef.of(MemoryBuffer.class); + static TypeRef classInfoTypeRef = TypeRef.of(TypeInfo.class); + static TypeRef classInfoHolderTypeRef = TypeRef.of(TypeInfoHolder.class); + + protected final CodegenContext ctx; + protected final TypeRef beanType; + protected final Class beanClass; + protected final boolean isRecord; + protected final boolean isInterface; + private final Set duplicatedFields; + public static final Reference recordComponentDefaultValues = + new Reference("recordComponentDefaultValues", OBJECT_ARRAY_TYPE); + protected final Map fieldMap = new HashMap<>(); + protected boolean recordCtrAccessible; + + public CodecBuilder(CodegenContext ctx, TypeRef beanType) { + this.ctx = ctx; + this.beanType = beanType; + this.beanClass = getRawType(beanType); + isRecord = RecordUtils.isRecord(beanClass); + isInterface = beanClass.isInterface(); + if (isRecord) { + recordCtrAccessible = recordCtrAccessible(beanClass); + } + duplicatedFields = Descriptor.getSortedDuplicatedMembers(beanClass).keySet(); + // don't ctx.addImport beanClass, because it maybe causes name collide. + ctx.reserveName(ROOT_OBJECT_NAME); + // Don't import other packages to avoid class conflicts. + // For example user class named as `Date`/`List`/`MemoryBuffer` + // Skip Java reserved words since they can't be used as variable names anyway + // (e.g., Kotlin allows field names like "new" which are valid at bytecode level) + ReflectionUtils.getFields(beanType.getRawType(), true).stream() + .map(Field::getName) + .filter(name -> !CodegenContext.JAVA_RESERVED_WORDS.contains(name)) + .collect(Collectors.toSet()) + .forEach(ctx::reserveName); + } + + public abstract String codecClassName(Class cls); + + /** Generate codec class code. */ + public abstract String genCode(); + + /** Returns an expression that serialize java bean of type {@link CodecBuilder#beanClass}. */ + public abstract Expression buildEncodeExpression(); + + protected boolean sourcePublicAccessible(Class cls) { + return ctx.sourcePublicAccessible(cls); + } + + protected boolean fieldNullable(Descriptor descriptor) { + return false; + } + + protected Expression tryInlineCast(Expression expression, TypeRef targetType) { + return tryCastIfPublic(expression, targetType, true); + } + + protected Expression tryCastIfPublic(Expression expression, TypeRef targetType) { + return tryCastIfPublic(expression, targetType, false); + } + + protected Expression tryCastIfPublic( + Expression expression, TypeRef targetType, boolean inline) { + Class rawType = getRawType(targetType); + if (inline) { + if (sourcePublicAccessible(rawType)) { + return new Cast(expression, targetType); + } else { + return new Cast(expression, ReflectionUtils.getPublicSuperType(TypeRef.of(rawType))); + } + } + return tryCastIfPublic(expression, targetType, "castedValue"); + } + + protected Expression tryCastIfPublic( + Expression expression, TypeRef targetType, String valuePrefix) { + Class rawType = getRawType(targetType); + Class expressionRawType = getRawType(expression.type()); + // Source casts use erased Java types. Captured wildcard metadata can fail full generic subtype + // checks even when the emitted local variable already has an assignable raw type. + boolean rawTypeAlreadyAssignable = rawType.isAssignableFrom(expressionRawType); + if (sourcePublicAccessible(rawType) + && !expression.type().wrap().isSubtypeOf(targetType.wrap()) + && !rawTypeAlreadyAssignable) { + return new Cast(expression, targetType, valuePrefix); + } + if (rawType.isArray()) { + return new Cast(expression, OBJECT_ARRAY_TYPE, valuePrefix); + } + return expression; + } + + protected Reference getRecordCtrHandle() { + String fieldName = "_record_ctr_"; + Reference fieldRef = fieldMap.get(fieldName); + if (fieldRef == null) { + // trigger cache for graalvm + RecordUtils.getRecordCtrHandle(beanClass); + StaticInvoke getRecordCtrHandle = + new StaticInvoke( + RecordUtils.class, + "getRecordCtrHandle", + TypeRef.of(MethodHandle.class), + beanClassExpr()); + ctx.addField(ctx.type(MethodHandle.class), fieldName, getRecordCtrHandle); + fieldRef = new Reference(fieldName, TypeRef.of(MethodHandle.class)); + fieldMap.put(fieldName, fieldRef); + } + return fieldRef; + } + + protected Expression buildDefaultComponentsArray() { + return new StaticInvoke( + UnsafeOps.class, "copyObjectArray", OBJECT_ARRAY_TYPE, recordComponentDefaultValues); + } + + /** Returns an expression that get field value from bean. */ + protected Expression getFieldValue(Expression inputBeanExpr, Descriptor descriptor) { + TypeRef fieldType = descriptor.getTypeRef(); + Class rawType = descriptor.getRawType(); + String fieldName = descriptor.getName(); + boolean fieldNullable = fieldNullable(descriptor); + if (isInterface) { + return new Invoke(inputBeanExpr, descriptor.getName(), fieldName, fieldType, fieldNullable); + } + if (isRecord) { + return getRecordFieldValue(inputBeanExpr, descriptor); + } + if (duplicatedFields.contains(fieldName) || !Modifier.isPublic(beanClass.getModifiers())) { + return unsafeAccessField(inputBeanExpr, beanClass, descriptor); + } + if (!sourcePublicAccessible(rawType)) { + fieldType = OBJECT_TYPE; + } + // public field or non-private non-java field access field directly. + if (Modifier.isPublic(descriptor.getModifiers())) { + return new Expression.FieldValue(inputBeanExpr, fieldName, fieldType, fieldNullable, false); + } else if (descriptor.getReadMethod() != null + && Modifier.isPublic(descriptor.getReadMethod().getModifiers())) { + return new Invoke( + inputBeanExpr, descriptor.getReadMethod().getName(), fieldName, fieldType, fieldNullable); + } else { + if (!Modifier.isPrivate(descriptor.getModifiers())) { + if (AccessorHelper.defineAccessor(descriptor.getField())) { + return new StaticInvoke( + AccessorHelper.getAccessorClass(descriptor.getField()), + fieldName, + fieldType, + fieldNullable, + inputBeanExpr); + } + } + if (descriptor.getReadMethod() != null + && !Modifier.isPrivate(descriptor.getReadMethod().getModifiers())) { + if (AccessorHelper.defineAccessor(descriptor.getReadMethod())) { + return new StaticInvoke( + AccessorHelper.getAccessorClass(descriptor.getReadMethod()), + descriptor.getReadMethod().getName(), + fieldType, + fieldNullable, + inputBeanExpr); + } + } + return unsafeAccessField(inputBeanExpr, beanClass, descriptor); + } + } + + private Expression getRecordFieldValue(Expression inputBeanExpr, Descriptor descriptor) { + TypeRef fieldType = descriptor.getTypeRef(); + if (!sourcePublicAccessible(descriptor.getRawType())) { + fieldType = OBJECT_TYPE; + } + String fieldName = descriptor.getName(); + boolean fieldNullable = fieldNullable(descriptor); + if (Modifier.isPublic(beanClass.getModifiers())) { + Preconditions.checkNotNull(descriptor.getReadMethod()); + return new Invoke( + inputBeanExpr, descriptor.getReadMethod().getName(), fieldName, fieldType, fieldNullable); + } else { + String key = "_" + fieldName + "_getter_"; + Reference ref = fieldMap.get(key); + Tuple2, String> methodInfo = Functions.getterMethodInfo(descriptor.getRawType()); + if (ref == null) { + Class funcInterface = methodInfo.f0; + TypeRef getterType = TypeRef.of(funcInterface); + if (GraalvmSupport.isGraalBuildTime()) { + // generate getter ahead at native image build time. + Functions.makeGetterFunction(beanClass, fieldName); + } + Expression getter = + new StaticInvoke( + Functions.class, + "makeGetterFunction", + OBJECT_TYPE, + beanClassExpr(), + Literal.ofString(fieldName)); + getter = new Cast(getter, getterType); + ctx.addField(funcInterface, key, getter); + ref = new Reference(key, getterType); + fieldMap.put(key, ref); + } + if (!fieldType.isPrimitive()) { + Expression v = inlineInvoke(ref, methodInfo.f1, OBJECT_TYPE, fieldNullable, inputBeanExpr); + return tryCastIfPublic(v, descriptor.getTypeRef(), fieldName); + } else { + return new Invoke(ref, methodInfo.f1, fieldType, fieldNullable, inputBeanExpr); + } + } + } + + /** Returns an expression that get field value> from bean using reflection. */ + private Expression reflectAccessField( + Expression inputObject, Class cls, Descriptor descriptor) { + Reference fieldRef = getReflectField(cls, descriptor.getField()); + // boolean fieldNullable = !descriptor.getTypeToken().isPrimitive(); + Invoke getObj = + new Invoke(fieldRef, "get", OBJECT_TYPE, fieldNullable(descriptor), inputObject); + return new Cast(getObj, descriptor.getTypeRef(), descriptor.getName()); + } + + /** Returns an expression that get field value> from bean using `Unsafe`. */ + private Expression unsafeAccessField( + Expression inputObject, Class cls, Descriptor descriptor) { + String fieldName = descriptor.getName(); + Reference fieldAccessor = getFieldAccessor(cls, 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); + } + } + + private Reference getFieldAccessor(Class cls, 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}. + */ + public abstract Expression buildDecodeExpression(); + + /** Returns an expression that set field value to bean. */ + protected Expression setFieldValue(Expression bean, Descriptor d, Expression value) { + String fieldName = d.getName(); + if (value instanceof Inlineable) { + ((Inlineable) value).inline(); + } + if (duplicatedFields.contains(fieldName) || !sourcePublicAccessible(beanClass)) { + return unsafeSetField(bean, d, value); + } + if (!d.isFinalField() + && Modifier.isPublic(d.getModifiers()) + && Modifier.isPublic(d.getRawType().getModifiers())) { + if (!d.getRawType().isAssignableFrom(value.type().getRawType())) { + value = tryInlineCast(value, d.getTypeRef()); + } + return new Expression.SetField(bean, fieldName, value); + } else if (d.getWriteMethod() != null && Modifier.isPublic(d.getWriteMethod().getModifiers())) { + if (!d.getRawType().isAssignableFrom(value.type().getRawType())) { + value = tryInlineCast(value, d.getTypeRef()); + } + return new Invoke(bean, d.getWriteMethod().getName(), value); + } else { + if (!d.isFinalField() && !Modifier.isPrivate(d.getModifiers())) { + if (AccessorHelper.defineSetter(d.getField())) { + Class accessorClass = AccessorHelper.getAccessorClass(d.getField()); + if (!d.getRawType().isAssignableFrom(value.type().getRawType())) { + value = tryInlineCast(value, d.getTypeRef()); + } + return new StaticInvoke( + accessorClass, d.getName(), PRIMITIVE_VOID_TYPE, false, bean, value); + } + } + if (d.getWriteMethod() != null && !Modifier.isPrivate(d.getWriteMethod().getModifiers())) { + if (AccessorHelper.defineSetter(d.getWriteMethod())) { + Class accessorClass = AccessorHelper.getAccessorClass(d.getWriteMethod()); + if (!d.getRawType().isAssignableFrom(value.type().getRawType())) { + value = tryInlineCast(value, d.getTypeRef()); + } + return new StaticInvoke( + accessorClass, d.getWriteMethod().getName(), PRIMITIVE_VOID_TYPE, false, bean, value); + } + } + return unsafeSetField(bean, d, value); + } + } + + /** + * Returns an expression that set field value to bean using reflection. + */ + private Expression reflectSetField(Expression bean, Field field, Expression value) { + // Class maybe have getter, but don't have setter, so we can't rely on reflectAccessField to + // populate fieldMap + Reference fieldRef = getReflectField(getRawType(bean.type()), field); + Preconditions.checkNotNull(fieldRef); + return new Invoke(fieldRef, "set", bean, value); + } + + /** + * Returns an expression that set field value to bean using `Unsafe`. + */ + private Expression unsafeSetField(Expression bean, Descriptor descriptor, Expression value) { + TypeRef fieldType = descriptor.getTypeRef(); + Reference fieldAccessor = getFieldAccessor(beanClass, 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); + } + } + + private Reference getReflectField(Class cls, Field field) { + return getReflectField(cls, field, true); + } + + private Reference getReflectField(Class cls, Field field, boolean setAccessible) { + String fieldName = field.getName(); + String fieldRefName; + if (duplicatedFields.contains(fieldName)) { + fieldRefName = + field.getDeclaringClass().getName().replaceAll("\\.|\\$", "_") + "_" + fieldName + "_Field"; + } else { + fieldRefName = fieldName + "_Field"; + } + return getOrCreateField( + true, + Field.class, + fieldRefName, + () -> { + TypeRef fieldTypeRef = TypeRef.of(Field.class); + Class declaringClass = field.getDeclaringClass(); + Expression classExpr = + staticClassFieldExpr( + declaringClass, declaringClass.getName().replaceAll("\\.|\\$", "_") + "__class__"); + Expression fieldExpr; + if (GraalvmSupport.isGraalBuildTime()) { + fieldExpr = + inlineInvoke( + classExpr, "getDeclaredField", fieldTypeRef, Literal.ofString(fieldName)); + } else { + fieldExpr = + reflectionUtilsInvoke( + "getField", fieldTypeRef, classExpr, Literal.ofString(fieldName)); + } + if (!setAccessible) { + return fieldExpr; + } + Invoke setAccess = new Invoke(fieldExpr, "setAccessible", Literal.True); + return new ListExpression(setAccess, fieldExpr); + }); + } + + protected Reference getOrCreateField( + boolean isStatic, Class type, String fieldName, Supplier value) { + Reference fieldRef = fieldMap.get(fieldName); + if (fieldRef == null) { + fieldName = ctx.newName(fieldName); + ctx.addField(isStatic, true, ctx.type(type), fieldName, value.get()); + fieldRef = new Reference(fieldName, TypeRef.of(type)); + fieldMap.put(fieldName, fieldRef); + } + return fieldRef; + } + + /** 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) && ReflectionUtils.hasPublicNoArgConstructor(beanClass)) { + return new Expression.NewInstance(beanType); + } else { + ObjectCreators.getObjectCreator(beanClass); // trigger cache + Invoke newInstance = new Invoke(getObjectCreator(beanClass), "newInstance", OBJECT_TYPE); + return sourcePublicAccessible(beanClass) ? new Cast(newInstance, beanType) : newInstance; + } + } + + protected Expression getObjectCreator(Class type) { + ObjectCreators.getObjectCreator(type); // trigger cache + return getOrCreateField( + true, + ObjectCreator.class, + ctx.newName("objectCreator_" + type.getSimpleName()), + () -> + new StaticInvoke( + ObjectCreators.class, + "getObjectCreator", + TypeRef.of(ObjectCreator.class), + staticBeanClassExpr())); + } + + protected void buildRecordComponentDefaultValues() { + ctx.reserveName(recordComponentDefaultValues.name()); + StaticInvoke expr = + new StaticInvoke( + RecordUtils.class, + "buildRecordComponentDefaultValues", + OBJECT_ARRAY_TYPE, + beanClassExpr()); + ctx.addField(Object[].class, recordComponentDefaultValues.name(), expr); + } + + static boolean recordCtrAccessible(Class cls) { + // support unexported packages in module + if (!Modifier.isPublic(cls.getModifiers())) { + return false; + } + for (RecordComponent component : Objects.requireNonNull(RecordUtils.getRecordComponents(cls))) { + if (!Modifier.isPublic(component.getType().getModifiers())) { + return false; + } + } + return true; + } + + protected Expression beanClassExpr(Class cls) { + if (cls == beanClass) { + return staticBeanClassExpr(); + } + if (GraalvmSupport.isGraalBuildTime()) { + String name = cls.getName().replaceAll("\\.|\\$", "_") + "__class__"; + return getOrCreateField( + true, + Class.class, + name, + () -> + inlineReflectionUtilsInvoke( + "loadClass", CLASS_TYPE, Literal.ofString(cls.getName()))); + } + throw new UnsupportedOperationException(); + } + + protected Expression beanClassExpr() { + if (GraalvmSupport.isGraalBuildTime()) { + return staticBeanClassExpr(); + } + throw new UnsupportedOperationException(); + } + + protected Expression staticBeanClassExpr() { + if (sourcePublicAccessible(beanClass)) { + return Literal.ofClass(beanClass); + } + return staticClassFieldExpr(beanClass, "__class__"); + } + + protected Expression staticClassFieldExpr(Class cls, String fieldName) { + if (sourcePublicAccessible(cls)) { + return Literal.ofClass(cls); + } + return getOrCreateField( + true, + Class.class, + fieldName, + () -> + inlineReflectionUtilsInvoke("loadClass", CLASS_TYPE, Literal.ofString(cls.getName()))); + } + + private StaticInvoke reflectionUtilsInvoke( + String methodName, TypeRef returnType, Expression... arguments) { + return new StaticInvoke( + ReflectionUtils.class, methodName, "", returnType, false, false, false, arguments); + } + + private StaticInvoke inlineReflectionUtilsInvoke( + String methodName, TypeRef returnType, Expression... arguments) { + return new StaticInvoke( + ReflectionUtils.class, methodName, "", returnType, false, true, false, arguments); + } + + /** Build unsafePut operation. */ + protected Expression unsafePut(Expression base, Expression pos, Expression value) { + return new StaticInvoke(UnsafeOps.class, "putByte", base, pos, value); + } + + protected Expression unsafePutBoolean(Expression base, Expression pos, Expression value) { + return new StaticInvoke(UnsafeOps.class, "putBoolean", base, pos, value); + } + + protected Expression unsafePutChar(Expression base, Expression pos, Expression value) { + return new StaticInvoke(UnsafeOps.class, "putChar", base, pos, value); + } + + protected Expression unsafePutShort(Expression base, Expression pos, Expression value) { + return new StaticInvoke(UnsafeOps.class, "putShort", base, pos, value); + } + + protected Expression unsafePutInt(Expression base, Expression pos, Expression value) { + return new StaticInvoke(UnsafeOps.class, "putInt", base, pos, value); + } + + protected Expression unsafePutLong(Expression base, Expression pos, Expression value) { + return new StaticInvoke(UnsafeOps.class, "putLong", base, pos, value); + } + + protected Expression unsafePutFloat(Expression base, Expression pos, Expression value) { + return new StaticInvoke(UnsafeOps.class, "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); + } + + /** Build unsafeGet operation. */ + protected Expression unsafeGet(Expression base, Expression pos) { + return new StaticInvoke(UnsafeOps.class, "getByte", PRIMITIVE_BYTE_TYPE, base, pos); + } + + protected Expression unsafeGetBoolean(Expression base, Expression pos) { + return new StaticInvoke(UnsafeOps.class, "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); + if (!NativeByteOrder.IS_LITTLE_ENDIAN) { + expr = new StaticInvoke(Character.class, "reverseBytes", PRIMITIVE_CHAR_TYPE, expr.inline()); + } + return expr; + } + + protected Expression unsafeGetShort(Expression base, Expression pos) { + StaticInvoke expr = + new StaticInvoke(UnsafeOps.class, "getShort", PRIMITIVE_SHORT_TYPE, base, pos); + if (!NativeByteOrder.IS_LITTLE_ENDIAN) { + expr = new StaticInvoke(Short.class, "reverseBytes", PRIMITIVE_SHORT_TYPE, expr.inline()); + } + return expr; + } + + protected Expression unsafeGetInt(Expression base, Expression pos) { + StaticInvoke expr = new StaticInvoke(UnsafeOps.class, "getInt", PRIMITIVE_INT_TYPE, base, pos); + if (!NativeByteOrder.IS_LITTLE_ENDIAN) { + expr = new StaticInvoke(Integer.class, "reverseBytes", PRIMITIVE_INT_TYPE, expr.inline()); + } + return expr; + } + + protected Expression unsafeGetLong(Expression base, Expression pos) { + StaticInvoke expr = + new StaticInvoke(UnsafeOps.class, "getLong", PRIMITIVE_LONG_TYPE, base, pos); + if (!NativeByteOrder.IS_LITTLE_ENDIAN) { + expr = new StaticInvoke(Long.class, "reverseBytes", PRIMITIVE_LONG_TYPE, expr.inline()); + } + return expr; + } + + protected Expression unsafeGetFloat(Expression base, Expression pos) { + StaticInvoke expr = new StaticInvoke(UnsafeOps.class, "getInt", PRIMITIVE_INT_TYPE, base, pos); + if (!NativeByteOrder.IS_LITTLE_ENDIAN) { + expr = new StaticInvoke(Integer.class, "reverseBytes", PRIMITIVE_INT_TYPE, expr.inline()); + } + return new StaticInvoke(Float.class, "intBitsToFloat", PRIMITIVE_FLOAT_TYPE, expr.inline()); + } + + protected Expression unsafeGetDouble(Expression base, Expression pos) { + StaticInvoke expr = + new StaticInvoke(UnsafeOps.class, "getLong", PRIMITIVE_LONG_TYPE, base, pos); + if (!NativeByteOrder.IS_LITTLE_ENDIAN) { + expr = new StaticInvoke(Long.class, "reverseBytes", PRIMITIVE_LONG_TYPE, expr.inline()); + } + return new StaticInvoke(Double.class, "longBitsToDouble", PRIMITIVE_DOUBLE_TYPE, expr.inline()); + } + + protected Expression readChar(Expression buffer) { + return new Invoke(buffer, "readChar", PRIMITIVE_CHAR_TYPE); + } + + protected Expression readInt16(Expression buffer) { + String func = NativeByteOrder.IS_LITTLE_ENDIAN ? "_readInt16OnLE" : "_readInt16OnBE"; + return new Invoke(buffer, func, PRIMITIVE_SHORT_TYPE); + } + + protected Expression readInt32(Expression buffer) { + String func = NativeByteOrder.IS_LITTLE_ENDIAN ? "_readInt32OnLE" : "_readInt32OnBE"; + return new Invoke(buffer, func, PRIMITIVE_INT_TYPE); + } + + public static String readIntFunc() { + return NativeByteOrder.IS_LITTLE_ENDIAN ? "_readInt32OnLE" : "_readInt32OnBE"; + } + + protected Expression readVarInt32(Expression buffer) { + String func = NativeByteOrder.IS_LITTLE_ENDIAN ? "_readVarInt32OnLE" : "_readVarInt32OnBE"; + return new Invoke(buffer, func, PRIMITIVE_INT_TYPE); + } + + protected Expression readInt64(Expression buffer) { + return new Invoke(buffer, readLongFunc(), PRIMITIVE_LONG_TYPE); + } + + public static String readLongFunc() { + return NativeByteOrder.IS_LITTLE_ENDIAN ? "_readInt64OnLE" : "_readInt64OnBE"; + } + + public static String readInt16Func() { + return NativeByteOrder.IS_LITTLE_ENDIAN ? "_readInt16OnLE" : "_readInt16OnBE"; + } + + public static String readVarInt32Func() { + return NativeByteOrder.IS_LITTLE_ENDIAN ? "_readVarInt32OnLE" : "_readVarInt32OnBE"; + } + + public static String readFloat32Func() { + return NativeByteOrder.IS_LITTLE_ENDIAN ? "_readFloat32OnLE" : "_readFloat32OnBE"; + } + + public static String readFloat64Func() { + return NativeByteOrder.IS_LITTLE_ENDIAN ? "_readFloat64OnLE" : "_readFloat64OnBE"; + } + + protected Expression readFloat32(Expression buffer) { + return new Invoke(buffer, readFloat32Func(), PRIMITIVE_FLOAT_TYPE); + } + + protected Expression readFloat64(Expression buffer) { + return new Invoke(buffer, readFloat64Func(), PRIMITIVE_DOUBLE_TYPE); + } +} diff --git a/java/fory-core/src/main/java25/org/apache/fory/builder/ObjectCodecBuilder.java b/java/fory-core/src/main/java25/org/apache/fory/builder/ObjectCodecBuilder.java new file mode 100644 index 0000000000..b5c5f1ca59 --- /dev/null +++ b/java/fory-core/src/main/java25/org/apache/fory/builder/ObjectCodecBuilder.java @@ -0,0 +1,1465 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fory.builder; + +import static org.apache.fory.codegen.Code.LiteralValue.FalseLiteral; +import static org.apache.fory.codegen.Expression.Invoke.inlineInvoke; +import static org.apache.fory.codegen.ExpressionUtils.add; +import static org.apache.fory.codegen.ExpressionUtils.cast; +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; +import static org.apache.fory.type.TypeUtils.PRIMITIVE_VOID_TYPE; +import static org.apache.fory.type.TypeUtils.SHORT_TYPE; +import static org.apache.fory.type.TypeUtils.getRawType; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.SortedMap; +import java.util.TreeMap; +import org.apache.fory.Fory; +import org.apache.fory.codegen.Code; +import org.apache.fory.codegen.CodegenContext; +import org.apache.fory.codegen.Expression; +import org.apache.fory.codegen.Expression.Cast; +import org.apache.fory.codegen.Expression.Inlineable; +import org.apache.fory.codegen.Expression.Invoke; +import org.apache.fory.codegen.Expression.ListExpression; +import org.apache.fory.codegen.Expression.Literal; +import org.apache.fory.codegen.Expression.NewInstance; +import org.apache.fory.codegen.Expression.Reference; +import org.apache.fory.codegen.Expression.ReplaceStub; +import org.apache.fory.codegen.Expression.StaticInvoke; +import org.apache.fory.codegen.ExpressionVisitor; +import org.apache.fory.logging.Logger; +import org.apache.fory.logging.LoggerFactory; +import org.apache.fory.meta.TypeDef; +import org.apache.fory.platform.UnsafeOps; +import org.apache.fory.reflect.ObjectCreator; +import org.apache.fory.reflect.ObjectCreators; +import org.apache.fory.reflect.TypeRef; +import org.apache.fory.serializer.ObjectSerializer; +import org.apache.fory.serializer.AbstractObjectSerializer; +import org.apache.fory.type.BFloat16; +import org.apache.fory.type.Descriptor; +import org.apache.fory.type.DescriptorGrouper; +import org.apache.fory.type.DispatchId; +import org.apache.fory.type.Float16; +import org.apache.fory.type.TypeUtils; +import org.apache.fory.type.Types; +import org.apache.fory.util.StringUtils; +import org.apache.fory.util.function.SerializableSupplier; +import org.apache.fory.util.record.RecordUtils; + +/** + * Generate sequential read/write code for java serialization to speed up performance. It also + * reduces space overhead introduced by aligning. Codegen only for time-consuming field, others + * delegate to fory. + * + *

In order to improve jit-compile and inline, serialization code should be spilt groups to avoid + * huge/big methods. + * + *

With meta context share enabled and compatible mode, this serializer will take all non-inner + * final types as non-final, so that fory can write class definition when write class info for those + * types. + * + * @see ObjectCodecOptimizer for code stats and split heuristics. + */ +public class ObjectCodecBuilder extends BaseObjectCodecBuilder { + private static final Logger LOG = LoggerFactory.getLogger(ObjectCodecBuilder.class); + + 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); + Collection descriptors; + DescriptorGrouper grouper; + boolean shareMeta = fory.getConfig().isMetaShareEnabled(); + if (shareMeta) { + TypeDef typeDef = typeResolver(r -> r.getTypeDef(beanClass, true)); + descriptors = typeResolver(r -> typeDef.getDescriptors(r, beanClass)); + grouper = typeResolver(r -> r.createDescriptorGrouper(typeDef, beanClass)); + } else { + grouper = typeResolver(r -> r.getFieldDescriptorGrouper(beanClass, true, false)); + descriptors = grouper.getSortedDescriptors(); + } + if (org.apache.fory.util.Utils.DEBUG_OUTPUT_ENABLED) { + LOG.info( + "========== {} sorted descriptors for {} ==========", + descriptors.size(), + beanClass.getSimpleName()); + List sortedDescriptors = grouper.getSortedDescriptors(); + for (Descriptor d : sortedDescriptors) { + LOG.info( + " {} -> {}, ref {}, nullable {}", + StringUtils.toSnakeCase(d.getName()), + d.getTypeName(), + d.isTrackingRef(), + d.isNullable()); + } + } + classVersionHash = + typeResolver.checkClassVersion() + ? new Literal( + ObjectSerializer.computeStructHash(typeResolver, grouper), PRIMITIVE_INT_TYPE) + : null; + objectCodecOptimizer = new ObjectCodecOptimizer(beanClass, grouper, false, ctx); + if (isRecord) { + if (!recordCtrAccessible) { + buildRecordComponentDefaultValues(); + } + recordReversedMapping = RecordUtils.buildFieldToComponentMapping(beanClass); + } else { + initConstructorFields(grouper.getSortedDescriptors(), true); + } + } + + protected ObjectCodecBuilder(TypeRef beanType, Fory fory, Class superSerializerClass) { + super(beanType, fory, superSerializerClass); + this.classVersionHash = null; + if (isRecord) { + if (!recordCtrAccessible) { + buildRecordComponentDefaultValues(); + } + recordReversedMapping = RecordUtils.buildFieldToComponentMapping(beanClass); + } + } + + 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()); + } + + @Override + protected String codecSuffix() { + return ""; + } + + @Override + protected void addCommonImports() { + super.addCommonImports(); + ctx.addImport(Generated.GeneratedObjectSerializer.class); + } + + /** + * Return an expression that serialize java bean of type {@link CodecBuilder#beanClass} to buffer. + */ + @Override + public Expression buildEncodeExpression() { + Reference inputObject = new Reference(ROOT_OBJECT_NAME, OBJECT_TYPE, false); + Reference buffer = new Reference(BUFFER_NAME, bufferTypeRef, false); + + ListExpression expressions = new ListExpression(); + Expression bean = tryCastIfPublic(inputObject, beanType, ctx.newName(beanClass)); + expressions.add(bean); + if (typeResolver.checkClassVersion()) { + expressions.add(new Invoke(buffer, "writeInt32", classVersionHash)); + } + expressions.addAll(serializePrimitives(bean, buffer, objectCodecOptimizer.primitiveGroups)); + int numGroups = getNumGroups(objectCodecOptimizer); + addGroupExpressions( + objectCodecOptimizer.boxedWriteGroups, numGroups, expressions, bean, buffer); + addGroupExpressions( + objectCodecOptimizer.nonPrimitiveWriteGroups, numGroups, expressions, bean, buffer); + return expressions; + } + + private void addGroupExpressions( + List> writeGroup, + int numGroups, + ListExpression expressions, + Expression bean, + Reference buffer) { + for (List group : writeGroup) { + if (group.isEmpty()) { + continue; + } + boolean inline = hasFewFields() || (group.size() == 1 && numGroups < 10); + expressions.add(serializeGroup(group, bean, buffer, inline)); + } + } + + protected boolean hasFewFields() { + return objectCodecOptimizer.descriptorGrouper.getNumDescriptors() < 6; + } + + protected int getNumGroups(ObjectCodecOptimizer objectCodecOptimizer) { + return objectCodecOptimizer.boxedWriteGroups.size() + + objectCodecOptimizer.nonPrimitiveWriteGroups.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; + } + + private Expression serializeGroup( + List group, Expression bean, Expression buffer, boolean inline) { + SerializableSupplier expressionSupplier = + () -> { + ListExpression groupExpressions = new ListExpression(); + for (Descriptor d : group) { + // `bean` will be replaced by `Reference` to cut-off expr dependency. + Expression fieldValue = getFieldValue(bean, d); + walkPath.add(d.getDeclaringClass() + d.getName()); + Expression fieldExpr = serializeField(fieldValue, buffer, d); + walkPath.removeLast(); + groupExpressions.add(fieldExpr); + } + return groupExpressions; + }; + if (inline) { + return expressionSupplier.get(); + } + return objectCodecOptimizer.invokeGenerated( + writeCutPoints(bean, buffer), expressionSupplier.get(), "writeFields"); + } + + /** + * Return a list of expressions that serialize all primitive fields. This can reduce unnecessary + * grow call and increment writerIndex in writeXXX. + */ + private List serializePrimitives( + Expression bean, Expression buffer, List> primitiveGroups) { + int totalSize = getTotalSizeOfPrimitives(primitiveGroups); + if (totalSize == 0) { + return new ArrayList<>(); + } + if (config.compressInt() || config.compressLong()) { + return serializePrimitivesCompressed(bean, buffer, primitiveGroups, totalSize); + } else { + return serializePrimitivesUnCompressed(bean, buffer, primitiveGroups, totalSize); + } + } + + protected int getNumPrimitiveFields(List> primitiveGroups) { + return primitiveGroups.stream().mapToInt(List::size).sum(); + } + + private List serializePrimitivesUnCompressed( + Expression bean, Expression buffer, List> primitiveGroups, int totalSize) { + List expressions = new ArrayList<>(); + int numPrimitiveFields = getNumPrimitiveFields(primitiveGroups); + Literal totalSizeLiteral = new Literal(totalSize, PRIMITIVE_INT_TYPE); + // After this grow, following writes can be unsafe without checks. + 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(); + // use Reference to cut-off expr dependency. + for (Descriptor descriptor : group) { + int dispatchId = getNumericDescriptorDispatchId(descriptor); + // `bean` will be replaced by `Reference` to cut-off expr dependency. + Expression fieldValue = getFieldValue(bean, descriptor); + if (fieldValue instanceof Inlineable) { + ((Inlineable) fieldValue).inline(); + } + if (dispatchId == DispatchId.BOOL) { + groupExpressions.add(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 serializePrimitivesCompressed( + Expression bean, Expression buffer, List> primitiveGroups, int totalSize) { + List expressions = new ArrayList<>(); + // int/long may need extra one-byte for writing. + 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) { + // varint may be written as 5bytes, use 8bytes for written as long to reduce cost. + 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; // long use 1~9 bytes. + } + } + } + int growSize = totalSize + extraSize; + // After this grow, following writes can be unsafe without checks. + 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); + // use Reference to cut-off expr dependency. + int acc = 0; + boolean compressStarted = false; + for (Descriptor descriptor : group) { + int dispatchId = getNumericDescriptorDispatchId(descriptor); + // `bean` will be replaced by `Reference` to cut-off expr dependency. + Expression fieldValue = getFieldValue(bean, descriptor); + if (fieldValue instanceof Inlineable) { + ((Inlineable) fieldValue).inline(); + } + if (dispatchId == DispatchId.BOOL) { + groupExpressions.add(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) { + // int/long are sorted in the last. + addIncWriterIndexExpr(groupExpressions, buffer, acc); + } + if (hasFewFields() || numPrimitiveFields < 4) { + expressions.add(groupExpressions); + } else { + expressions.add( + objectCodecOptimizer.invokeGenerated( + ofHashSet(bean, buffer, 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) + : new Invoke(boxedNumericValue(fieldValue, descriptor), "byteValue", PRIMITIVE_BYTE_TYPE); + } + + private Expression primitiveShortValue(Expression fieldValue, Descriptor descriptor) { + return fieldValue.type().isPrimitive() + ? cast(fieldValue, PRIMITIVE_SHORT_TYPE) + : new Invoke(boxedNumericValue(fieldValue, descriptor), "shortValue", PRIMITIVE_SHORT_TYPE); + } + + private Expression primitiveIntValue(Expression fieldValue, Descriptor descriptor) { + return fieldValue.type().isPrimitive() + ? cast(fieldValue, PRIMITIVE_INT_TYPE) + : new Invoke(boxedNumericValue(fieldValue, descriptor), "intValue", PRIMITIVE_INT_TYPE); + } + + private Expression boxedNumericValue(Expression fieldValue, Descriptor descriptor) { + return Number.class.isAssignableFrom(getRawType(fieldValue.type())) + ? fieldValue + : cast(fieldValue, descriptor.getTypeRef()); + } + + private void addIncWriterIndexExpr(ListExpression expressions, Expression buffer, int diff) { + if (diff != 0) { + expressions.add(new Invoke(buffer, "_increaseWriterIndexUnsafe", Literal.ofInt(diff))); + } + } + + private int getTotalSizeOfPrimitives(List> primitiveGroups) { + return primitiveGroups.stream() + .flatMap(Collection::stream) + .mapToInt( + d -> { + Class rawType = d.getRawType(); + if (TypeUtils.isPrimitive(rawType) || TypeUtils.isBoxed(rawType)) { + return TypeUtils.getSizeOfPrimitiveType(TypeUtils.unwrap(rawType)); + } + return Types.getPrimitiveTypeSize(Types.getDescriptorTypeId(typeResolver, d)); + }) + .sum(); + } + + private Expression getWriterPos(Expression writerPos, long acc) { + if (acc == 0) { + return writerPos; + } + return add(writerPos, Literal.ofLong(acc)); + } + + public Expression buildDecodeExpression() { + Reference buffer = new Reference(BUFFER_NAME, bufferTypeRef, false); + ListExpression expressions = new ListExpression(); + if (typeResolver.checkClassVersion()) { + expressions.add(checkClassVersion(buffer)); + } + if (!isRecord && constructorFieldIndexes != null) { + return buildConstructorDecodeExpression(buffer, expressions); + } + Expression bean; + if (!isRecord) { + 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(); + } else { + bean = buildComponentsArray(); + } + } + expressions.addAll(deserializePrimitives(bean, buffer, objectCodecOptimizer.primitiveGroups)); + int numGroups = getNumGroups(objectCodecOptimizer); + deserializeReadGroup( + objectCodecOptimizer.boxedReadGroups, numGroups, expressions, bean, buffer); + deserializeReadGroup( + objectCodecOptimizer.nonPrimitiveReadGroups, numGroups, expressions, bean, buffer); + if (isRecord) { + if (recordCtrAccessible) { + assert bean instanceof FieldsCollector; + FieldsCollector collector = (FieldsCollector) bean; + bean = createRecord(collector.recordValuesMap); + } else { + ObjectCreators.getObjectCreator(beanClass); // trigger cache and make error raised early + bean = + new Invoke(getObjectCreator(beanClass), "newInstanceWithArguments", OBJECT_TYPE, bean); + } + } + 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 StaticInvoke( + UnsafeOps.class, "copyObjectArray", OBJECT_ARRAY_TYPE, recordComponentDefaultValues); + } + + 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 (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; + } + + private void addNonConstructorFieldSetters( + ListExpression expressions, Expression bean, FieldsArray fieldValues) { + for (Descriptor descriptor : objectCodecOptimizer.descriptorGrouper.getSortedDescriptors()) { + int index = fieldIndexes.get(descriptor); + if (constructorFieldMask[index]) { + continue; + } + 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)); + } + } + + 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)); + } + } + + 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 { + private final TreeMap recordValuesMap = new TreeMap<>(); + + protected FieldsCollector() { + super(new Expression[0]); + } + + @Override + public TypeRef type() { + return beanType; + } + + @Override + public Code.ExprCode doGenCode(CodegenContext ctx) { + return new Code.ExprCode(FalseLiteral, Code.variable(getRawType(beanType), "null")); + } + } + + 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) { + ((Inlineable) value).inline(false); + } + int index = recordReversedMapping.get(d.getName()); + FieldsCollector collector = (FieldsCollector) bean; + collector.recordValuesMap.put(index, value); + return value; + } else { + int index = recordReversedMapping.get(d.getName()); + return new Expression.AssignArrayElem(bean, value, Literal.ofInt(index)); + } + } + return super.setFieldValue(bean, d, value); + } + + protected Expression deserializeGroup( + List group, Expression bean, Expression buffer, boolean inline) { + if (isRecord) { + return deserializeGroupForRecord(group, bean, buffer); + } + SerializableSupplier exprSupplier = + () -> { + ListExpression groupExpressions = new ListExpression(); + // use Reference to cut-off expr dependency. + for (Descriptor d : group) { + ExpressionVisitor.ExprHolder exprHolder = ExpressionVisitor.ExprHolder.of("bean", bean); + walkPath.add(d.getDeclaringClass() + d.getName()); + TypeRef castTypeRef = + hasCompatibleCollectionArrayRead(d) + ? compatibleReadTargetTypeRef(d) + : d.getTypeRef(); + Expression action = + deserializeField( + buffer, + d, + // `bean` will be replaced by `Reference` to cut-off expr + // dependency. + expr -> + setFieldValue(exprHolder.get("bean"), d, tryInlineCast(expr, castTypeRef))); + walkPath.removeLast(); + if (needsGeneratedReadFieldMethod(d)) { + action = + objectCodecOptimizer.invokeGenerated( + readCutPoints(bean, buffer), action, "readField"); + } + groupExpressions.add(action); + } + return groupExpressions; + }; + if (inline) { + return exprSupplier.get(); + } else { + return objectCodecOptimizer.invokeGenerated( + readCutPoints(bean, buffer), exprSupplier.get(), "readFields"); + } + } + + private boolean needsGeneratedReadFieldMethod(Descriptor descriptor) { + return !hasFewFields() + && !isMonomorphic(descriptor) + && !useCollectionSerialization(descriptor) + && !useMapSerialization(descriptor.getTypeRef()); + } + + protected Expression deserializeGroupForRecord( + List group, Expression bean, Expression buffer) { + ListExpression groupExpressions = new ListExpression(); + // use Reference to cut-off expr dependency. + for (Descriptor d : group) { + TypeRef castTypeRef = + hasCompatibleCollectionArrayRead(d) ? compatibleReadTargetTypeRef(d) : d.getTypeRef(); + Expression value = deserializeField(buffer, d, expr -> expr); + Expression action = setFieldValue(bean, d, tryInlineCast(value, castTypeRef)); + groupExpressions.add(action); + } + return groupExpressions; + } + + private Expression checkClassVersion(Expression buffer) { + return new StaticInvoke( + ObjectSerializer.class, + "checkClassVersion", + PRIMITIVE_VOID_TYPE, + false, + beanClassExpr(), + inlineInvoke(buffer, readIntFunc(), PRIMITIVE_INT_TYPE), + Objects.requireNonNull(classVersionHash)); + } + + /** + * Return a list of expressions that deserialize all primitive fields. This can reduce unnecessary + * check call and increment readerIndex in writeXXX. + */ + protected List deserializePrimitives( + Expression bean, Expression buffer, List> primitiveGroups) { + int totalSize = getTotalSizeOfPrimitives(primitiveGroups); + if (totalSize == 0) { + return new ArrayList<>(); + } + if (config.compressInt() || config.compressLong()) { + return deserializeCompressedPrimitives(bean, buffer, primitiveGroups); + } else { + return deserializeUnCompressedPrimitives(bean, buffer, primitiveGroups, totalSize); + } + } + + private List deserializeUnCompressedPrimitives( + Expression bean, Expression buffer, List> primitiveGroups, int totalSize) { + List expressions = new ArrayList<>(); + int numPrimitiveFields = getNumPrimitiveFields(primitiveGroups); + Literal totalSizeLiteral = Literal.ofInt(totalSize); + // After this check, following read can be totally unsafe without checks + 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); + } + // `bean` will be replaced by `Reference` to cut-off expr dependency. + groupExpressions.add(setFieldValue(bean, descriptor, fieldValue)); + } + if (hasFewFields() || numPrimitiveFields < 4 || isRecord) { + expressions.add(groupExpressions); + } else { + expressions.add( + objectCodecOptimizer.invokeGenerated( + ofHashSet(bean, buffer, readerIndex), groupExpressions, "readFields")); + } + } + Expression increaseReaderIndex = + new Invoke( + buffer, "increaseReaderIndex", new Literal(totalSizeLiteral, PRIMITIVE_INT_TYPE)); + expressions.add(increaseReaderIndex); + return expressions; + } + + private List deserializeCompressedPrimitives( + Expression bean, Expression buffer, List> primitiveGroups) { + List expressions = new ArrayList<>(); + int numPrimitiveFields = getNumPrimitiveFields(primitiveGroups); + for (List group : primitiveGroups) { + // After this check, following read can be totally unsafe without checks. + // checkReadableBytes first, `fillBuffer` may create a new heap buffer. + ReplaceStub checkReadableBytesStub = new ReplaceStub(); + expressions.add(checkReadableBytesStub); + Expression 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); + } + // `bean` will be replaced by `Reference` to cut-off expr dependency. + groupExpressions.add(setFieldValue(bean, descriptor, fieldValue)); + } + if (acc != 0) { + checkReadableBytesStub.setTargetObject( + new Invoke(buffer, "checkReadableBytes", Literal.ofInt(acc))); + } + if (!compressStarted) { + addIncReaderIndexExpr(groupExpressions, buffer, acc); + } + if (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))); + } + } + + private Expression getReaderAddress(Expression readerPos, long acc) { + if (acc == 0) { + return readerPos; + } + 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/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..82203215cf --- /dev/null +++ b/java/fory-core/src/main/java25/org/apache/fory/memory/MemoryBuffer.java @@ -0,0 +1,4521 @@ +/* + * 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.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.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; + +/** + * 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 = !AndroidSupport.IS_ANDROID && UnsafeOps.unaligned(); + private static final ByteOrder NATIVE_ORDER = ByteOrder.nativeOrder(); + 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); + private static final ValueLayout.OfChar NATIVE_CHAR = + ValueLayout.JAVA_CHAR_UNALIGNED.withOrder(NATIVE_ORDER); + private static final ValueLayout.OfShort NATIVE_SHORT = + ValueLayout.JAVA_SHORT_UNALIGNED.withOrder(NATIVE_ORDER); + private static final ValueLayout.OfInt NATIVE_INT = + ValueLayout.JAVA_INT_UNALIGNED.withOrder(NATIVE_ORDER); + private static final ValueLayout.OfLong NATIVE_LONG = + ValueLayout.JAVA_LONG_UNALIGNED.withOrder(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; + MemorySegment offHeapSegment; + // 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; + private final MemoryAccess memoryAccess = new MemoryAccess(this); + + // 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; + this.nativeOffHeapBuffer = offHeapBuffer.duplicate().order(NATIVE_ORDER); + ByteBuffer segmentBuffer = offHeapBuffer.duplicate(); + ByteBufferUtil.position(segmentBuffer, 0); + this.offHeapSegment = MemorySegment.ofBuffer(segmentBuffer); + 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; + this.offHeapSegment = null; + // Versioned UnsafeOps array base offsets are zero on JDK25; address is a logical byte index. + final long startPos = UnsafeOps.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 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(); + } + memoryAccess.copyMemory(null, pos, dst, UnsafeOps.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(); + } + final long arrayAddress = UnsafeOps.BYTE_ARRAY_OFFSET + offset; + memoryAccess.copyMemory(src, arrayAddress, null, pos, 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); + memoryAccess.copyMemory( + values, + UnsafeOps.BOOLEAN_ARRAY_OFFSET + offset, + heapMemory, + address + writerIdx, + 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); + memoryAccess.copyMemory( + values, + UnsafeOps.CHAR_ARRAY_OFFSET + ((long) offset << 1), + heapMemory, + address + writerIdx, + 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); + memoryAccess.copyMemory( + values, + UnsafeOps.SHORT_ARRAY_OFFSET + ((long) offset << 1), + heapMemory, + address + writerIdx, + 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); + memoryAccess.copyMemory( + values, + UnsafeOps.INT_ARRAY_OFFSET + ((long) offset << 2), + heapMemory, + address + writerIdx, + 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); + memoryAccess.copyMemory( + values, + UnsafeOps.LONG_ARRAY_OFFSET + ((long) offset << 3), + heapMemory, + address + writerIdx, + 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); + memoryAccess.copyMemory( + values, + UnsafeOps.FLOAT_ARRAY_OFFSET + ((long) offset << 2), + heapMemory, + address + writerIdx, + 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); + memoryAccess.copyMemory( + values, + UnsafeOps.DOUBLE_ARRAY_OFFSET + ((long) offset << 3), + heapMemory, + address + writerIdx, + 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; + } + byte[] heapMemory = this.heapMemory; + if (heapMemory != null) { + // System.arraycopy faster for some jdk than Unsafe. + System.arraycopy(heapMemory, heapOffset + readerIdx, bytes, 0, length); + } else { + memoryAccess.copyMemory(null, address + readerIdx, bytes, UnsafeOps.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) memoryAccess.getByte(null, 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; + } + byte[] heapMemory = this.heapMemory; + if (heapMemory != null) { + System.arraycopy(heapMemory, heapOffset + readerIdx, arr, 0, numBytes); + } else { + memoryAccess.copyMemory( + null, address + readerIdx, arr, UnsafeOps.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; + } + byte[] heapMemory = this.heapMemory; + if (heapMemory != null) { + System.arraycopy(heapMemory, heapOffset + readerIdx, values, 0, numBytes); + } else { + memoryAccess.copyMemory(null, address + readerIdx, values, UnsafeOps.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; + } + memoryAccess.copyMemory( + heapMemory, address + readerIdx, values, UnsafeOps.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; + } + memoryAccess.copyMemory( + heapMemory, address + readerIdx, values, UnsafeOps.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; + } + memoryAccess.copyMemory( + heapMemory, address + readerIdx, values, UnsafeOps.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; + } + memoryAccess.copyMemory( + heapMemory, address + readerIdx, values, UnsafeOps.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; + } + memoryAccess.copyMemory( + heapMemory, address + readerIdx, values, UnsafeOps.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; + } + memoryAccess.copyMemory( + heapMemory, address + readerIdx, values, UnsafeOps.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; + } + memoryAccess.copyMemory( + heapMemory, address + readerIdx, values, UnsafeOps.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; + memoryAccess.copyMemory( + heapMemory, + address + readerIdx, + values, + UnsafeOps.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; + memoryAccess.copyMemory( + heapMemory, + address + readerIdx, + chars, + UnsafeOps.CHAR_ARRAY_OFFSET + ((long) offset << 1), + 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; + memoryAccess.copyMemory( + heapMemory, + address + readerIdx, + values, + UnsafeOps.SHORT_ARRAY_OFFSET + ((long) offset << 1), + 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; + memoryAccess.copyMemory( + heapMemory, + address + readerIdx, + values, + UnsafeOps.INT_ARRAY_OFFSET + ((long) offset << 2), + 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; + memoryAccess.copyMemory( + heapMemory, + address + readerIdx, + values, + UnsafeOps.LONG_ARRAY_OFFSET + ((long) offset << 3), + 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; + memoryAccess.copyMemory( + heapMemory, + address + readerIdx, + values, + UnsafeOps.FLOAT_ARRAY_OFFSET + ((long) offset << 2), + 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; + memoryAccess.copyMemory( + heapMemory, + address + readerIdx, + values, + UnsafeOps.DOUBLE_ARRAY_OFFSET + ((long) offset << 3), + 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) { + memoryAccess.copyMemory(thisHeapRef, thisPointer, otherHeapRef, 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); + } + + /** + * 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.throwRawUnsafeMemoryCopyUnsupported(); + } else { + checkArgument(target != null, "Raw native-address target copy is unsupported on JDK25"); + final long thisPointer = this.address + offset; + checkArgument(thisPointer + numBytes <= addressLimit); + memoryAccess.copyMemory(this.heapMemory, thisPointer, target, targetPointer, 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) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.throwRawUnsafeMemoryCopyUnsupported(); + } else { + checkArgument(source != null, "Raw native-address source copy is unsupported on JDK25"); + final long thisPointer = this.address + offset; + checkArgument(thisPointer + numBytes <= addressLimit); + memoryAccess.copyMemory(source, sourcePointer, heapMemory, thisPointer, 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; + 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(memoryAccess, heapMemory, pos1, buf2.memoryAccess, 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( + memoryAccess, heapMemory, pos, memoryAccess, bytes, UnsafeOps.BYTE_ARRAY_OFFSET + bytesOffset, len); + } + + private static boolean unsafeEqualTo( + MemoryAccess leftAccess, + Object leftBase, + long leftOffset, + MemoryAccess rightAccess, + Object rightBase, + long rightOffset, + int length) { + int i = 0; + if ((leftOffset % 8) == (rightOffset % 8)) { + while ((leftOffset + i) % 8 != 0 && i < length) { + if (leftAccess.getByte(leftBase, leftOffset + i) + != rightAccess.getByte(rightBase, rightOffset + i)) { + return false; + } + i += 1; + } + } + if (UNALIGNED || (((leftOffset + i) % 8 == 0) && ((rightOffset + i) % 8 == 0))) { + while (i <= length - 8) { + if (leftAccess.getLong(leftBase, leftOffset + i) + != rightAccess.getLong(rightBase, rightOffset + i)) { + return false; + } + i += 8; + } + } + while (i < length) { + if (leftAccess.getByte(leftBase, leftOffset + i) + != rightAccess.getByte(rightBase, rightOffset + i)) { + return false; + } + i += 1; + } + return true; + } + + private static final class MemoryAccess { + private final MemoryBuffer buffer; + + private MemoryAccess(MemoryBuffer buffer) { + this.buffer = buffer; + } + + private byte getByte(Object base, long offset) { + if (base != null) { + return UnsafeOps.getByte(base, offset); + } + return directSegment().get(ValueLayout.JAVA_BYTE, offset); + } + + private void putByte(Object base, long offset, byte value) { + if (base != null) { + UnsafeOps.putByte(base, offset, value); + return; + } + directSegment().set(ValueLayout.JAVA_BYTE, offset, value); + } + + private char getChar(Object base, long offset) { + if (base != null) { + return UnsafeOps.getChar(base, offset); + } + return directSegment().get(NATIVE_CHAR, offset); + } + + private void putChar(Object base, long offset, char value) { + if (base != null) { + UnsafeOps.putChar(base, offset, value); + return; + } + directSegment().set(NATIVE_CHAR, offset, value); + } + + private short getShort(Object base, long offset) { + if (base != null) { + return UnsafeOps.getShort(base, offset); + } + return directSegment().get(NATIVE_SHORT, offset); + } + + private void putShort(Object base, long offset, short value) { + if (base != null) { + UnsafeOps.putShort(base, offset, value); + return; + } + directSegment().set(NATIVE_SHORT, offset, value); + } + + private int getInt(Object base, long offset) { + if (base != null) { + return UnsafeOps.getInt(base, offset); + } + return directSegment().get(NATIVE_INT, offset); + } + + private void putInt(Object base, long offset, int value) { + if (base != null) { + UnsafeOps.putInt(base, offset, value); + return; + } + directSegment().set(NATIVE_INT, offset, value); + } + + private long getLong(Object base, long offset) { + if (base != null) { + return UnsafeOps.getLong(base, offset); + } + return directSegment().get(NATIVE_LONG, offset); + } + + private void putLong(Object base, long offset, long value) { + if (base != null) { + UnsafeOps.putLong(base, offset, value); + return; + } + directSegment().set(NATIVE_LONG, offset, value); + } + + private void copyMemory( + Object src, long srcOffset, Object dst, long dstOffset, long length) { + int len = toIntLength(length); + if (len == 0) { + return; + } + if (src != null && dst != null) { + if (dst instanceof byte[] && copyArrayToBytes(src, srcOffset, (byte[]) dst, toIntIndex(dstOffset), len)) { + return; + } + if (src instanceof byte[] && copyBytesToArray((byte[]) src, toIntIndex(srcOffset), dst, dstOffset, len)) { + return; + } + UnsafeOps.copyMemory(src, srcOffset, dst, dstOffset, len); + } else if (src == null && dst == null) { + copyDirect(srcOffset, dstOffset, len); + } else if (src == null) { + if (dst instanceof byte[]) { + readDirect(srcOffset, (byte[]) dst, toIntIndex(dstOffset), len); + } else if (readArray(srcOffset, dst, dstOffset, len)) { + return; + } else { + for (int i = 0; i < len; i++) { + UnsafeOps.putByte(dst, dstOffset + i, getByte(null, srcOffset + i)); + } + } + } else if (src instanceof byte[]) { + writeDirect(dstOffset, (byte[]) src, toIntIndex(srcOffset), len); + } else if (writeArray(src, srcOffset, dstOffset, len)) { + return; + } else { + for (int i = 0; i < len; i++) { + putByte(null, dstOffset + i, UnsafeOps.getByte(src, srcOffset + i)); + } + } + } + + private boolean copyArrayToBytes( + Object src, long srcOffset, byte[] dst, int dstOffset, int len) { + if (src instanceof boolean[]) { + boolean[] array = (boolean[]) src; + int srcIndex = toIntIndex(srcOffset); + for (int i = 0; i < len; i++) { + dst[dstOffset + i] = array[srcIndex + i] ? (byte) 1 : (byte) 0; + } + return true; + } else if (src instanceof char[] && aligned(srcOffset, len, Character.BYTES)) { + heapBytes(dst, dstOffset, len) + .asCharBuffer() + .put((char[]) src, toIntIndex(srcOffset / Character.BYTES), len / Character.BYTES); + return true; + } else if (src instanceof short[] && aligned(srcOffset, len, Short.BYTES)) { + heapBytes(dst, dstOffset, len) + .asShortBuffer() + .put((short[]) src, toIntIndex(srcOffset / Short.BYTES), len / Short.BYTES); + return true; + } else if (src instanceof int[] && aligned(srcOffset, len, Integer.BYTES)) { + heapBytes(dst, dstOffset, len) + .asIntBuffer() + .put((int[]) src, toIntIndex(srcOffset / Integer.BYTES), len / Integer.BYTES); + return true; + } else if (src instanceof long[] && aligned(srcOffset, len, Long.BYTES)) { + heapBytes(dst, dstOffset, len) + .asLongBuffer() + .put((long[]) src, toIntIndex(srcOffset / Long.BYTES), len / Long.BYTES); + return true; + } else if (src instanceof float[] && aligned(srcOffset, len, Float.BYTES)) { + heapBytes(dst, dstOffset, len) + .asFloatBuffer() + .put((float[]) src, toIntIndex(srcOffset / Float.BYTES), len / Float.BYTES); + return true; + } else if (src instanceof double[] && aligned(srcOffset, len, Double.BYTES)) { + heapBytes(dst, dstOffset, len) + .asDoubleBuffer() + .put((double[]) src, toIntIndex(srcOffset / Double.BYTES), len / Double.BYTES); + return true; + } + return false; + } + + private boolean copyBytesToArray( + byte[] src, int srcOffset, Object dst, long dstOffset, int len) { + if (dst instanceof boolean[]) { + boolean[] array = (boolean[]) dst; + int dstIndex = toIntIndex(dstOffset); + for (int i = 0; i < len; i++) { + array[dstIndex + i] = src[srcOffset + i] != 0; + } + return true; + } else if (dst instanceof char[] && aligned(dstOffset, len, Character.BYTES)) { + heapBytes(src, srcOffset, len) + .asCharBuffer() + .get((char[]) dst, toIntIndex(dstOffset / Character.BYTES), len / Character.BYTES); + return true; + } else if (dst instanceof short[] && aligned(dstOffset, len, Short.BYTES)) { + heapBytes(src, srcOffset, len) + .asShortBuffer() + .get((short[]) dst, toIntIndex(dstOffset / Short.BYTES), len / Short.BYTES); + return true; + } else if (dst instanceof int[] && aligned(dstOffset, len, Integer.BYTES)) { + heapBytes(src, srcOffset, len) + .asIntBuffer() + .get((int[]) dst, toIntIndex(dstOffset / Integer.BYTES), len / Integer.BYTES); + return true; + } else if (dst instanceof long[] && aligned(dstOffset, len, Long.BYTES)) { + heapBytes(src, srcOffset, len) + .asLongBuffer() + .get((long[]) dst, toIntIndex(dstOffset / Long.BYTES), len / Long.BYTES); + return true; + } else if (dst instanceof float[] && aligned(dstOffset, len, Float.BYTES)) { + heapBytes(src, srcOffset, len) + .asFloatBuffer() + .get((float[]) dst, toIntIndex(dstOffset / Float.BYTES), len / Float.BYTES); + return true; + } else if (dst instanceof double[] && aligned(dstOffset, len, Double.BYTES)) { + heapBytes(src, srcOffset, len) + .asDoubleBuffer() + .get((double[]) dst, toIntIndex(dstOffset / Double.BYTES), len / Double.BYTES); + return true; + } + return false; + } + + private boolean writeArray(Object src, long srcOffset, long dstOffset, int len) { + if (src instanceof boolean[]) { + boolean[] array = (boolean[]) src; + int srcIndex = toIntIndex(srcOffset); + ByteBuffer dst = directBytes(dstOffset, len); + for (int i = 0; i < len; i++) { + dst.put(i, array[srcIndex + i] ? (byte) 1 : (byte) 0); + } + return true; + } else if (src instanceof char[] && aligned(srcOffset, len, Character.BYTES)) { + directBytes(dstOffset, len) + .asCharBuffer() + .put((char[]) src, toIntIndex(srcOffset / Character.BYTES), len / Character.BYTES); + return true; + } else if (src instanceof short[] && aligned(srcOffset, len, Short.BYTES)) { + directBytes(dstOffset, len) + .asShortBuffer() + .put((short[]) src, toIntIndex(srcOffset / Short.BYTES), len / Short.BYTES); + return true; + } else if (src instanceof int[] && aligned(srcOffset, len, Integer.BYTES)) { + directBytes(dstOffset, len) + .asIntBuffer() + .put((int[]) src, toIntIndex(srcOffset / Integer.BYTES), len / Integer.BYTES); + return true; + } else if (src instanceof long[] && aligned(srcOffset, len, Long.BYTES)) { + directBytes(dstOffset, len) + .asLongBuffer() + .put((long[]) src, toIntIndex(srcOffset / Long.BYTES), len / Long.BYTES); + return true; + } else if (src instanceof float[] && aligned(srcOffset, len, Float.BYTES)) { + directBytes(dstOffset, len) + .asFloatBuffer() + .put((float[]) src, toIntIndex(srcOffset / Float.BYTES), len / Float.BYTES); + return true; + } else if (src instanceof double[] && aligned(srcOffset, len, Double.BYTES)) { + directBytes(dstOffset, len) + .asDoubleBuffer() + .put((double[]) src, toIntIndex(srcOffset / Double.BYTES), len / Double.BYTES); + return true; + } + return false; + } + + private boolean readArray(long srcOffset, Object dst, long dstOffset, int len) { + if (dst instanceof boolean[]) { + boolean[] array = (boolean[]) dst; + int dstIndex = toIntIndex(dstOffset); + ByteBuffer src = directBytes(srcOffset, len); + for (int i = 0; i < len; i++) { + array[dstIndex + i] = src.get(i) != 0; + } + return true; + } else if (dst instanceof char[] && aligned(dstOffset, len, Character.BYTES)) { + directBytes(srcOffset, len) + .asCharBuffer() + .get((char[]) dst, toIntIndex(dstOffset / Character.BYTES), len / Character.BYTES); + return true; + } else if (dst instanceof short[] && aligned(dstOffset, len, Short.BYTES)) { + directBytes(srcOffset, len) + .asShortBuffer() + .get((short[]) dst, toIntIndex(dstOffset / Short.BYTES), len / Short.BYTES); + return true; + } else if (dst instanceof int[] && aligned(dstOffset, len, Integer.BYTES)) { + directBytes(srcOffset, len) + .asIntBuffer() + .get((int[]) dst, toIntIndex(dstOffset / Integer.BYTES), len / Integer.BYTES); + return true; + } else if (dst instanceof long[] && aligned(dstOffset, len, Long.BYTES)) { + directBytes(srcOffset, len) + .asLongBuffer() + .get((long[]) dst, toIntIndex(dstOffset / Long.BYTES), len / Long.BYTES); + return true; + } else if (dst instanceof float[] && aligned(dstOffset, len, Float.BYTES)) { + directBytes(srcOffset, len) + .asFloatBuffer() + .get((float[]) dst, toIntIndex(dstOffset / Float.BYTES), len / Float.BYTES); + return true; + } else if (dst instanceof double[] && aligned(dstOffset, len, Double.BYTES)) { + directBytes(srcOffset, len) + .asDoubleBuffer() + .get((double[]) dst, toIntIndex(dstOffset / Double.BYTES), len / Double.BYTES); + return true; + } + return false; + } + + private void copyDirect(long srcOffset, long dstOffset, int len) { + if (srcOffset < dstOffset && dstOffset < srcOffset + len) { + byte[] tmp = new byte[len]; + readDirect(srcOffset, tmp, 0, len); + writeDirect(dstOffset, tmp, 0, len); + } else { + MemorySegment segment = directSegment(); + MemorySegment.copy(segment, srcOffset, segment, dstOffset, len); + } + } + + private ByteBuffer directBuffer() { + ByteBuffer directBuffer = buffer.nativeOffHeapBuffer; + if (directBuffer == null) { + throw new IllegalStateException("Memory buffer does not own a ByteBuffer"); + } + return directBuffer; + } + + private MemorySegment directSegment() { + MemorySegment segment = buffer.offHeapSegment; + if (segment == null) { + throw new IllegalStateException("Memory buffer does not own an off-heap segment"); + } + return segment; + } + + private void readDirect(long offset, byte[] dst, int dstOffset, int length) { + directBuffer().get(toIntIndex(offset), dst, dstOffset, length); + } + + private void writeDirect(long offset, byte[] src, int srcOffset, int length) { + directBuffer().put(toIntIndex(offset), src, srcOffset, length); + } + + private ByteBuffer directBytes(long offset, int length) { + ByteBuffer duplicate = directBuffer().duplicate().order(NATIVE_ORDER); + int start = toIntIndex(offset); + ByteBufferUtil.position(duplicate, start); + duplicate.limit(start + length); + return duplicate.slice().order(NATIVE_ORDER); + } + + private static ByteBuffer heapBytes(byte[] bytes, int offset, int length) { + return ByteBuffer.wrap(bytes, offset, length).order(NATIVE_ORDER); + } + + private static boolean aligned(long offset, int length, int width) { + return offset % width == 0 && length % width == 0; + } + + 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 toIntLength(long length) { + if (length < 0 || length > Integer.MAX_VALUE) { + throw new IndexOutOfBoundsException("length out of int range: " + length); + } + return (int) length; + } + } + + @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/UnsafeOps.java b/java/fory-core/src/main/java25/org/apache/fory/platform/UnsafeOps.java index ce00b9b6c7..cd478704d3 100644 --- a/java/fory-core/src/main/java25/org/apache/fory/platform/UnsafeOps.java +++ b/java/fory-core/src/main/java25/org/apache/fory/platform/UnsafeOps.java @@ -34,6 +34,9 @@ public final class UnsafeOps { @SuppressWarnings("restriction") public static final Unsafe UNSAFE = _JDKAccess.UNSAFE; + // JDK25 array operations use Java/VarHandle indexes instead of raw Unsafe byte offsets. + // Keep these constants zero so versioned MemoryBuffer code can preserve the root API shape + // without mixing Unsafe base-offset domains into the zero-Unsafe runtime. public static final int BOOLEAN_ARRAY_OFFSET = 0; public static final int BYTE_ARRAY_OFFSET = 0; public static final int CHAR_ARRAY_OFFSET = 0; @@ -234,6 +237,9 @@ public static void copyMemory( System.arraycopy((byte[]) src, toIntIndex(srcOffset), (byte[]) dst, toIntIndex(dstOffset), len); return; } + if (copySamePrimitiveArray(src, srcOffset, dst, dstOffset, len)) { + return; + } if (!isPrimitiveArray(src) || !isPrimitiveArray(dst)) { throw unsupportedObjectMemory(); } @@ -248,10 +254,78 @@ public static void copyMemory( } } + private static boolean copySamePrimitiveArray( + Object src, long srcOffset, Object dst, long dstOffset, int len) { + if (src.getClass() != dst.getClass()) { + return false; + } + if (src instanceof boolean[]) { + System.arraycopy((boolean[]) src, toIntIndex(srcOffset), (boolean[]) dst, toIntIndex(dstOffset), len); + return true; + } else if (src instanceof char[] && aligned(srcOffset, dstOffset, len, Character.BYTES)) { + System.arraycopy( + (char[]) src, + toIntIndex(srcOffset / Character.BYTES), + (char[]) dst, + toIntIndex(dstOffset / Character.BYTES), + len / Character.BYTES); + return true; + } else if (src instanceof short[] && aligned(srcOffset, dstOffset, len, Short.BYTES)) { + System.arraycopy( + (short[]) src, + toIntIndex(srcOffset / Short.BYTES), + (short[]) dst, + toIntIndex(dstOffset / Short.BYTES), + len / Short.BYTES); + return true; + } else if (src instanceof int[] && aligned(srcOffset, dstOffset, len, Integer.BYTES)) { + System.arraycopy( + (int[]) src, + toIntIndex(srcOffset / Integer.BYTES), + (int[]) dst, + toIntIndex(dstOffset / Integer.BYTES), + len / Integer.BYTES); + return true; + } else if (src instanceof long[] && aligned(srcOffset, dstOffset, len, Long.BYTES)) { + System.arraycopy( + (long[]) src, + toIntIndex(srcOffset / Long.BYTES), + (long[]) dst, + toIntIndex(dstOffset / Long.BYTES), + len / Long.BYTES); + return true; + } else if (src instanceof float[] && aligned(srcOffset, dstOffset, len, Float.BYTES)) { + System.arraycopy( + (float[]) src, + toIntIndex(srcOffset / Float.BYTES), + (float[]) dst, + toIntIndex(dstOffset / Float.BYTES), + len / Float.BYTES); + return true; + } else if (src instanceof double[] && aligned(srcOffset, dstOffset, len, Double.BYTES)) { + System.arraycopy( + (double[]) src, + toIntIndex(srcOffset / Double.BYTES), + (double[]) dst, + toIntIndex(dstOffset / Double.BYTES), + len / Double.BYTES); + return true; + } + return false; + } + + public static Object[] copyObjectArray(Object[] arr) { + Object[] objects = new Object[arr.length]; + System.arraycopy(arr, 0, objects, 0, arr.length); + return objects; + } + /** Create an instance of type. This method does not call constructor. */ public static T newInstance(Class type) { throw new UnsupportedOperationException( - "Constructor-bypassing allocation is unsupported on JDK25 without sun.misc.Unsafe"); + "Constructor-bypassing allocation is unsupported on JDK25 without sun.misc.Unsafe; " + + "use a constructor-based serializer path for " + + type); } private static int getIntFromArray(Object object, long offset) { @@ -386,6 +460,10 @@ private static boolean isPrimitiveArray(Object object) { return cls.isArray() && cls.getComponentType().isPrimitive(); } + private static boolean aligned(long srcOffset, long dstOffset, int len, int width) { + return srcOffset % width == 0 && dstOffset % width == 0 && len % width == 0; + } + private static int toIntIndex(long offset) { if (offset < 0 || offset > Integer.MAX_VALUE) { throw new IndexOutOfBoundsException("offset out of int range: " + offset); 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 index 7bdb2ac158..306b782642 100644 --- 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 @@ -21,7 +21,9 @@ 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; @@ -30,9 +32,14 @@ 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; @@ -60,6 +67,13 @@ public class _JDKAccess { 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; @@ -68,9 +82,10 @@ public class _JDKAccess { 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; - private static final long STRING_VALUE_FIELD_OFFSET = -1; - private static final long STRING_COUNT_FIELD_OFFSET = -1; - private static final long STRING_OFFSET_FIELD_OFFSET = -1; + 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; @@ -80,6 +95,11 @@ public class _JDKAccess { 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", ""); @@ -103,17 +123,43 @@ public class _JDKAccess { } else { STRING_HAS_COUNT_OFFSET = false; } - FieldHandles handles = initFieldHandles(valueField.getType(), countField, offsetField); - JDK_INTERNAL_FIELD_ACCESS = handles != null; - STRING_VALUE_HANDLE = handles == null ? null : handles.stringValue; - STRING_CODER_HANDLE = handles == null ? null : handles.stringCoder; - STRING_COUNT_HANDLE = handles == null ? null : handles.stringCount; - STRING_OFFSET_HANDLE = handles == null ? null : handles.stringOffset; - BAS_BUF_HANDLE = handles == null ? null : handles.basBuf; - BAS_COUNT_HANDLE = handles == null ? null : handles.basCount; - BIS_BUF_HANDLE = handles == null ? null : handles.bisBuf; - BIS_POS_HANDLE = handles == null ? null : handles.bisPos; - BIS_COUNT_HANDLE = handles == null ? null : handles.bisCount; + + 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); } @@ -127,30 +173,31 @@ private static Field getStringFieldNullable(String fieldName) { } } - private static FieldHandles initFieldHandles( + private static StringHandles initStringHandles( Class stringValueType, Field countField, Field offsetField) { try { Lookup stringLookup = MethodHandles.privateLookupIn(String.class, MethodHandles.lookup()); - VarHandle stringValue = stringLookup.findVarHandle(String.class, "value", stringValueType); - VarHandle stringCoder = + return new StringHandles( + stringLookup.findVarHandle(String.class, "value", stringValueType), STRING_VALUE_FIELD_IS_BYTES ? stringLookup.findVarHandle(String.class, "coder", byte.class) - : null; - VarHandle stringCount = - countField == null ? null : stringLookup.findVarHandle(String.class, "count", int.class); - VarHandle stringOffset = + : null, + countField == null ? null : stringLookup.findVarHandle(String.class, "count", int.class), offsetField == null ? null - : stringLookup.findVarHandle(String.class, "offset", int.class); + : 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 FieldHandles( - stringValue, - stringCoder, - stringCount, - stringOffset, + return new StreamHandles( basLookup.findVarHandle(ByteArrayOutputStream.class, "buf", byte[].class), basLookup.findVarHandle(ByteArrayOutputStream.class, "count", int.class), bisLookup.findVarHandle(ByteArrayInputStream.class, "buf", byte[].class), @@ -161,31 +208,65 @@ private static FieldHandles initFieldHandles( } } - private static class FieldHandles { - private final VarHandle stringValue; - private final VarHandle stringCoder; - private final VarHandle stringCount; - private final VarHandle stringOffset; + 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 FieldHandles( - VarHandle stringValue, - VarHandle stringCoder, - VarHandle stringCount, - VarHandle stringOffset, + private StreamHandles( VarHandle basBuf, VarHandle basCount, VarHandle bisBuf, VarHandle bisPos, VarHandle bisCount) { - this.stringValue = stringValue; - this.stringCoder = stringCoder; - this.stringCount = stringCount; - this.stringOffset = stringOffset; this.basBuf = basBuf; this.basCount = basCount; this.bisBuf = bisBuf; @@ -194,13 +275,40 @@ private FieldHandles( } } + 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) { - checkInternalFieldAccess("String.value"); + checkStringAccess("String.value"); return STRING_VALUE_HANDLE.get(value); } public static byte getStringCoder(String value) { - checkInternalFieldAccess("String.coder"); + checkStringAccess("String.coder"); if (STRING_CODER_HANDLE == null) { throw new UnsupportedOperationException("String.coder is not available on this JDK"); } @@ -208,7 +316,7 @@ public static byte getStringCoder(String value) { } public static int getStringOffset(String value) { - checkInternalFieldAccess("String.offset"); + checkStringAccess("String.offset"); if (STRING_OFFSET_HANDLE == null) { throw new UnsupportedOperationException("String.offset is not available on this JDK"); } @@ -216,7 +324,7 @@ public static int getStringOffset(String value) { } public static int getStringCount(String value) { - checkInternalFieldAccess("String.count"); + checkStringAccess("String.count"); if (STRING_COUNT_HANDLE == null) { throw new UnsupportedOperationException("String.count is not available on this JDK"); } @@ -233,9 +341,181 @@ public static Lookup _trustedLookup(Class objectClass) { return lookupCache.get(objectClass, () -> _Lookup._trustedLookup(objectClass)); } + 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); - checkInternalFieldAccess("ByteArrayOutputStream"); + checkByteArrayStreamAccess("ByteArrayOutputStream"); byte[] buf = (byte[]) BAS_BUF_HANDLE.get(stream); int count = (int) BAS_COUNT_HANDLE.get(stream); buffer.pointTo(buf, 0, buf.length); @@ -244,7 +524,7 @@ public static void wrap(ByteArrayOutputStream stream, MemoryBuffer buffer) { public static void wrap(MemoryBuffer buffer, ByteArrayOutputStream stream) { Preconditions.checkNotNull(stream); - checkInternalFieldAccess("ByteArrayOutputStream"); + checkByteArrayStreamAccess("ByteArrayOutputStream"); byte[] bytes = buffer.getHeapMemory(); Preconditions.checkNotNull(bytes); BAS_BUF_HANDLE.set(stream, bytes); @@ -253,7 +533,7 @@ public static void wrap(MemoryBuffer buffer, ByteArrayOutputStream stream) { public static void wrap(ByteArrayInputStream stream, MemoryBuffer buffer) { Preconditions.checkNotNull(stream); - checkInternalFieldAccess("ByteArrayInputStream"); + 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); @@ -261,12 +541,57 @@ public static void wrap(ByteArrayInputStream stream, MemoryBuffer buffer) { buffer.readerIndex(pos); } - private static void checkInternalFieldAccess(String target) { - if (!JDK_INTERNAL_FIELD_ACCESS) { + 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.lang and java.base/java.io " - + "to org.apache.fory.core"); + + " private access is unavailable; open java.base/java.io to " + + "org.apache.fory.core,org.apache.fory.format"); } } @@ -451,6 +776,8 @@ public static Object makeGetterFunction( 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) { @@ -458,6 +785,90 @@ public static Object makeGetterFunction( } } + 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(); @@ -467,4 +878,8 @@ 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 index 0d3a669321..b7a0b8272f 100644 --- 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 @@ -35,6 +35,11 @@ public static Lookup _trustedLookup(Class objectClass) { 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); @@ -64,6 +69,6 @@ private static String privateAccessMessage(Class targetClass) { + packageName + " in module " + module.getName() - + " to be open to org.apache.fory.core"; + + " 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 index 3f0e9a2ff7..82c8cc21e9 100644 --- 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 @@ -35,6 +35,7 @@ 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; @@ -46,8 +47,21 @@ /** 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); @@ -57,6 +71,29 @@ 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); @@ -65,6 +102,41 @@ 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; } @@ -133,11 +205,11 @@ public void putDouble(Object targetObject, double value) { set(targetObject, value); } - public void putObject(Object targetObject, Object object) { + public final void putObject(Object targetObject, Object object) { set(targetObject, object); } - public Object getObject(Object targetObject) { + public final Object getObject(Object targetObject) { return get(targetObject); } @@ -179,9 +251,13 @@ public static FieldAccessor createAccessor(Field field) { // generated accessors, or primitive-specific reflection subclasses. return new ReflectionFieldAccessor(field); } - if (GraalvmSupport.isGraalBuildTime()) { + 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); } @@ -240,13 +316,16 @@ private static FieldAccessor createRecordAccessor(Field field) { } private static VarHandle fieldHandle(Field field) { - MethodHandles.Lookup lookup = privateLookup(field); try { - if (Modifier.isStatic(field.getModifiers())) { - return lookup.findStaticVarHandle( - field.getDeclaringClass(), field.getName(), field.getType()); + 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 lookup.findVarHandle(field.getDeclaringClass(), field.getName(), field.getType()); + return findFieldHandle(privateLookup(field), field); } catch (IllegalAccessException e) { throw accessFailure(field, e); } catch (NoSuchFieldException e) { @@ -255,10 +334,15 @@ private static VarHandle fieldHandle(Field field) { } private static MethodHandle recordGetter(Field field) { - MethodHandles.Lookup lookup = privateLookup(field); try { - return lookup.findVirtual( - field.getDeclaringClass(), field.getName(), MethodType.methodType(field.getType())); + 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) { @@ -266,13 +350,29 @@ private static MethodHandle recordGetter(Field field) { } } + 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(); - try { - return MethodHandles.privateLookupIn(declaringClass, MethodHandles.lookup()); - } catch (IllegalAccessException e) { - throw accessFailure(field, e); - } + 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) { @@ -280,6 +380,8 @@ private static IllegalStateException accessFailure(Field field, Throwable cause) 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 @@ -288,7 +390,10 @@ private static IllegalStateException accessFailure(Field field, Throwable cause) + " in module " + moduleName(targetModule) + " is not open to " - + moduleName(FieldAccessor.class.getModule()), + + moduleName(FieldAccessor.class.getModule()) + + ". For named modules, open the package with --add-opens=" + + openTarget + + "=org.apache.fory.core,org.apache.fory.format", cause); } @@ -351,6 +456,98 @@ private abstract static class VarHandleAccessor extends FieldAccessor { handle = fieldHandle(field); isStatic = Modifier.isStatic(field.getModifiers()); } + + protected void setReflectively(Object obj, Object value, Throwable cause) { + try { + prepareReflectiveWrite(cause); + field.set(target(obj), value); + } catch (IllegalAccessException | RuntimeException e) { + throw unsupportedWrite(field, e); + } + } + + private Object target(Object obj) { + return isStatic ? null : obj; + } + + private void prepareReflectiveWrite(Throwable cause) { + if (field.getDeclaringClass().getName().startsWith("java.")) { + throw unsupportedWrite(field, cause); + } + field.setAccessible(true); + } + + protected void setBooleanReflectively(Object obj, boolean value, Throwable cause) { + try { + prepareReflectiveWrite(cause); + field.setBoolean(target(obj), value); + } catch (IllegalAccessException | RuntimeException e) { + throw unsupportedWrite(field, e); + } + } + + protected void setByteReflectively(Object obj, byte value, Throwable cause) { + try { + prepareReflectiveWrite(cause); + field.setByte(target(obj), value); + } catch (IllegalAccessException | RuntimeException e) { + throw unsupportedWrite(field, e); + } + } + + protected void setCharReflectively(Object obj, char value, Throwable cause) { + try { + prepareReflectiveWrite(cause); + field.setChar(target(obj), value); + } catch (IllegalAccessException | RuntimeException e) { + throw unsupportedWrite(field, e); + } + } + + protected void setShortReflectively(Object obj, short value, Throwable cause) { + try { + prepareReflectiveWrite(cause); + field.setShort(target(obj), value); + } catch (IllegalAccessException | RuntimeException e) { + throw unsupportedWrite(field, e); + } + } + + protected void setIntReflectively(Object obj, int value, Throwable cause) { + try { + prepareReflectiveWrite(cause); + field.setInt(target(obj), value); + } catch (IllegalAccessException | RuntimeException e) { + throw unsupportedWrite(field, e); + } + } + + protected void setLongReflectively(Object obj, long value, Throwable cause) { + try { + prepareReflectiveWrite(cause); + field.setLong(target(obj), value); + } catch (IllegalAccessException | RuntimeException e) { + throw unsupportedWrite(field, e); + } + } + + protected void setFloatReflectively(Object obj, float value, Throwable cause) { + try { + prepareReflectiveWrite(cause); + field.setFloat(target(obj), value); + } catch (IllegalAccessException | RuntimeException e) { + throw unsupportedWrite(field, e); + } + } + + protected void setDoubleReflectively(Object obj, double value, Throwable cause) { + try { + prepareReflectiveWrite(cause); + field.setDouble(target(obj), value); + } catch (IllegalAccessException | RuntimeException e) { + throw unsupportedWrite(field, e); + } + } } /** Primitive boolean accessor. */ @@ -389,7 +586,7 @@ public void putBoolean(Object obj, boolean value) { handle.set(obj, value); } } catch (UnsupportedOperationException e) { - throw unsupportedWrite(field, e); + setBooleanReflectively(obj, value, e); } } } @@ -467,7 +664,7 @@ public void putByte(Object obj, byte value) { handle.set(obj, value); } } catch (UnsupportedOperationException e) { - throw unsupportedWrite(field, e); + setByteReflectively(obj, value, e); } } } @@ -546,7 +743,7 @@ public void putChar(Object obj, char value) { handle.set(obj, value); } } catch (UnsupportedOperationException e) { - throw unsupportedWrite(field, e); + setCharReflectively(obj, value, e); } } } @@ -624,7 +821,7 @@ public void putShort(Object obj, short value) { handle.set(obj, value); } } catch (UnsupportedOperationException e) { - throw unsupportedWrite(field, e); + setShortReflectively(obj, value, e); } } } @@ -702,7 +899,7 @@ public void putInt(Object obj, int value) { handle.set(obj, value); } } catch (UnsupportedOperationException e) { - throw unsupportedWrite(field, e); + setIntReflectively(obj, value, e); } } } @@ -780,7 +977,7 @@ public void putLong(Object obj, long value) { handle.set(obj, value); } } catch (UnsupportedOperationException e) { - throw unsupportedWrite(field, e); + setLongReflectively(obj, value, e); } } } @@ -858,7 +1055,7 @@ public void putFloat(Object obj, float value) { handle.set(obj, value); } } catch (UnsupportedOperationException e) { - throw unsupportedWrite(field, e); + setFloatReflectively(obj, value, e); } } } @@ -936,7 +1133,7 @@ public void putDouble(Object obj, double value) { handle.set(obj, value); } } catch (UnsupportedOperationException e) { - throw unsupportedWrite(field, e); + setDoubleReflectively(obj, value, e); } } } @@ -1004,7 +1201,7 @@ public void set(Object obj, Object value) { handle.set(obj, value); } } catch (UnsupportedOperationException e) { - throw unsupportedWrite(field, e); + setReflectively(obj, value, e); } } } @@ -1100,7 +1297,7 @@ public void set(Object obj, Object value) { handle.set(obj, value); } } catch (UnsupportedOperationException e) { - throw unsupportedWrite(field, e); + setReflectively(obj, value, e); } catch (RuntimeException e) { throw accessorFailure(field, e); } @@ -1129,7 +1326,7 @@ public void putBoolean(Object obj, boolean value) { handle.set(obj, value); } } catch (UnsupportedOperationException e) { - throw unsupportedWrite(field, e); + setBooleanReflectively(obj, value, e); } catch (RuntimeException e) { throw accessorFailure(field, e); } @@ -1158,7 +1355,7 @@ public void putByte(Object obj, byte value) { handle.set(obj, value); } } catch (UnsupportedOperationException e) { - throw unsupportedWrite(field, e); + setByteReflectively(obj, value, e); } catch (RuntimeException e) { throw accessorFailure(field, e); } @@ -1187,7 +1384,7 @@ public void putChar(Object obj, char value) { handle.set(obj, value); } } catch (UnsupportedOperationException e) { - throw unsupportedWrite(field, e); + setCharReflectively(obj, value, e); } catch (RuntimeException e) { throw accessorFailure(field, e); } @@ -1216,7 +1413,7 @@ public void putShort(Object obj, short value) { handle.set(obj, value); } } catch (UnsupportedOperationException e) { - throw unsupportedWrite(field, e); + setShortReflectively(obj, value, e); } catch (RuntimeException e) { throw accessorFailure(field, e); } @@ -1245,7 +1442,7 @@ public void putInt(Object obj, int value) { handle.set(obj, value); } } catch (UnsupportedOperationException e) { - throw unsupportedWrite(field, e); + setIntReflectively(obj, value, e); } catch (RuntimeException e) { throw accessorFailure(field, e); } @@ -1274,7 +1471,7 @@ public void putLong(Object obj, long value) { handle.set(obj, value); } } catch (UnsupportedOperationException e) { - throw unsupportedWrite(field, e); + setLongReflectively(obj, value, e); } catch (RuntimeException e) { throw accessorFailure(field, e); } @@ -1303,7 +1500,7 @@ public void putFloat(Object obj, float value) { handle.set(obj, value); } } catch (UnsupportedOperationException e) { - throw unsupportedWrite(field, e); + setFloatReflectively(obj, value, e); } catch (RuntimeException e) { throw accessorFailure(field, e); } @@ -1332,7 +1529,7 @@ public void putDouble(Object obj, double value) { handle.set(obj, value); } } catch (UnsupportedOperationException e) { - throw unsupportedWrite(field, e); + setDoubleReflectively(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/SerializedLambdaSerializer.java b/java/fory-core/src/main/java25/org/apache/fory/serializer/SerializedLambdaSerializer.java new file mode 100644 index 0000000000..a744b99fff --- /dev/null +++ b/java/fory-core/src/main/java25/org/apache/fory/serializer/SerializedLambdaSerializer.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.serializer; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.SerializedLambda; +import java.lang.reflect.Method; +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.platform.AndroidSupport; +import org.apache.fory.platform.internal._JDKAccess; +import org.apache.fory.resolver.TypeResolver; +import org.apache.fory.util.Preconditions; + +/** + * Serializer for {@link SerializedLambda}. It writes the JDK lambda payload through the public + * getter API, applies {@code readResolve} on read, and preserves unresolved {@code + * SerializedLambda} form on direct copy. + */ +@SuppressWarnings({"rawtypes", "unchecked"}) +public class SerializedLambdaSerializer extends Serializer { + static final Class SERIALIZED_LAMBDA = SerializedLambda.class; + private static final MethodHandle READ_RESOLVE_HANDLE; + private final TypeResolver typeResolver; + + static { + MethodHandle readResolveHandle = null; + if (AndroidSupport.IS_ANDROID) { + // Lambda serialization is unsupported on Android. + } else { + try { + Method readResolveMethod = JavaSerializer.getReadResolveMethod(SERIALIZED_LAMBDA); + Preconditions.checkNotNull( + readResolveMethod, "Missing readResolve for " + SERIALIZED_LAMBDA); + try { + readResolveHandle = + _JDKAccess._trustedLookup(SERIALIZED_LAMBDA).unreflect(readResolveMethod); + } catch (RuntimeException e) { + // JDK25 rejects privateLookupIn for java.lang.invoke.SerializedLambda itself. With + // java.lang.invoke opened, reflective access still exposes the JDK readResolve method + // without using Unsafe. + readResolveMethod.setAccessible(true); + readResolveHandle = java.lang.invoke.MethodHandles.lookup().unreflect(readResolveMethod); + } + } catch (IllegalAccessException | RuntimeException e) { + // Keep serializer registration available; readResolve reports the missing open if used. + } + } + READ_RESOLVE_HANDLE = readResolveHandle; + } + + public SerializedLambdaSerializer(TypeResolver typeResolver, Class cls) { + super(typeResolver.getConfig(), cls); + this.typeResolver = typeResolver; + Preconditions.checkArgument(cls == SERIALIZED_LAMBDA); + } + + @Override + public void write(WriteContext writeContext, Object value) { + throwIfAndroid(); + MemoryBuffer buffer = writeContext.getBuffer(); + SerializedLambda serializedLambda = (SerializedLambda) value; + writeContext.writeStringRef(serializedLambda.getCapturingClass()); + writeContext.writeStringRef(serializedLambda.getFunctionalInterfaceClass()); + writeContext.writeStringRef(serializedLambda.getFunctionalInterfaceMethodName()); + writeContext.writeStringRef(serializedLambda.getFunctionalInterfaceMethodSignature()); + writeContext.writeStringRef(serializedLambda.getImplClass()); + writeContext.writeStringRef(serializedLambda.getImplMethodName()); + writeContext.writeStringRef(serializedLambda.getImplMethodSignature()); + buffer.writeVarInt32(serializedLambda.getImplMethodKind()); + writeContext.writeStringRef(serializedLambda.getInstantiatedMethodType()); + int capturedArgCount = serializedLambda.getCapturedArgCount(); + buffer.writeVarUInt32Small7(capturedArgCount); + for (int i = 0; i < capturedArgCount; i++) { + writeContext.writeRef(serializedLambda.getCapturedArg(i)); + } + } + + @Override + public Object copy(CopyContext copyContext, Object value) { + throwIfAndroid(); + SerializedLambda serializedLambda = (SerializedLambda) value; + int capturedArgCount = serializedLambda.getCapturedArgCount(); + Object[] capturedArgs = new Object[capturedArgCount]; + for (int i = 0; i < capturedArgCount; i++) { + capturedArgs[i] = copyContext.copyObject(serializedLambda.getCapturedArg(i)); + } + return newSerializedLambda( + serializedLambda.getCapturingClass(), + serializedLambda.getFunctionalInterfaceClass(), + serializedLambda.getFunctionalInterfaceMethodName(), + serializedLambda.getFunctionalInterfaceMethodSignature(), + serializedLambda.getImplMethodKind(), + serializedLambda.getImplClass(), + serializedLambda.getImplMethodName(), + serializedLambda.getImplMethodSignature(), + serializedLambda.getInstantiatedMethodType(), + capturedArgs); + } + + @Override + public Object read(ReadContext readContext) { + throwIfAndroid(); + return readResolve(readUnresolved(readContext)); + } + + Object readUnresolved(ReadContext readContext) { + throwIfAndroid(); + MemoryBuffer buffer = readContext.getBuffer(); + String capturingClass = readContext.readStringRef(); + String functionalInterfaceClass = readContext.readStringRef(); + String functionalInterfaceMethodName = readContext.readStringRef(); + String functionalInterfaceMethodSignature = readContext.readStringRef(); + String implClass = readContext.readStringRef(); + String implMethodName = readContext.readStringRef(); + String implMethodSignature = readContext.readStringRef(); + int implMethodKind = buffer.readVarInt32(); + String instantiatedMethodType = readContext.readStringRef(); + int capturedArgCount = buffer.readVarUInt32Small7(); + Object[] capturedArgs = new Object[capturedArgCount]; + for (int i = 0; i < capturedArgCount; i++) { + capturedArgs[i] = readContext.readRef(); + } + return newSerializedLambda( + capturingClass, + functionalInterfaceClass, + functionalInterfaceMethodName, + functionalInterfaceMethodSignature, + implMethodKind, + implClass, + implMethodName, + implMethodSignature, + instantiatedMethodType, + capturedArgs); + } + + static Object readResolve(Object replacement) { + throwIfAndroid(); + if (READ_RESOLVE_HANDLE == null) { + throw new ForyException( + "SerializedLambda.readResolve is inaccessible. On JDK25+, deserialize lambdas only " + + "with --add-opens=java.base/java.lang.invoke=" + + "org.apache.fory.core,org.apache.fory.format."); + } + try { + return READ_RESOLVE_HANDLE.invoke(replacement); + } catch (Throwable e) { + throw new RuntimeException("Can't deserialize lambda", e); + } + } + + private static void throwIfAndroid() { + if (AndroidSupport.IS_ANDROID) { + throw new UnsupportedOperationException( + "Lambda serialization is unsupported on Android; serialize explicit data objects instead."); + } + } + + private SerializedLambda newSerializedLambda( + String capturingClass, + String functionalInterfaceClass, + String functionalInterfaceMethodName, + String functionalInterfaceMethodSignature, + int implMethodKind, + String implClass, + String implMethodName, + String implMethodSignature, + String instantiatedMethodType, + Object[] capturedArgs) { + return new SerializedLambda( + loadCapturingClass(capturingClass), + functionalInterfaceClass, + functionalInterfaceMethodName, + functionalInterfaceMethodSignature, + implMethodKind, + implClass, + implMethodName, + implMethodSignature, + instantiatedMethodType, + capturedArgs); + } + + private Class loadCapturingClass(String className) { + String binaryClassName = className.replace('/', '.'); + try { + return Class.forName(binaryClassName, false, typeResolver.getClassLoader()); + } catch (ClassNotFoundException e) { + try { + return Class.forName( + binaryClassName, false, Thread.currentThread().getContextClassLoader()); + } catch (ClassNotFoundException ex) { + throw new RuntimeException("Can't load capturing class " + binaryClassName, ex); + } + } + } +} diff --git a/java/fory-core/src/main/java25/org/apache/fory/serializer/Serializers.java b/java/fory-core/src/main/java25/org/apache/fory/serializer/Serializers.java new file mode 100644 index 0000000000..6e99a27848 --- /dev/null +++ b/java/fory-core/src/main/java25/org/apache/fory/serializer/Serializers.java @@ -0,0 +1,865 @@ +/* + * 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 static org.apache.fory.util.function.Functions.makeGetterFunction; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.math.BigDecimal; +import java.math.BigInteger; +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; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import java.util.function.ToIntFunction; +import java.util.regex.Pattern; +import org.apache.fory.Fory; +import org.apache.fory.collection.Cache; +import org.apache.fory.collection.CacheBuilder; +import org.apache.fory.collection.Tuple2; +import org.apache.fory.config.Config; +import org.apache.fory.context.CopyContext; +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; +import org.apache.fory.resolver.TypeResolver; +import org.apache.fory.serializer.CodegenSerializer.LazyInitBeanSerializer; +import org.apache.fory.serializer.collection.ChildContainerSerializers; +import org.apache.fory.serializer.collection.CollectionSerializer; +import org.apache.fory.serializer.collection.CollectionSerializers; +import org.apache.fory.serializer.collection.MapSerializer; +import org.apache.fory.serializer.collection.MapSerializers; +import org.apache.fory.serializer.scala.SingletonCollectionSerializer; +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; + +/** Serialization utils and common serializers. */ +@SuppressWarnings({"rawtypes", "unchecked"}) +public class Serializers { + // avoid duplicate reflect inspection and cache for graalvm support too. + private static final Cache> CTR_MAP; + + static { + if (GraalvmSupport.isGraalBuildTime()) { + CTR_MAP = CacheBuilder.newBuilder().concurrencyLevel(32).build(); + } else { + CTR_MAP = CacheBuilder.newBuilder().weakKeys().softValues().build(); + } + } + + private static final MethodType SIG1 = + MethodType.methodType(void.class, TypeResolver.class, Class.class); + private static final MethodType SIG2 = MethodType.methodType(void.class, TypeResolver.class); + private static final MethodType SIG3 = + MethodType.methodType(void.class, Config.class, Class.class); + private static final MethodType SIG4 = MethodType.methodType(void.class, Config.class); + private static final MethodType SIG5 = MethodType.methodType(void.class, Class.class); + private static final MethodType SIG6 = MethodType.methodType(void.class); + + /** + * Serializer subclass must have a constructor which take parameters of type {@link TypeResolver} + * and {@link Class}, or {@link TypeResolver}, or {@link Config} and {@link Class}, or {@link + * Config}, or {@link Class}, or no-arg constructor. + */ + public static Serializer newSerializer( + Fory fory, Class type, Class serializerClass) { + return newSerializer(fory.getTypeResolver(), type, serializerClass); + } + + /** + * Serializer subclass must have a constructor which take parameters of type {@link TypeResolver} + * and {@link Class}, or {@link TypeResolver}, or {@link Config} and {@link Class}, or {@link + * Config}, or {@link Class}, or no-arg constructor. + */ + public static Serializer newSerializer( + TypeResolver typeResolver, Class type, Class serializerClass) { + TypeInfo typeInfo = typeResolver.getTypeInfo(type, false); + Serializer serializer = typeInfo == null ? null : typeInfo.getSerializer(); + try { + return buildSerializer(typeResolver, type, serializerClass); + } catch (Throwable t) { + // Some serializer may set itself in constructor as serializer, but the + // constructor failed later. For example, some final type field doesn't + // support serialization. + typeResolver.resetSerializer(type, serializer); + if (t instanceof java.lang.reflect.InvocationTargetException && t.getCause() != null) { + ExceptionUtils.throwException(t.getCause()); + } + ExceptionUtils.throwException(t); + } + throw new IllegalStateException("unreachable"); + } + + private static Serializer buildSerializer( + TypeResolver typeResolver, Class type, Class serializerClass) { + try { + Config config = typeResolver.getConfig(); + Serializer serializer = + buildBuiltinSerializer(typeResolver, config, type, serializerClass); + if (serializer != null) { + return serializer; + } + Tuple2 ctrInfo = CTR_MAP.getIfPresent(serializerClass); + if (ctrInfo != null) { + MethodType sig = ctrInfo.f0; + MethodHandle handle = ctrInfo.f1; + if (sig.equals(SIG1)) { + return (Serializer) handle.invoke(typeResolver, type); + } else if (sig.equals(SIG2)) { + return (Serializer) handle.invoke(typeResolver); + } else if (sig.equals(SIG3)) { + return (Serializer) handle.invoke(config, type); + } else if (sig.equals(SIG4)) { + return (Serializer) handle.invoke(config); + } else if (sig.equals(SIG5)) { + return (Serializer) handle.invoke(type); + } else { + return (Serializer) handle.invoke(); + } + } + return createSerializer(typeResolver, type, serializerClass); + } catch (Throwable t) { + ExceptionUtils.throwException(t); + throw new IllegalStateException("unreachable"); + } + } + + private static Serializer buildBuiltinSerializer( + TypeResolver typeResolver, + Config config, + Class type, + Class serializerClass) { + if (serializerClass == ObjectSerializer.class) { + return new ObjectSerializer(typeResolver, type); + } + if (serializerClass == ArraySerializers.ObjectArraySerializer.class) { + return (Serializer) new ArraySerializers.ObjectArraySerializer(typeResolver, type); + } + if (serializerClass == ObjectStreamSerializer.class) { + return new ObjectStreamSerializer(typeResolver, type); + } + if (serializerClass == ExceptionSerializers.ExceptionSerializer.class) { + return new ExceptionSerializers.ExceptionSerializer(typeResolver, type); + } + if (serializerClass == ExceptionSerializers.StackTraceElementSerializer.class) { + return (Serializer) new ExceptionSerializers.StackTraceElementSerializer(config); + } + if (serializerClass == CompatibleSerializer.class) { + TypeDef typeDef = typeResolver.getTypeDef(type, true); + return new CompatibleSerializer(typeResolver, type, typeDef); + } + if (serializerClass == EnumSerializer.class) { + return (Serializer) new EnumSerializer(config, type); + } + if (serializerClass == LambdaSerializer.class) { + return new LambdaSerializer(typeResolver, type); + } + if (serializerClass == JdkProxySerializer.class) { + return new JdkProxySerializer(typeResolver, type); + } + if (serializerClass == ReplaceResolveSerializer.class) { + return new ReplaceResolveSerializer(typeResolver, type); + } + if (serializerClass == ExternalizableSerializer.class) { + return new ExternalizableSerializer(typeResolver, type); + } + if (serializerClass == LazyInitBeanSerializer.class) { + return new LazyInitBeanSerializer(typeResolver, type); + } + if (serializerClass == TimeSerializers.CalendarSerializer.class) { + return (Serializer) new TimeSerializers.CalendarSerializer(config, type); + } + if (serializerClass == TimeSerializers.ZoneIdSerializer.class) { + return (Serializer) new TimeSerializers.ZoneIdSerializer(config, type); + } + if (serializerClass == TimeSerializers.TimeZoneSerializer.class) { + return (Serializer) new TimeSerializers.TimeZoneSerializer(config, type); + } + if (serializerClass == BufferSerializers.ByteBufferSerializer.class) { + return (Serializer) new BufferSerializers.ByteBufferSerializer(typeResolver, type); + } + if (serializerClass == CharsetSerializer.class) { + return new CharsetSerializer(config, type); + } + if (serializerClass == CollectionSerializers.EnumSetSerializer.class) { + return (Serializer) new CollectionSerializers.EnumSetSerializer(typeResolver, type); + } + if (serializerClass == CollectionSerializer.class) { + return new CollectionSerializer(typeResolver, type); + } + if (serializerClass == CollectionSerializers.DefaultJavaCollectionSerializer.class) { + return new CollectionSerializers.DefaultJavaCollectionSerializer(typeResolver, type); + } + if (serializerClass == CollectionSerializers.JDKCompatibleCollectionSerializer.class) { + return new CollectionSerializers.JDKCompatibleCollectionSerializer(typeResolver, type); + } + if (serializerClass == MapSerializer.class) { + return new MapSerializer(typeResolver, type); + } + if (serializerClass == MapSerializers.DefaultJavaMapSerializer.class) { + return new MapSerializers.DefaultJavaMapSerializer(typeResolver, type); + } + if (serializerClass == MapSerializers.JDKCompatibleMapSerializer.class) { + return new MapSerializers.JDKCompatibleMapSerializer(typeResolver, type); + } + if (serializerClass == ChildContainerSerializers.ChildCollectionSerializer.class) { + return new ChildContainerSerializers.ChildCollectionSerializer(typeResolver, type); + } + if (serializerClass == ChildContainerSerializers.ChildArrayListSerializer.class) { + return new ChildContainerSerializers.ChildArrayListSerializer(typeResolver, type); + } + if (serializerClass == ChildContainerSerializers.ChildMapSerializer.class) { + return new ChildContainerSerializers.ChildMapSerializer(typeResolver, type); + } + if (serializerClass == ChildContainerSerializers.ChildSortedSetSerializer.class) { + return new ChildContainerSerializers.ChildSortedSetSerializer(typeResolver, type); + } + if (serializerClass == ChildContainerSerializers.ChildPriorityQueueSerializer.class) { + return new ChildContainerSerializers.ChildPriorityQueueSerializer(typeResolver, type); + } + if (serializerClass == ChildContainerSerializers.ChildSortedMapSerializer.class) { + return new ChildContainerSerializers.ChildSortedMapSerializer(typeResolver, type); + } + if (serializerClass == SingletonCollectionSerializer.class) { + return new SingletonCollectionSerializer(typeResolver, type); + } + if (serializerClass == SingletonMapSerializer.class) { + return new SingletonMapSerializer(typeResolver, type); + } + if (serializerClass == SingletonObjectSerializer.class) { + return new SingletonObjectSerializer(typeResolver, type); + } + return null; + } + + private static Serializer createSerializer( + TypeResolver typeResolver, Class type, Class serializerClass) { + if (AndroidSupport.IS_ANDROID) { + return createSerializerReflectively(typeResolver, type, serializerClass); + } + try { + Config config = typeResolver.getConfig(); + try { + MethodHandle ctr = findConstructor(serializerClass, SIG1); + CTR_MAP.put(serializerClass, Tuple2.of(SIG1, ctr)); + return (Serializer) ctr.invoke(typeResolver, type); + } catch (NoSuchMethodException e) { + ExceptionUtils.ignore(e); + } + try { + MethodHandle ctr = findConstructor(serializerClass, SIG2); + CTR_MAP.put(serializerClass, Tuple2.of(SIG2, ctr)); + return (Serializer) ctr.invoke(typeResolver); + } catch (NoSuchMethodException e) { + ExceptionUtils.ignore(e); + } + try { + MethodHandle ctr = findConstructor(serializerClass, SIG3); + CTR_MAP.put(serializerClass, Tuple2.of(SIG3, ctr)); + return (Serializer) ctr.invoke(config, type); + } catch (NoSuchMethodException e) { + ExceptionUtils.ignore(e); + } + try { + MethodHandle ctr = findConstructor(serializerClass, SIG4); + CTR_MAP.put(serializerClass, Tuple2.of(SIG4, ctr)); + return (Serializer) ctr.invoke(config); + } catch (NoSuchMethodException e) { + ExceptionUtils.ignore(e); + } + try { + MethodHandle ctr = findConstructor(serializerClass, SIG5); + CTR_MAP.put(serializerClass, Tuple2.of(SIG5, ctr)); + return (Serializer) ctr.invoke(type); + } catch (NoSuchMethodException e) { + ExceptionUtils.ignore(e); + } + 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"); + } + } + + private static MethodHandle findConstructor(Class cls, MethodType sig) + throws NoSuchMethodException, IllegalAccessException { + if (Modifier.isPublic(cls.getModifiers())) { + try { + return MethodHandles.publicLookup().findConstructor(cls, sig); + } catch (IllegalAccessException ignored) { + // The class may be public in a non-exported package. Fall back to the private lookup path so + // named-module users can still enable access with opens. + } + } + return _JDKAccess._trustedLookup(cls).findConstructor(cls, sig); + } + + private static Serializer createSerializerReflectively( + TypeResolver typeResolver, Class type, Class serializerClass) { + Config config = typeResolver.getConfig(); + try { + Constructor ctr = + serializerClass.getDeclaredConstructor(TypeResolver.class, Class.class); + ctr.setAccessible(true); + return (Serializer) ctr.newInstance(typeResolver, type); + } catch (NoSuchMethodException e) { + ExceptionUtils.ignore(e); + } catch (Throwable t) { + ExceptionUtils.throwException(t); + throw new IllegalStateException("unreachable"); + } + try { + Constructor ctr = + serializerClass.getDeclaredConstructor(TypeResolver.class); + ctr.setAccessible(true); + return (Serializer) ctr.newInstance(typeResolver); + } catch (NoSuchMethodException e) { + ExceptionUtils.ignore(e); + } catch (Throwable t) { + ExceptionUtils.throwException(t); + throw new IllegalStateException("unreachable"); + } + try { + Constructor ctr = + serializerClass.getDeclaredConstructor(Config.class, Class.class); + ctr.setAccessible(true); + return (Serializer) ctr.newInstance(config, type); + } catch (NoSuchMethodException e) { + ExceptionUtils.ignore(e); + } catch (Throwable t) { + ExceptionUtils.throwException(t); + throw new IllegalStateException("unreachable"); + } + try { + Constructor ctr = serializerClass.getDeclaredConstructor(Config.class); + ctr.setAccessible(true); + return (Serializer) ctr.newInstance(config); + } catch (NoSuchMethodException e) { + ExceptionUtils.ignore(e); + } catch (Throwable t) { + ExceptionUtils.throwException(t); + throw new IllegalStateException("unreachable"); + } + try { + Constructor ctr = serializerClass.getDeclaredConstructor(Class.class); + ctr.setAccessible(true); + return (Serializer) ctr.newInstance(type); + } catch (NoSuchMethodException e) { + ExceptionUtils.ignore(e); + } catch (Throwable t) { + ExceptionUtils.throwException(t); + throw new IllegalStateException("unreachable"); + } + try { + Constructor ctr = serializerClass.getDeclaredConstructor(); + ctr.setAccessible(true); + return (Serializer) ctr.newInstance(); + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException( + "Serializer " + + serializerClass.getName() + + " doesn't define a supported constructor for " + + type, + e); + } catch (Throwable t) { + ExceptionUtils.throwException(t); + throw new IllegalStateException("unreachable"); + } + } + + public static void write(WriteContext writeContext, Serializer serializer, T obj) { + serializer.write(writeContext, obj); + } + + public static T read(ReadContext readContext, Serializer serializer) { + return serializer.read(readContext); + } + + private static final ToIntFunction GET_CODER; + private static final Function GET_VALUE; + + static { + if (AndroidSupport.IS_ANDROID) { + GET_VALUE = null; + GET_CODER = null; + } else { + Function getValue; + ToIntFunction getCoder; + try { + getValue = (Function) makeGetterFunction(StringBuilder.class.getSuperclass(), "getValue"); + } catch (Throwable e) { + getValue = null; + } + try { + Method getCoderMethod = StringBuilder.class.getSuperclass().getDeclaredMethod("getCoder"); + getCoder = (ToIntFunction) makeGetterFunction(getCoderMethod, int.class); + } catch (NoSuchMethodException e) { + getCoder = null; + } catch (Throwable e) { + getCoder = null; + } + GET_VALUE = getValue; + GET_CODER = getCoder; + } + } + + public abstract static class AbstractStringBuilderSerializer + extends Serializer { + private final Config config; + + public AbstractStringBuilderSerializer(Config config, Class type) { + super(config, type); + this.config = config; + } + + @Override + public void write(WriteContext writeContext, T value) { + MemoryBuffer buffer = writeContext.getBuffer(); + StringSerializer stringSerializer = writeContext.getStringSerializer(); + if (config.isXlang()) { + stringSerializer.writeString(buffer, value.toString()); + return; + } + if (AndroidSupport.IS_ANDROID) { + stringSerializer.writeString(buffer, value.toString()); + return; + } + if (GET_VALUE == null) { + stringSerializer.writeString(buffer, value.toString()); + return; + } + if (GET_CODER != null) { + int coder = GET_CODER.applyAsInt(value); + byte[] v = (byte[]) GET_VALUE.apply(value); + int bytesLen = value.length(); + if (coder != 0) { + if (coder != 1) { + throw new UnsupportedOperationException("Unsupported coder " + coder); + } + bytesLen <<= 1; + } + long header = ((long) bytesLen << 2) | coder; + buffer.writeVarUInt64(header); + buffer.writeBytes(v, 0, bytesLen); + } else { + Object rawValue = GET_VALUE.apply(value); + if (!(rawValue instanceof char[])) { + stringSerializer.writeString(buffer, value.toString()); + return; + } + char[] v = (char[]) rawValue; + if (StringUtils.isLatin(v)) { + stringSerializer.writeCharsLatin1(buffer, v, value.length()); + } else { + stringSerializer.writeCharsUTF16(buffer, v, value.length()); + } + } + } + } + + public static final class StringBuilderSerializer + extends AbstractStringBuilderSerializer { + + public StringBuilderSerializer(Config config) { + super(config, StringBuilder.class); + } + + @Override + public StringBuilder copy(CopyContext copyContext, StringBuilder origin) { + return new StringBuilder(origin); + } + + @Override + public StringBuilder read(ReadContext readContext) { + return new StringBuilder(readContext.readString()); + } + } + + public static final class StringBufferSerializer + extends AbstractStringBuilderSerializer { + + public StringBufferSerializer(Config config) { + super(config, StringBuffer.class); + } + + @Override + public StringBuffer copy(CopyContext copyContext, StringBuffer origin) { + return new StringBuffer(origin); + } + + @Override + public StringBuffer read(ReadContext readContext) { + return new StringBuffer(readContext.readString()); + } + } + + 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 { + + public AtomicBooleanSerializer(Config config) { + super(config, AtomicBoolean.class); + } + + @Override + public void write(WriteContext writeContext, AtomicBoolean value) { + writeContext.getBuffer().writeBoolean(value.get()); + } + + @Override + public AtomicBoolean copy(CopyContext copyContext, AtomicBoolean origin) { + return new AtomicBoolean(origin.get()); + } + + @Override + public AtomicBoolean read(ReadContext readContext) { + return new AtomicBoolean(readContext.getBuffer().readBoolean()); + } + } + + public static final class AtomicIntegerSerializer extends Serializer + implements Shareable { + + public AtomicIntegerSerializer(Config config) { + super(config, AtomicInteger.class); + } + + @Override + public void write(WriteContext writeContext, AtomicInteger value) { + writeContext.getBuffer().writeInt32(value.get()); + } + + @Override + public AtomicInteger copy(CopyContext copyContext, AtomicInteger origin) { + return new AtomicInteger(origin.get()); + } + + @Override + public AtomicInteger read(ReadContext readContext) { + return new AtomicInteger(readContext.getBuffer().readInt32()); + } + } + + public static final class AtomicLongSerializer extends Serializer + implements Shareable { + + public AtomicLongSerializer(Config config) { + super(config, AtomicLong.class); + } + + @Override + public void write(WriteContext writeContext, AtomicLong value) { + writeContext.getBuffer().writeInt64(value.get()); + } + + @Override + public AtomicLong copy(CopyContext copyContext, AtomicLong origin) { + return new AtomicLong(origin.get()); + } + + @Override + public AtomicLong read(ReadContext readContext) { + return new AtomicLong(readContext.getBuffer().readInt64()); + } + } + + public static final class AtomicReferenceSerializer extends Serializer + implements Shareable { + + public AtomicReferenceSerializer(Config config) { + super(config, AtomicReference.class); + } + + @Override + public void write(WriteContext writeContext, AtomicReference value) { + writeContext.writeRef(value.get()); + } + + @Override + public AtomicReference copy(CopyContext copyContext, AtomicReference origin) { + return new AtomicReference(copyContext.copyObject(origin.get())); + } + + @Override + public AtomicReference read(ReadContext readContext) { + return new AtomicReference(readContext.readRef()); + } + } + + public static final class CurrencySerializer extends ImmutableSerializer + implements Shareable { + public CurrencySerializer(Config config) { + super(config, Currency.class); + } + + @Override + public void write(WriteContext writeContext, Currency object) { + writeContext.writeString(object.getCurrencyCode()); + } + + @Override + public Currency read(ReadContext readContext) { + return Currency.getInstance(readContext.readString()); + } + } + + /** Serializer for {@link Charset}. */ + public static final class CharsetSerializer extends ImmutableSerializer + implements Shareable { + public CharsetSerializer(Config config, Class type) { + super(config, type); + } + + public void write(WriteContext writeContext, T object) { + writeContext.writeString(object.name()); + } + + public T read(ReadContext readContext) { + return (T) Charset.forName(readContext.readString()); + } + } + + public static final class URISerializer extends ImmutableSerializer + implements Shareable { + + public URISerializer(Config config) { + super(config, URI.class); + } + + @Override + public void write(WriteContext writeContext, final URI uri) { + writeContext.writeString(uri.toString()); + } + + @Override + public URI read(ReadContext readContext) { + return URI.create(readContext.readString()); + } + } + + public static final class RegexSerializer extends ImmutableSerializer + implements Shareable { + public RegexSerializer(Config config) { + super(config, Pattern.class); + } + + @Override + public void write(WriteContext writeContext, Pattern pattern) { + MemoryBuffer buffer = writeContext.getBuffer(); + writeContext.writeString(pattern.pattern()); + buffer.writeInt32(pattern.flags()); + } + + @Override + public Pattern read(ReadContext readContext) { + MemoryBuffer buffer = readContext.getBuffer(); + String regex = readContext.readString(); + int flags = buffer.readInt32(); + return Pattern.compile(regex, flags); + } + } + + public static final class UUIDSerializer extends ImmutableSerializer implements Shareable { + + public UUIDSerializer(Config config) { + super(config, UUID.class); + } + + @Override + public void write(WriteContext writeContext, final UUID uuid) { + MemoryBuffer buffer = writeContext.getBuffer(); + buffer.writeInt64(uuid.getMostSignificantBits()); + buffer.writeInt64(uuid.getLeastSignificantBits()); + } + + @Override + public UUID read(ReadContext readContext) { + MemoryBuffer buffer = readContext.getBuffer(); + return new UUID(buffer.readInt64(), buffer.readInt64()); + } + } + + public static final class ClassSerializer extends ImmutableSerializer + implements Shareable { + public ClassSerializer(Config config) { + super(config, Class.class); + } + + @Override + public void write(WriteContext writeContext, Class value) { + ((ClassResolver) writeContext.getTypeResolver()).writeClassInternal(writeContext, value); + } + + @Override + public Class read(ReadContext readContext) { + return ((ClassResolver) readContext.getTypeResolver()).readClassInternal(readContext); + } + } + + /** + * Serializer for empty object of type {@link Object}. Fory disabled serialization for jdk + * internal types which doesn't implement {@link java.io.Serializable} for security, but empty + * object is safe and used sometimes, so fory should support its serialization without disable + * serializable or class registration checks. + */ + // Use a separate serializer to avoid codegen for empty object. + public static final class EmptyObjectSerializer extends ImmutableSerializer + implements Shareable { + + public EmptyObjectSerializer(Config config) { + super(config, Object.class); + } + + @Override + public void write(WriteContext writeContext, Object value) {} + + @Override + public Object read(ReadContext readContext) { + return new Object(); + } + } + + public static void registerDefaultSerializers(TypeResolver resolver) { + Config config = resolver.getConfig(); + 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)); + resolver.registerInternalSerializer(AtomicInteger.class, new AtomicIntegerSerializer(config)); + resolver.registerInternalSerializer(AtomicLong.class, new AtomicLongSerializer(config)); + resolver.registerInternalSerializer( + AtomicReference.class, new AtomicReferenceSerializer(config)); + resolver.registerInternalSerializer(Currency.class, new CurrencySerializer(config)); + resolver.registerInternalSerializer(URI.class, new URISerializer(config)); + resolver.registerInternalSerializer(Pattern.class, new RegexSerializer(config)); + resolver.registerInternalSerializer(UUID.class, new UUIDSerializer(config)); + resolver.registerInternalSerializer(Object.class, new EmptyObjectSerializer(config)); + } +} diff --git a/java/fory-core/src/main/java25/org/apache/fory/serializer/SlicedStringUtil.java b/java/fory-core/src/main/java25/org/apache/fory/serializer/SlicedStringUtil.java new file mode 100644 index 0000000000..924c16d1f6 --- /dev/null +++ b/java/fory-core/src/main/java25/org/apache/fory/serializer/SlicedStringUtil.java @@ -0,0 +1,285 @@ +/* + * 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) { + writeCharsUTF16BEToHeap(chars, offset, arrIndex, numBytes, targetArray); + } 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); + writeCharsUTF16BEToHeap(chars, offset, 0, numBytes, tmpArray); + 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/java25/org/apache/fory/serializer/StringSerializer.java b/java/fory-core/src/main/java25/org/apache/fory/serializer/StringSerializer.java new file mode 100644 index 0000000000..e290be4069 --- /dev/null +++ b/java/fory-core/src/main/java25/org/apache/fory/serializer/StringSerializer.java @@ -0,0 +1,1166 @@ +/* + * 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 static org.apache.fory.type.TypeUtils.STRING_TYPE; +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.nio.charset.StandardCharsets; +import java.util.Arrays; +import org.apache.fory.annotation.CodegenInvoke; +import org.apache.fory.codegen.Expression; +import org.apache.fory.codegen.Expression.Invoke; +import org.apache.fory.codegen.Expression.StaticInvoke; +import org.apache.fory.config.Config; +import org.apache.fory.context.ReadContext; +import org.apache.fory.context.WriteContext; +import org.apache.fory.memory.LittleEndian; +import org.apache.fory.memory.MemoryBuffer; +import org.apache.fory.memory.NativeByteOrder; +import org.apache.fory.platform.AndroidSupport; +import org.apache.fory.platform.UnsafeOps; +import org.apache.fory.platform.internal._JDKAccess; +import org.apache.fory.util.MathUtils; +import org.apache.fory.util.Preconditions; +import org.apache.fory.util.StringEncodingUtils; +import org.apache.fory.util.StringUtils; + +/** + * String serializer based on method handles 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 + * manually. + */ +@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 byte LATIN1 = 0; + private static final byte UTF16 = 1; + private static final byte UTF8 = 2; + private static final int DEFAULT_BUFFER_SIZE = 1024; + + private static final boolean STRING_HAS_COUNT_OFFSET; + + static { + if (!jdkInternalFieldAccess()) { + STRING_VALUE_FIELD_IS_CHARS = false; + STRING_VALUE_FIELD_IS_BYTES = false; + STRING_HAS_COUNT_OFFSET = false; + } else { + STRING_VALUE_FIELD_IS_CHARS = _JDKAccess.STRING_VALUE_FIELD_IS_CHARS; + STRING_VALUE_FIELD_IS_BYTES = _JDKAccess.STRING_VALUE_FIELD_IS_BYTES; + STRING_HAS_COUNT_OFFSET = _JDKAccess.STRING_HAS_COUNT_OFFSET; + } + } + + private static boolean jdkInternalFieldAccess() { + return !AndroidSupport.IS_ANDROID && _JDKAccess.JDK_STRING_FIELD_ACCESS; + } + + private final boolean compressString; + private final boolean writeNumUtf16BytesForUtf8Encoding; + private final boolean xlang; + + // set default length to 0, since char array and bytes array won't be used at the same time. + private static final byte[] EMPTY_BYTES_STUB = new byte[0]; + private static final char[] EMPTY_CHARS_STUB = new char[0]; + private byte[] byteArray = EMPTY_BYTES_STUB; + private int smoothByteArrayLength = DEFAULT_BUFFER_SIZE; + private char[] charArray = EMPTY_CHARS_STUB; + private int smoothCharArrayLength = DEFAULT_BUFFER_SIZE; + private byte[] byteArray2 = EMPTY_BYTES_STUB; + + public StringSerializer(Config config) { + super(config, String.class, config.trackingRef() && !config.isStringRefIgnored()); + compressString = config.compressString(); + xlang = config.isXlang(); + if (xlang) { + Preconditions.checkArgument(compressString, "compress string muse be enabled for xlang mode"); + } + writeNumUtf16BytesForUtf8Encoding = config.writeNumUtf16BytesForUtf8Encoding(); + } + + @Override + public void write(WriteContext writeContext, String value) { + writeString(writeContext.getBuffer(), value); + } + + @Override + public String read(ReadContext readContext) { + return readString(readContext.getBuffer()); + } + + 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); + } else { + return new StaticInvoke(StringSerializer.class, "writeBytesString", buffer, str); + } + } else { + if (!STRING_VALUE_FIELD_IS_CHARS) { + throw new UnsupportedOperationException(); + } + if (STRING_HAS_COUNT_OFFSET) { + if (compressString) { + return new Invoke(strSerializer, "writeCompressedCharsStringWithOffset", buffer, str); + } else { + return new Invoke(strSerializer, "writeCharsStringWithOffset", buffer, str); + } + } else { + if (compressString) { + return new Invoke(strSerializer, "writeCompressedCharsString", buffer, str); + } else { + return new Invoke(strSerializer, "writeCharsString", buffer, str); + } + } + } + } + + 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); + } else { + return new Invoke(strSerializer, "readBytesString", STRING_TYPE, buffer); + } + } else { + if (!STRING_VALUE_FIELD_IS_CHARS) { + throw new UnsupportedOperationException(); + } + if (compressString) { + return new Invoke(strSerializer, "readCompressedCharsString", STRING_TYPE, buffer); + } else { + return new Invoke(strSerializer, "readCharsString", STRING_TYPE, buffer); + } + } + } + + @CodegenInvoke + public String readBytesString(MemoryBuffer buffer) { + long header = buffer.readVarUint36Small(); + byte coder = (byte) (header & 0b11); + int numBytes = (int) (header >>> 2); + byte[] bytes; + if (!NativeByteOrder.IS_LITTLE_ENDIAN && coder == UTF16) { + bytes = readBytesUTF16BE(buffer, numBytes); + } else { + bytes = readBytesUnCompressedUTF16(buffer, numBytes); + } + if (coder != UTF8) { + return newBytesStringZeroCopy(coder, bytes); + } else { + return new String(bytes, 0, numBytes, StandardCharsets.UTF_8); + } + } + + @CodegenInvoke + public String readCharsString(MemoryBuffer buffer) { + long header = buffer.readVarUint36Small(); + byte coder = (byte) (header & 0b11); + int numBytes = (int) (header >>> 2); + char[] chars; + if (coder == LATIN1) { + chars = readCharsLatin1(buffer, numBytes); + } else if (coder == UTF16) { + chars = readCharsUTF16(buffer, numBytes); + } else { + throw new RuntimeException("Unknown coder type " + coder); + } + return newCharsStringZeroCopy(chars); + } + + @CodegenInvoke + public String readCompressedBytesString(MemoryBuffer buffer) { + long header = buffer.readVarUint36Small(); + byte coder = (byte) (header & 0b11); + int numBytes = (int) (header >>> 2); + if (coder == UTF8) { + byte[] data; + if (writeNumUtf16BytesForUtf8Encoding) { + data = readBytesUTF8PerfOptimized(buffer, numBytes); + } else { + if (xlang) { + return readBytesUTF8ForXlang(buffer, numBytes); + } + data = readBytesUTF8(buffer, numBytes); + } + return newBytesStringZeroCopy(UTF16, data); + } else if (coder == LATIN1) { + return newBytesStringZeroCopy(coder, readBytesUnCompressedUTF16(buffer, numBytes)); + } else if (coder == UTF16) { + byte[] bytes; + if (NativeByteOrder.IS_LITTLE_ENDIAN) { + bytes = readBytesUnCompressedUTF16(buffer, numBytes); + } else { + bytes = readBytesUTF16BE(buffer, numBytes); + } + return newBytesStringZeroCopy(coder, bytes); + } else { + throw new RuntimeException("Unknown coder type " + coder); + } + } + + // the utf8 data may can be encoded with latin1, so the read need to check whether it can be + // encoded by latin1, if true, the coder should be latin1 instead of utf16 + String readBytesUTF8ForXlang(MemoryBuffer buffer, int numBytes) { + buffer.checkReadableBytes(numBytes); + byte[] srcArray = buffer.getHeapMemory(); + + if (srcArray != null) { + int srcIndex = buffer._unsafeHeapReaderIndex(); + + // Fast path: vectorized ASCII check (8 bytes at a time) + if (StringEncodingUtils.isUTF8WithinAscii(srcArray, srcIndex, numBytes)) { + byte[] result = new byte[numBytes]; + System.arraycopy(srcArray, srcIndex, result, 0, numBytes); + buffer._increaseReaderIndexUnsafe(numBytes); + return newBytesStringZeroCopy(LATIN1, result); + } + + // Two-pass approach: scan first, then convert + boolean isLatin1 = StringEncodingUtils.isUTF8WithinLatin1(srcArray, srcIndex, numBytes); + buffer._increaseReaderIndexUnsafe(numBytes); + + if (isLatin1) { + byte[] latin1Buffer = getByteArray(numBytes); + int latin1Len = + StringEncodingUtils.convertUTF8ToLatin1(srcArray, srcIndex, numBytes, latin1Buffer); + return newBytesStringZeroCopy(LATIN1, Arrays.copyOf(latin1Buffer, latin1Len)); + } else { + byte[] utf16Buffer = getByteArray(numBytes << 1); + int utf16Len = + StringEncodingUtils.convertUTF8ToUTF16(srcArray, srcIndex, numBytes, utf16Buffer); + return newBytesStringZeroCopy(UTF16, Arrays.copyOf(utf16Buffer, utf16Len)); + } + } else { + // Off-heap path + byte[] srcBytes = getByteArray2(numBytes); + buffer.readBytes(srcBytes, 0, numBytes); + + // Fast path: vectorized ASCII check + if (StringEncodingUtils.isUTF8WithinAscii(srcBytes, 0, numBytes)) { + // Must copy to exact size since srcBytes is a reusable buffer + return newBytesStringZeroCopy(LATIN1, Arrays.copyOf(srcBytes, numBytes)); + } + + // Two-pass approach: scan first, then convert + boolean isLatin1 = StringEncodingUtils.isUTF8WithinLatin1(srcBytes, 0, numBytes); + + if (isLatin1) { + byte[] latin1Buffer = getByteArray(numBytes); + int latin1Len = + StringEncodingUtils.convertUTF8ToLatin1(srcBytes, 0, numBytes, latin1Buffer); + return newBytesStringZeroCopy(LATIN1, Arrays.copyOf(latin1Buffer, latin1Len)); + } else { + byte[] utf16Buffer = getByteArray(numBytes << 1); + int utf16Len = StringEncodingUtils.convertUTF8ToUTF16(srcBytes, 0, numBytes, utf16Buffer); + return newBytesStringZeroCopy(UTF16, Arrays.copyOf(utf16Buffer, utf16Len)); + } + } + } + + @CodegenInvoke + public String readCompressedCharsString(MemoryBuffer buffer) { + long header = buffer.readVarUint36Small(); + byte coder = (byte) (header & 0b11); + int numBytes = (int) (header >>> 2); + char[] chars; + if (coder == LATIN1) { + chars = readCharsLatin1(buffer, numBytes); + } else if (coder == UTF8) { + return writeNumUtf16BytesForUtf8Encoding + ? readCharsUTF8PerfOptimized(buffer, numBytes) + : readCharsUTF8(buffer, numBytes); + } else if (coder == UTF16) { + chars = readCharsUTF16(buffer, numBytes); + } else { + throw new RuntimeException("Unknown coder type " + coder); + } + return newCharsStringZeroCopy(chars); + } + + // Invoked by fory JIT + public void writeString(MemoryBuffer buffer, String value) { + if (!jdkInternalFieldAccess()) { + writeStringSlow(buffer, value); + return; + } + if (STRING_VALUE_FIELD_IS_BYTES) { + if (compressString) { + writeCompressedBytesString(buffer, value); + } else { + writeBytesString(buffer, value); + } + } else { + writeJava8String(buffer, value); + } + } + + private void writeJava8String(MemoryBuffer buffer, String value) { + assert STRING_VALUE_FIELD_IS_CHARS; + if (STRING_HAS_COUNT_OFFSET) { + if (compressString) { + writeCompressedCharsStringWithOffset(buffer, value); + } else { + writeCharsStringWithOffset(buffer, value); + } + } else { + if (compressString) { + writeCompressedCharsString(buffer, value); + } else { + writeCharsString(buffer, value); + } + } + } + + // Invoked by fory JIT + public String readString(MemoryBuffer buffer) { + if (!jdkInternalFieldAccess()) { + return readStringSlow(buffer); + } + if (STRING_VALUE_FIELD_IS_BYTES) { + if (compressString) { + return readCompressedBytesString(buffer); + } else { + return readBytesString(buffer); + } + } else { + assert STRING_VALUE_FIELD_IS_CHARS; + if (compressString) { + return readCompressedCharsString(buffer); + } else { + return readCharsString(buffer); + } + } + } + + private void writeStringSlow(MemoryBuffer buffer, String value) { + char[] chars = value.toCharArray(); + if (isLatin(chars)) { + writeCharsLatin1(buffer, chars, chars.length); + return; + } + if (compressString) { + byte[] utf8Bytes = value.getBytes(StandardCharsets.UTF_8); + int utf16Bytes = chars.length << 1; + if (utf8Bytes.length < utf16Bytes) { + writeStringUtf8Slow(buffer, utf8Bytes, utf16Bytes); + return; + } + } + writeCharsUTF16(buffer, chars, chars.length); + } + + private String readStringSlow(MemoryBuffer buffer) { + long header = buffer.readVarUint36Small(); + byte coder = (byte) (header & 0b11); + int numBytes = (int) (header >>> 2); + if (coder == LATIN1) { + return new String(readBytesUnCompressedUTF16(buffer, numBytes), StandardCharsets.ISO_8859_1); + } else if (coder == UTF16) { + return new String(readCharsUTF16(buffer, numBytes)); + } else if (coder == UTF8) { + int utf8Bytes = writeNumUtf16BytesForUtf8Encoding ? buffer.readInt32() : numBytes; + return new String(buffer.readBytes(utf8Bytes), StandardCharsets.UTF_8); + } else { + throw new RuntimeException("Unknown coder type " + coder); + } + } + + private void writeStringUtf8Slow(MemoryBuffer buffer, byte[] utf8Bytes, int utf16Bytes) { + int headerLength = writeNumUtf16BytesForUtf8Encoding ? utf16Bytes : utf8Bytes.length; + writeVarUint36Small(buffer, ((long) headerLength << 2) | UTF8); + if (writeNumUtf16BytesForUtf8Encoding) { + buffer.writeInt32(utf8Bytes.length); + } + buffer.writeBytes(utf8Bytes); + } + + private static void writeVarUint36Small(MemoryBuffer buffer, long value) { + int writerIndex = buffer.writerIndex(); + buffer.ensure(writerIndex + 9); + writerIndex += buffer._unsafePutVarUint36Small(writerIndex, 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 _JDKAccess.getStringValue(value); + } + + private static byte getStringCoder(String value) { + return _JDKAccess.getStringCoder(value); + } + + private static int getStringOffset(String value) { + return _JDKAccess.getStringOffset(value); + } + + private static int getStringCount(String value) { + return _JDKAccess.getStringCount(value); + } + + @CodegenInvoke + public void writeCompressedBytesString(MemoryBuffer buffer, String value) { + final byte[] bytes = (byte[]) getStringValue(value); + final byte coder = getStringCoder(value); + if (coder == LATIN1 || bestCoder(bytes) == UTF16) { + writeBytesString(buffer, coder, bytes); + } else { + if (writeNumUtf16BytesForUtf8Encoding) { + writeBytesUTF8PerfOptimized(buffer, bytes); + } else { + writeBytesUTF8(buffer, bytes); + } + } + } + + @CodegenInvoke + public void writeCompressedCharsString(MemoryBuffer buffer, String value) { + final char[] chars = (char[]) getStringValue(value); + final byte coder = bestCoder(chars); + if (coder == LATIN1) { + writeCharsLatin1(buffer, chars, chars.length); + } else if (coder == UTF8) { + if (writeNumUtf16BytesForUtf8Encoding) { + writeCharsUTF8PerfOptimized(buffer, chars); + } else { + writeCharsUTF8(buffer, chars); + } + } else { + writeCharsUTF16(buffer, chars, chars.length); + } + } + + @CodegenInvoke + public void writeCompressedCharsStringWithOffset(MemoryBuffer buffer, String value) { + final char[] chars = (char[]) getStringValue(value); + final int offset = getStringOffset(value); + final int count = getStringCount(value); + final byte coder = SlicedStringUtil.bestCoder(chars, offset, count); + if (coder == LATIN1) { + SlicedStringUtil.writeCharsLatin1WithOffset(this, buffer, chars, offset, count); + } else if (coder == UTF8) { + if (writeNumUtf16BytesForUtf8Encoding) { + SlicedStringUtil.writeCharsUTF8PerfOptimizedWithOffset(this, buffer, chars, offset, count); + } else { + SlicedStringUtil.writeCharsUTF8WithOffset(this, buffer, chars, offset, count); + } + } else { + SlicedStringUtil.writeCharsUTF16WithOffset(this, buffer, chars, offset, count); + } + } + + @CodegenInvoke + public static void writeBytesString(MemoryBuffer buffer, String value) { + byte[] bytes = (byte[]) getStringValue(value); + byte coder = getStringCoder(value); + writeBytesString(buffer, coder, bytes); + } + + public static void writeBytesString(MemoryBuffer buffer, byte coder, byte[] bytes) { + if (!NativeByteOrder.IS_LITTLE_ENDIAN && coder == UTF16) { + writeBytesStringUTF16BE(buffer, bytes); + return; + } + int bytesLen = bytes.length; + long header = ((long) bytesLen << 2) | coder; + int writerIndex = buffer.writerIndex(); + // The `ensure` ensure next operations are safe without bound checks, + // and inner heap buffer doesn't change. + buffer.ensure(writerIndex + 9 + bytesLen); // 1 byte coder + varint max 8 bytes + final byte[] targetArray = buffer.getHeapMemory(); + if (targetArray != null) { + // Some JDK11 Unsafe.copyMemory will `copyMemoryChecks`, and + // jvm doesn't eliminate well in some jdk. + final int targetIndex = buffer._unsafeHeapWriterIndex(); + int arrIndex = targetIndex; + arrIndex += LittleEndian.putVarUint36Small(targetArray, arrIndex, header); + writerIndex += arrIndex - targetIndex; + System.arraycopy(bytes, 0, targetArray, arrIndex, bytesLen); + } else { + final int headerBytes = buffer._unsafePutVarUint36Small(writerIndex, header); + writerIndex += headerBytes; + buffer.put(writerIndex, bytes, 0, bytesLen); + } + writerIndex += bytesLen; + buffer._unsafeWriterIndex(writerIndex); + } + + @CodegenInvoke + public void writeCharsString(MemoryBuffer buffer, String value) { + final char[] chars = (char[]) getStringValue(value); + if (StringUtils.isLatin(chars)) { + writeCharsLatin1(buffer, chars, chars.length); + } else { + writeCharsUTF16(buffer, chars, chars.length); + } + } + + @CodegenInvoke + public void writeCharsStringWithOffset(MemoryBuffer buffer, String value) { + final char[] chars = (char[]) getStringValue(value); + final int offset = getStringOffset(value); + final int count = getStringCount(value); + if (SlicedStringUtil.isLatin(chars, offset, count)) { + SlicedStringUtil.writeCharsLatin1WithOffset(this, buffer, chars, offset, count); + } else { + SlicedStringUtil.writeCharsUTF16WithOffset(this, buffer, chars, offset, count); + } + } + + public char[] readCharsLatin1(MemoryBuffer buffer, int numBytes) { + buffer.checkReadableBytes(numBytes); + byte[] srcArray = buffer.getHeapMemory(); + char[] chars = new char[numBytes]; + if (srcArray != null) { + int srcIndex = buffer._unsafeHeapReaderIndex(); + for (int i = 0; i < numBytes; i++) { + chars[i] = (char) (srcArray[srcIndex++] & 0xff); + } + buffer._increaseReaderIndexUnsafe(numBytes); + } else { + byte[] tmpArray = getByteArray(numBytes); + buffer.readBytes(tmpArray, 0, numBytes); + for (int i = 0; i < numBytes; i++) { + chars[i] = (char) (tmpArray[i] & 0xff); + } + } + return chars; + } + + public byte[] readBytesUTF8(MemoryBuffer buffer, int numBytes) { + byte[] tmpArray = getByteArray(numBytes << 1); + buffer.checkReadableBytes(numBytes); + int utf16NumBytes; + byte[] srcArray = buffer.getHeapMemory(); + if (srcArray != null) { + int srcIndex = buffer._unsafeHeapReaderIndex(); + utf16NumBytes = + StringEncodingUtils.convertUTF8ToUTF16(srcArray, srcIndex, numBytes, tmpArray); + buffer._increaseReaderIndexUnsafe(numBytes); + } else { + byte[] byteArray2 = getByteArray2(numBytes); + buffer.readBytes(byteArray2, 0, numBytes); + utf16NumBytes = StringEncodingUtils.convertUTF8ToUTF16(byteArray2, 0, numBytes, tmpArray); + } + return Arrays.copyOf(tmpArray, utf16NumBytes); + } + + private byte[] readBytesUTF8PerfOptimized(MemoryBuffer buffer, int numBytes) { + int udf8Bytes = buffer.readInt32(); + byte[] bytes = new byte[numBytes]; + // noinspection Duplicates + buffer.checkReadableBytes(udf8Bytes); + byte[] srcArray = buffer.getHeapMemory(); + if (srcArray != null) { + int srcIndex = buffer._unsafeHeapReaderIndex(); + int readLen = StringEncodingUtils.convertUTF8ToUTF16(srcArray, srcIndex, udf8Bytes, bytes); + assert readLen == numBytes : "Decode UTF8 to UTF16 failed"; + buffer._increaseReaderIndexUnsafe(udf8Bytes); + } else { + byte[] tmpArray = getByteArray(udf8Bytes); + buffer.readBytes(tmpArray, 0, udf8Bytes); + int readLen = StringEncodingUtils.convertUTF8ToUTF16(tmpArray, 0, udf8Bytes, bytes); + assert readLen == numBytes : "Decode UTF8 to UTF16 failed"; + } + return bytes; + } + + public byte[] readBytesUnCompressedUTF16(MemoryBuffer buffer, int numBytes) { + buffer.checkReadableBytes(numBytes); + byte[] bytes; + byte[] heapMemory = buffer.getHeapMemory(); + if (heapMemory != null) { + final int arrIndex = buffer._unsafeHeapReaderIndex(); + buffer.increaseReaderIndex(numBytes); + bytes = new byte[numBytes]; + System.arraycopy(heapMemory, arrIndex, bytes, 0, numBytes); + } else { + bytes = buffer.readBytes(numBytes); + } + return bytes; + } + + public char[] readCharsUTF16(MemoryBuffer buffer, int numBytes) { + if (NativeByteOrder.IS_LITTLE_ENDIAN) { + char[] chars = new char[numBytes >> 1]; + // FIXME JDK11 utf16 string uses little-endian order. + buffer.readChars(chars, numBytes >> 1); + return chars; + } else { + return readCharsUTF16BE(buffer, numBytes); + } + } + + public String readCharsUTF8(MemoryBuffer buffer, int numBytes) { + char[] chars = getCharArray(numBytes); + int charsLen; + buffer.checkReadableBytes(numBytes); + byte[] srcArray = buffer.getHeapMemory(); + if (srcArray != null) { + int srcIndex = buffer._unsafeHeapReaderIndex(); + charsLen = StringEncodingUtils.convertUTF8ToUTF16(srcArray, srcIndex, numBytes, chars); + buffer._increaseReaderIndexUnsafe(numBytes); + } else { + byte[] tmpArray = getByteArray(numBytes); + buffer.readBytes(tmpArray, 0, numBytes); + charsLen = StringEncodingUtils.convertUTF8ToUTF16(tmpArray, 0, numBytes, chars); + } + return new String(chars, 0, charsLen); + } + + public String readCharsUTF8PerfOptimized(MemoryBuffer buffer, int numBytes) { + int udf16Chars = numBytes >> 1; + int udf8Bytes = buffer.readInt32(); + char[] chars = new char[udf16Chars]; + // noinspection Duplicates + buffer.checkReadableBytes(udf8Bytes); + byte[] srcArray = buffer.getHeapMemory(); + if (srcArray != null) { + int srcIndex = buffer._unsafeHeapReaderIndex(); + int readLen = StringEncodingUtils.convertUTF8ToUTF16(srcArray, srcIndex, udf8Bytes, chars); + assert readLen == udf16Chars : "Decode UTF8 to UTF16 failed"; + buffer._increaseReaderIndexUnsafe(udf8Bytes); + } else { + byte[] tmpArray = getByteArray(udf8Bytes); + buffer.readBytes(tmpArray, 0, udf8Bytes); + int readLen = StringEncodingUtils.convertUTF8ToUTF16(tmpArray, 0, udf8Bytes, chars); + assert readLen == udf16Chars : "Decode UTF8 to UTF16 failed"; + } + return newCharsStringZeroCopy(chars); + } + + public void writeCharsLatin1(MemoryBuffer buffer, char[] chars, int numBytes) { + int writerIndex = buffer.writerIndex(); + long header = ((long) numBytes << 2) | LATIN1; + buffer.ensure(writerIndex + 5 + numBytes); + 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 < numBytes; i++) { + targetArray[arrIndex + i] = (byte) chars[i]; + } + } else { + writerIndex += buffer._unsafePutVarUint36Small(writerIndex, header); + final byte[] tmpArray = getByteArray(numBytes); + for (int i = 0; i < numBytes; i++) { + tmpArray[i] = (byte) chars[i]; + } + buffer.put(writerIndex, tmpArray, 0, numBytes); + } + writerIndex += numBytes; + buffer._unsafeWriterIndex(writerIndex); + } + + public void writeCharsUTF16(MemoryBuffer buffer, char[] chars, int numChars) { + int numBytes = MathUtils.doubleExact(numChars); + 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) { + if (AndroidSupport.IS_ANDROID) { + writeCharsUTF16ToHeapSlow(chars, arrIndex, numBytes, targetArray); + } else { + writeCharsUTF16ToHeapSlow(chars, arrIndex, numBytes, targetArray); + } + } else { + writeCharsUTF16BEToHeap(chars, arrIndex, numBytes, targetArray); + } + } else { + writerIndex += buffer._unsafePutVarUint36Small(writerIndex, header); + if (NativeByteOrder.IS_LITTLE_ENDIAN) { + writerIndex = offHeapWriteCharsUTF16(buffer, chars, writerIndex, numBytes); + } else { + writerIndex = offHeapWriteCharsUTF16BE(buffer, chars, writerIndex, numBytes); + } + } + buffer._unsafeWriterIndex(writerIndex); + } + + public void writeCharsUTF8(MemoryBuffer buffer, char[] chars) { + int estimateMaxBytes = chars.length * 3; + // num bytes of utf8 should be smaller than utf16, otherwise we should + // utf16 instead. + // We can't use length in header since we don't know num chars in go/c++ + int approxNumBytes = (int) (chars.length * 1.5) + 1; + int writerIndex = buffer.writerIndex(); + // 9 for max bytes of header + buffer.ensure(writerIndex + 9 + estimateMaxBytes); + byte[] targetArray = buffer.getHeapMemory(); + if (targetArray != null) { + // noinspection Duplicates + int targetIndex = buffer._unsafeHeapWriterIndex(); + // keep this index in case actual num utf8 bytes need different bytes for header + int headerPos = targetIndex; + int arrIndex = targetIndex; + long header = ((long) approxNumBytes << 2) | UTF8; + int headerBytesWritten = LittleEndian.putVarUint36Small(targetArray, arrIndex, header); + arrIndex += headerBytesWritten; + writerIndex += headerBytesWritten; + // noinspection Duplicates + targetIndex = StringEncodingUtils.convertUTF16ToUTF8(chars, 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 { + // noinspection Duplicates + final byte[] tmpArray = getByteArray(estimateMaxBytes); + int written = StringEncodingUtils.convertUTF16ToUTF8(chars, tmpArray, 0); + long header = ((long) written << 2) | UTF8; + writerIndex += buffer._unsafePutVarUint36Small(writerIndex, header); + buffer.put(writerIndex, tmpArray, 0, written); + buffer._unsafeWriterIndex(writerIndex + written); + } + } + + public void writeCharsUTF8PerfOptimized(MemoryBuffer buffer, char[] chars) { + int estimateMaxBytes = chars.length * 3; + int numBytes = MathUtils.doubleExact(chars.length); + // noinspection Duplicates + 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, targetArray, arrIndex + 4); + int written = targetIndex - arrIndex - 4; + buffer._unsafePutInt32(writerIndex, written); + buffer._unsafeWriterIndex(writerIndex + 4 + written); + } else { + final byte[] tmpArray = getByteArray(estimateMaxBytes); + int written = StringEncodingUtils.convertUTF16ToUTF8(chars, tmpArray, 0); + writerIndex += buffer._unsafePutVarUint36Small(writerIndex, header); + buffer._unsafePutInt32(writerIndex, written); + writerIndex += 4; + buffer.put(writerIndex, tmpArray, 0, written); + buffer._unsafeWriterIndex(writerIndex + written); + } + } + + private 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 void writeBytesUTF8(MemoryBuffer buffer, byte[] bytes) { + int numBytes = bytes.length; + int estimateMaxBytes = bytes.length / 2 * 3; + int writerIndex = buffer.writerIndex(); + buffer.ensure(writerIndex + 9 + estimateMaxBytes); + byte[] targetArray = buffer.getHeapMemory(); + if (targetArray != null) { + // noinspection Duplicates + int targetIndex = buffer._unsafeHeapWriterIndex(); + // keep this index in case actual num utf8 bytes need different bytes for header + int headerPos = targetIndex; + int arrIndex = targetIndex; + long header = ((long) numBytes << 2) | UTF8; + int headerBytesWritten = LittleEndian.putVarUint36Small(targetArray, arrIndex, header); + arrIndex += headerBytesWritten; + writerIndex += arrIndex - targetIndex; + // noinspection Duplicates + targetIndex = StringEncodingUtils.convertUTF16ToUTF8(bytes, 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 { + // noinspection Duplicates + final byte[] tmpArray = getByteArray(estimateMaxBytes); + int written = StringEncodingUtils.convertUTF16ToUTF8(bytes, tmpArray, 0); + long header = ((long) written << 2) | UTF8; + writerIndex += buffer._unsafePutVarUint36Small(writerIndex, header); + buffer.put(writerIndex, tmpArray, 0, written); + buffer._unsafeWriterIndex(writerIndex + written); + } + } + + private void writeBytesUTF8PerfOptimized(MemoryBuffer buffer, byte[] bytes) { + int numBytes = bytes.length; + int estimateMaxBytes = bytes.length / 2 * 3; + 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(bytes, targetArray, arrIndex + 4); + int written = targetIndex - arrIndex - 4; + buffer._unsafePutInt32(writerIndex, written); + buffer._unsafeWriterIndex(writerIndex + 4 + written); + } else { + final byte[] tmpArray = getByteArray(estimateMaxBytes); + int written = StringEncodingUtils.convertUTF16ToUTF8(bytes, tmpArray, 0); + writerIndex += buffer._unsafePutVarUint36Small(writerIndex, header); + buffer._unsafePutInt32(writerIndex, written); + writerIndex += 4; + buffer.put(writerIndex, tmpArray, 0, written); + buffer._unsafeWriterIndex(writerIndex + written); + } + } + + public static String newCharsStringZeroCopy(char[] data) { + if (!jdkInternalFieldAccess()) { + return newCharsStringSlow(data); + } + return _JDKAccess.newCharsStringZeroCopy(data); + } + + private static String newCharsStringSlow(char[] data) { + return new String(data); + } + + // coder param first to make inline call args + // `(buffer.readByte(), buffer.readBytesWithSizeEmbedded())` work. + public static String newBytesStringZeroCopy(byte coder, byte[] data) { + if (!jdkInternalFieldAccess()) { + 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); + } + } + + private static void writeCharsUTF16BEToHeap( + char[] chars, int arrIndex, int numBytes, byte[] targetArray) { + // Write to heap memory then copy is 250% faster than unsafe write to direct memory. + int charIndex = 0; + 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 void writeCharsUTF16ToHeapSlow( + char[] chars, int arrIndex, int numBytes, byte[] targetArray) { + writeCharsUTF16BEToHeap(chars, arrIndex, numBytes, targetArray); + } + + private int offHeapWriteCharsUTF16( + MemoryBuffer buffer, char[] chars, int writerIndex, int numBytes) { + byte[] tmpArray = getByteArray(numBytes); + int charIndex = 0; + for (int i = 0; i < numBytes; i += 2) { + char c = chars[charIndex++]; + tmpArray[i] = (byte) (c >> StringUTF16.HI_BYTE_SHIFT); + tmpArray[i + 1] = (byte) (c >> StringUTF16.LO_BYTE_SHIFT); + } + buffer.put(writerIndex, tmpArray, 0, numBytes); + writerIndex += numBytes; + return writerIndex; + } + + private int offHeapWriteCharsUTF16BE( + MemoryBuffer buffer, char[] chars, int writerIndex, int numBytes) { + byte[] tmpArray = getByteArray(numBytes); + int charIndex = 0; + 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; + } + + private char[] readCharsUTF16BE(MemoryBuffer buffer, int numBytes) { + buffer.checkReadableBytes(numBytes); + final byte[] targetArray = buffer.getHeapMemory(); + char[] chars = new char[numBytes >> 1]; + if (targetArray != null) { + int charIndex = 0; + for (int i = buffer._unsafeHeapReaderIndex(), end = i + numBytes; i < end; i += 2) { + int lo = targetArray[i] & 0xff; + int hi = targetArray[i + 1] & 0xff; + chars[charIndex++] = (char) (lo | (hi << 8)); + } + buffer._increaseReaderIndexUnsafe(numBytes); + } else { + final byte[] tmpArray = getByteArray(numBytes); + buffer.readBytes(tmpArray, 0, numBytes); + int charIndex = 0; + for (int i = 0; i < numBytes; i += 2) { + int lo = tmpArray[i] & 0xff; + int hi = tmpArray[i + 1] & 0xff; + chars[charIndex++] = (char) (lo | (hi << 8)); + } + } + return chars; + } + + private byte[] readBytesUTF16BE(MemoryBuffer buffer, int numBytes) { + byte[] bytes = readBytesUnCompressedUTF16(buffer, numBytes); + swapUTF16BytesInPlace(bytes); + return bytes; + } + + private static void swapUTF16BytesInPlace(byte[] bytes) { + for (int i = 0; i < bytes.length; i += 2) { + byte tmp = bytes[i]; + bytes[i] = bytes[i + 1]; + bytes[i + 1] = tmp; + } + } + + private static void writeBytesStringUTF16BE(MemoryBuffer buffer, byte[] bytes) { + int bytesLen = bytes.length; + long header = ((long) bytesLen << 2) | UTF16; + int writerIndex = buffer.writerIndex(); + buffer.ensure(writerIndex + 9 + bytesLen); + 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; + for (int i = 0; i < bytesLen; i += 2) { + targetArray[arrIndex + i] = bytes[i + 1]; + targetArray[arrIndex + i + 1] = bytes[i]; + } + } else { + writerIndex += buffer._unsafePutVarUint36Small(writerIndex, header); + byte[] tmpArray = new byte[bytesLen]; + for (int i = 0; i < bytesLen; i += 2) { + tmpArray[i] = bytes[i + 1]; + tmpArray[i + 1] = bytes[i]; + } + buffer.put(writerIndex, tmpArray, 0, bytesLen); + } + buffer._unsafeWriterIndex(writerIndex + bytesLen); + } + + private static byte bestCoder(char[] chars) { + int numChars = chars.length; + // sample 64 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); + 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) { + if (chars[charOffset + i] < 0x80) { + latin1Count++; + asciiCount++; + } else if (chars[charOffset + i] <= 0xFF) { + latin1Count++; + } + } + } + } + + for (int i = vectorizedChars; i < sampleNum; i++) { + if (chars[i] < 0x80) { + latin1Count++; + asciiCount++; + } else if (chars[i] <= 0xFF) { + latin1Count++; + } + } + + if (latin1Count == numChars + || (latin1Count == sampleNum && StringUtils.isLatin(chars, sampleNum))) { + return LATIN1; + } else if (asciiCount >= sampleNum * 0.5) { + // ascii number > 50%, choose UTF-8 + return UTF8; + } else { + return UTF16; + } + } + + private static byte bestCoder(byte[] bytes) { + int numBytes = bytes.length; + // sample 64 chars + 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); + 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) { + asciiCount++; + } + } + } + } + for (int i = vectorizedBytes; vectorizedBytes < sampleNum; vectorizedBytes += 2) { + if (UnsafeOps.getChar(bytes, UnsafeOps.BYTE_ARRAY_OFFSET + i) < 0x80) { + asciiCount++; + } + } + // ascii number > 50%, choose UTF-8 + if (asciiCount >= sampleNum * 0.5) { + return UTF8; + } else { + return UTF16; + } + } + + private char[] getCharArray(int numElements) { + char[] charArray = this.charArray; + if (charArray.length < numElements) { + charArray = new char[numElements]; + this.charArray = charArray; + } + if (charArray.length > DEFAULT_BUFFER_SIZE) { + smoothCharArrayLength = + Math.max(((int) (smoothCharArrayLength * 0.9 + numElements * 0.1)), DEFAULT_BUFFER_SIZE); + if (smoothByteArrayLength <= DEFAULT_BUFFER_SIZE) { + this.charArray = new char[DEFAULT_BUFFER_SIZE]; + } + } + return charArray; + } + + byte[] getByteArray(int numElements) { + byte[] byteArray = this.byteArray; + if (byteArray.length < numElements) { + byteArray = new byte[numElements]; + this.byteArray = byteArray; + } + if (byteArray.length > DEFAULT_BUFFER_SIZE) { + smoothByteArrayLength = + Math.max(((int) (smoothByteArrayLength * 0.9 + numElements * 0.1)), DEFAULT_BUFFER_SIZE); + if (smoothByteArrayLength <= DEFAULT_BUFFER_SIZE) { + this.byteArray = new byte[DEFAULT_BUFFER_SIZE]; + } + } + return byteArray; + } + + private byte[] getByteArray2(int numElements) { + byte[] byteArray2 = this.byteArray2; + if (byteArray2.length < numElements) { + byteArray2 = new byte[numElements]; + this.byteArray = byteArray2; + } + if (byteArray2.length > DEFAULT_BUFFER_SIZE) { + smoothByteArrayLength = + Math.max(((int) (smoothByteArrayLength * 0.9 + numElements * 0.1)), DEFAULT_BUFFER_SIZE); + if (smoothByteArrayLength <= DEFAULT_BUFFER_SIZE) { + this.byteArray2 = new byte[DEFAULT_BUFFER_SIZE]; + } + } + return byteArray2; + } + + public void clearBuffer(int size) { + if (byteArray.length >= size) { + byteArray = EMPTY_BYTES_STUB; + } + if (byteArray2.length >= size) { + byteArray2 = EMPTY_BYTES_STUB; + } + if (charArray.length >= size) { + this.charArray = EMPTY_CHARS_STUB; + } + } +} 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 9ffc90564f..2995057ccd 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 @@ -251,6 +251,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 +332,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 +398,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 +459,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 +491,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,7 +534,8 @@ 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.platform.internal._JDKAccess$1,\ + 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,\ 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/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/memory/MemoryBufferTest.java b/java/fory-core/src/test/java/org/apache/fory/memory/MemoryBufferTest.java index 03f38a9a16..8eced95b1e 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 @@ -32,7 +32,9 @@ import java.nio.charset.StandardCharsets; import java.util.Random; 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 { @@ -84,7 +86,7 @@ public void testBufferWrite() { @Test public void testByteArrayStreamWrap() { - if (!MemoryUtils.JDK_INTERNAL_FIELD_ACCESS) { + if (!MemoryUtils.JDK_BYTE_ARRAY_STREAM_FIELD_ACCESS) { return; } ByteArrayOutputStream outputStream = new ByteArrayOutputStream(8); @@ -105,6 +107,13 @@ public void testByteArrayStreamWrap() { assertEquals(buffer.readByte(), (byte) 6); } + @Test + public void testFromDirectByteBufferRejectsHeapBuffer() { + assertThrows( + IllegalArgumentException.class, + () -> MemoryBuffer.fromDirectByteBuffer(ByteBuffer.allocate(8), 8, null)); + } + @Test public void testAndroidHeapMemoryBufferPaths() throws Exception { String javaBin = @@ -326,6 +335,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]; @@ -378,6 +398,48 @@ 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 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 6367a8625c..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; 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 68209013fb..c86faa6bf5 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; @@ -65,6 +66,34 @@ public void testGeneratedAccessor() throws Exception { 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())); + } + } + + private static boolean isHidden(Class cls) throws Exception { + return (Boolean) Class.class.getMethod("isHidden").invoke(cls); + } + @Test public void testAndroidReflectionFieldAccessorPaths() throws Exception { String javaBin = @@ -145,4 +174,10 @@ 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; + } } 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/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..8eb70dffa5 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,463 @@ 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; + } + + @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; 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/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/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-format/pom.xml b/java/fory-format/pom.xml index 01cdd773ec..15deca64b3 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 + + --add-opens=java.base/java.nio=ALL-UNNAMED + --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/vectorized/ArrowUtils.java b/java/fory-format/src/main/java/org/apache/fory/format/vectorized/ArrowUtils.java index 0d93894bf5..226117b7df 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,41 @@ 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; + } + } + } + cause = cause.getCause(); + } + return false; + } } 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-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..c51ebe940d 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -109,6 +109,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 + + + + + + + From b8ce0ab69b7e8f8fa3d567feb6da05426de2bf7b Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 24 May 2026 14:18:44 +0800 Subject: [PATCH 15/34] chore(java): fix JDK25 memory buffer license header --- .../java25/org/apache/fory/memory/MemoryBuffer.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) 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 index 82203215cf..30c5d7b0e5 100644 --- 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 @@ -7,13 +7,14 @@ * "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 + * 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. + * 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; From 287d5ef25aecbd16581658e2315a0b64285e7d31 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 24 May 2026 14:21:35 +0800 Subject: [PATCH 16/34] fix(java): keep string coder lookup Java 8 safe --- .../java/org/apache/fory/platform/internal/_JDKAccess.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 index e4c9d47ad9..88e8b2e9ae 100644 --- 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 @@ -167,14 +167,15 @@ private static class StringCoderField { } } - public static final long STRING_CODER_FIELD_OFFSET = StringCoderField.OFFSET; + 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, StringCoderField.OFFSET); + return UNSAFE.getByte(value, STRING_CODER_FIELD_OFFSET); } public static int getStringOffset(String value) { From e947b5cdb8b3d4d845f9395b83038340d1539c0a Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 24 May 2026 14:27:38 +0800 Subject: [PATCH 17/34] fix(ci): repair style and kotlin xlang peer jar --- .../java/org/apache/fory/context/MapRefReader.java | 12 ++++++------ kotlin/fory-kotlin-tests/pom.xml | 11 +++++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) 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 6613349580..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 @@ -132,6 +132,12 @@ public Object getReadRef(int id) { return readObjects.get(id); } + /** Returns the object resolved by the last ref header that pointed to an existing instance. */ + @Override + public Object getReadRef() { + return readObject; + } + private Object readRef(int id) { if (trackedRefIds.size == 0) { return readObjects.get(id); @@ -152,12 +158,6 @@ private boolean isTrackedRef(int id) { return false; } - /** Returns the object resolved by the last ref header that pointed to an existing instance. */ - @Override - public Object getReadRef() { - return readObject; - } - /** Stores {@code object} under an already reserved read ref id. */ @Override public void setReadRef(int id, Object object) { 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 + + + From e9303f8529d281a6c1ca0620208a081afcd352ce Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 24 May 2026 14:43:08 +0800 Subject: [PATCH 18/34] fix(ci): avoid android and graal field access failures --- .../org/apache/fory/collection/MapEntry.java | 2 -- .../org/apache/fory/memory/MemoryUtils.java | 29 ++++++++++++++----- .../java/org/apache/fory/type/BFloat16.java | 2 -- .../org/apache/fory/type/BFloat16Array.java | 2 -- .../java/org/apache/fory/type/Float16.java | 2 -- .../org/apache/fory/type/Float16Array.java | 2 -- .../org/apache/fory/type/unsigned/UInt16.java | 2 -- .../org/apache/fory/type/unsigned/UInt32.java | 2 -- .../org/apache/fory/type/unsigned/UInt64.java | 2 -- .../org/apache/fory/type/unsigned/UInt8.java | 2 -- java/pom.xml | 1 + 11 files changed, 23 insertions(+), 25 deletions(-) diff --git a/java/fory-core/src/main/java/org/apache/fory/collection/MapEntry.java b/java/fory-core/src/main/java/org/apache/fory/collection/MapEntry.java index 5ec07a7974..b0bc8c3337 100644 --- a/java/fory-core/src/main/java/org/apache/fory/collection/MapEntry.java +++ b/java/fory-core/src/main/java/org/apache/fory/collection/MapEntry.java @@ -19,7 +19,6 @@ package org.apache.fory.collection; -import java.beans.ConstructorProperties; import java.util.Map; import java.util.Objects; @@ -28,7 +27,6 @@ public class MapEntry implements Map.Entry { private final K key; private V value; - @ConstructorProperties({"key", "value"}) public MapEntry(K key, V value) { this.key = key; this.value = value; 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 81d35bb4d1..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,6 +23,7 @@ import java.io.ByteArrayOutputStream; import java.nio.ByteBuffer; import org.apache.fory.platform.AndroidSupport; +import org.apache.fory.platform.GraalvmSupport; import org.apache.fory.platform.internal._JDKAccess; /** Memory utils for fory. */ @@ -31,19 +32,33 @@ public class MemoryUtils { // 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 && _JDKAccess.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 && _JDKAccess.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 && _JDKAccess.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 && _JDKAccess.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 && _JDKAccess.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 && _JDKAccess.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 && _JDKAccess.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]); diff --git a/java/fory-core/src/main/java/org/apache/fory/type/BFloat16.java b/java/fory-core/src/main/java/org/apache/fory/type/BFloat16.java index b1e0f2b67e..fb0d905ffb 100644 --- a/java/fory-core/src/main/java/org/apache/fory/type/BFloat16.java +++ b/java/fory-core/src/main/java/org/apache/fory/type/BFloat16.java @@ -19,7 +19,6 @@ package org.apache.fory.type; -import java.beans.ConstructorProperties; import java.io.Serializable; /** @@ -70,7 +69,6 @@ public final class BFloat16 extends Number implements Comparable, Seri private final short bits; - @ConstructorProperties("bits") private BFloat16(short bits) { this.bits = bits; } 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 538b7095bb..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 @@ -19,7 +19,6 @@ package org.apache.fory.type; -import java.beans.ConstructorProperties; import java.io.Serializable; import java.util.Arrays; import java.util.Iterator; @@ -46,7 +45,6 @@ public BFloat16Array(BFloat16[] values) { } } - @ConstructorProperties("bits") private BFloat16Array(short[] bits) { this.bits = bits; } diff --git a/java/fory-core/src/main/java/org/apache/fory/type/Float16.java b/java/fory-core/src/main/java/org/apache/fory/type/Float16.java index 562df4d180..c76eaa362c 100644 --- a/java/fory-core/src/main/java/org/apache/fory/type/Float16.java +++ b/java/fory-core/src/main/java/org/apache/fory/type/Float16.java @@ -19,7 +19,6 @@ package org.apache.fory.type; -import java.beans.ConstructorProperties; import java.io.Serializable; public final class Float16 extends Number implements Comparable, Serializable { @@ -62,7 +61,6 @@ public final class Float16 extends Number implements Comparable, Serial private final short bits; - @ConstructorProperties("bits") private Float16(short bits) { this.bits = 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 f94e618f72..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 @@ -19,7 +19,6 @@ package org.apache.fory.type; -import java.beans.ConstructorProperties; import java.io.Serializable; import java.util.Arrays; import java.util.Iterator; @@ -46,7 +45,6 @@ public Float16Array(Float16[] values) { } } - @ConstructorProperties("bits") private Float16Array(short[] bits) { this.bits = bits; } diff --git a/java/fory-core/src/main/java/org/apache/fory/type/unsigned/UInt16.java b/java/fory-core/src/main/java/org/apache/fory/type/unsigned/UInt16.java index 320260816c..cd57b1dab1 100644 --- a/java/fory-core/src/main/java/org/apache/fory/type/unsigned/UInt16.java +++ b/java/fory-core/src/main/java/org/apache/fory/type/unsigned/UInt16.java @@ -19,7 +19,6 @@ package org.apache.fory.type.unsigned; -import java.beans.ConstructorProperties; import java.io.Serializable; /** @@ -40,7 +39,6 @@ public final class UInt16 implements Comparable, Serializable { private final short data; - @ConstructorProperties("data") public UInt16(short data) { this.data = data; } diff --git a/java/fory-core/src/main/java/org/apache/fory/type/unsigned/UInt32.java b/java/fory-core/src/main/java/org/apache/fory/type/unsigned/UInt32.java index 3f8052e69a..2a1a4999fd 100644 --- a/java/fory-core/src/main/java/org/apache/fory/type/unsigned/UInt32.java +++ b/java/fory-core/src/main/java/org/apache/fory/type/unsigned/UInt32.java @@ -19,7 +19,6 @@ package org.apache.fory.type.unsigned; -import java.beans.ConstructorProperties; import java.io.Serializable; /** @@ -38,7 +37,6 @@ public final class UInt32 implements Comparable, Serializable { private final int data; - @ConstructorProperties("data") public UInt32(int data) { this.data = data; } diff --git a/java/fory-core/src/main/java/org/apache/fory/type/unsigned/UInt64.java b/java/fory-core/src/main/java/org/apache/fory/type/unsigned/UInt64.java index 3caff91f83..d5c7267c9f 100644 --- a/java/fory-core/src/main/java/org/apache/fory/type/unsigned/UInt64.java +++ b/java/fory-core/src/main/java/org/apache/fory/type/unsigned/UInt64.java @@ -19,7 +19,6 @@ package org.apache.fory.type.unsigned; -import java.beans.ConstructorProperties; import java.io.Serializable; /** @@ -37,7 +36,6 @@ public final class UInt64 implements Comparable, Serializable { private final long data; - @ConstructorProperties("data") public UInt64(long data) { this.data = data; } diff --git a/java/fory-core/src/main/java/org/apache/fory/type/unsigned/UInt8.java b/java/fory-core/src/main/java/org/apache/fory/type/unsigned/UInt8.java index 70eb002787..b4183108f1 100644 --- a/java/fory-core/src/main/java/org/apache/fory/type/unsigned/UInt8.java +++ b/java/fory-core/src/main/java/org/apache/fory/type/unsigned/UInt8.java @@ -19,7 +19,6 @@ package org.apache.fory.type.unsigned; -import java.beans.ConstructorProperties; import java.io.Serializable; /** @@ -40,7 +39,6 @@ public final class UInt8 implements Comparable, Serializable { private final byte data; - @ConstructorProperties("data") public UInt8(byte data) { this.data = data; } diff --git a/java/pom.xml b/java/pom.xml index c51ebe940d..7884abb1bc 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -270,6 +270,7 @@ ${maven.compiler.source} ${maven.compiler.target} + true From c8b8ddacfce8db9da0af1158e2a8bb9f8c2f68c4 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 24 May 2026 14:48:27 +0800 Subject: [PATCH 19/34] style(ci): format java task helper --- ci/tasks/java.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ci/tasks/java.py b/ci/tasks/java.py index cf8bb10041..bed7db53fc 100644 --- a/ci/tasks/java.py +++ b/ci/tasks/java.py @@ -153,9 +153,7 @@ def install_jdk25_fory_artifacts(): ) 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" - ) + 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") From 999fd9214d41f899c3e70ff3ad6560a41b46f4ed Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 24 May 2026 14:56:01 +0800 Subject: [PATCH 20/34] fix(android): skip jdk string internals on android --- .../fory/platform/internal/_JDKAccess.java | 94 ++++++++++++------- 1 file changed, 58 insertions(+), 36 deletions(-) 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 index 88e8b2e9ae..7019213ed0 100644 --- 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 @@ -45,6 +45,7 @@ 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; @@ -88,27 +89,39 @@ public class _JDKAccess { throw new UnsupportedOperationException("Unsafe is not supported in this platform."); } UNSAFE = unsafe; - 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; - if (JdkVersion.MAJOR_VERSION >= 11) { + 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); - _INNER_UNSAFE = theInternalUnsafeField.get(null); - _INNER_UNSAFE_CLASS = _INNER_UNSAFE.getClass(); + innerUnsafe = theInternalUnsafeField.get(null); + innerUnsafeClass = innerUnsafe.getClass(); } catch (Exception e) { throw new RuntimeException(e); } - } else { - _INNER_UNSAFE_CLASS = null; - _INNER_UNSAFE = null; } + _INNER_UNSAFE = innerUnsafe; + _INNER_UNSAFE_CLASS = innerUnsafeClass; } private static final ClassValueCache lookupCache = ClassValueCache.newClassKeyCache(32); @@ -121,29 +134,38 @@ public class _JDKAccess { public static final long STRING_OFFSET_FIELD_OFFSET; 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; - 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; + 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); } - } catch (NoSuchFieldException e) { - throw new RuntimeException(e); } } From e56a62d27aba447b3c294292bc80a2116222c308 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 24 May 2026 15:07:22 +0800 Subject: [PATCH 21/34] fix(graalvm): use public string access in native images --- .../java/org/apache/fory/serializer/StringSerializer.java | 7 ++++++- .../org/apache/fory/serializer/StringSerializer.java | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) 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 8e9fa43965..078bc109bc 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 @@ -36,6 +36,7 @@ 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.UnsafeOps; import org.apache.fory.platform.internal._JDKAccess; import org.apache.fory.util.MathUtils; @@ -84,7 +85,11 @@ public final class StringSerializer extends ImmutableSerializer { } private static boolean jdkInternalFieldAccess() { - return !AndroidSupport.IS_ANDROID && _JDKAccess.JDK_STRING_FIELD_ACCESS; + // Native-image runtime string layout is not a HotSpot Unsafe-offset contract; use public + // string copies there even when the image build can see JDK private fields. + return !AndroidSupport.IS_ANDROID + && !GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE + && _JDKAccess.JDK_STRING_FIELD_ACCESS; } private final boolean compressString; diff --git a/java/fory-core/src/main/java25/org/apache/fory/serializer/StringSerializer.java b/java/fory-core/src/main/java25/org/apache/fory/serializer/StringSerializer.java index e290be4069..92cbb62154 100644 --- a/java/fory-core/src/main/java25/org/apache/fory/serializer/StringSerializer.java +++ b/java/fory-core/src/main/java25/org/apache/fory/serializer/StringSerializer.java @@ -36,6 +36,7 @@ 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.UnsafeOps; import org.apache.fory.platform.internal._JDKAccess; import org.apache.fory.util.MathUtils; @@ -75,7 +76,11 @@ public final class StringSerializer extends ImmutableSerializer { } private static boolean jdkInternalFieldAccess() { - return !AndroidSupport.IS_ANDROID && _JDKAccess.JDK_STRING_FIELD_ACCESS; + // Native-image runtime string layout is not a HotSpot private-field contract; use public + // string copies there even when the image build can see JDK private fields. + return !AndroidSupport.IS_ANDROID + && !GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE + && _JDKAccess.JDK_STRING_FIELD_ACCESS; } private final boolean compressString; From 607a180ff75bf03ae742d19fdcdb13586b58a960 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 24 May 2026 15:11:44 +0800 Subject: [PATCH 22/34] fix(graalvm): avoid private string codegen in native images --- .../java/org/apache/fory/serializer/StringSerializer.java | 6 ++++++ 1 file changed, 6 insertions(+) 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 078bc109bc..8dafbccdd9 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 @@ -127,6 +127,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); @@ -155,6 +158,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); From 047f712cf5925c91740b6ce7fc7d333b662d1eb0 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 24 May 2026 16:16:01 +0800 Subject: [PATCH 23/34] perf(java): add JDK25 direct memory benchmark --- benchmarks/java25/README.md | 32 +++ benchmarks/java25/pom.xml | 115 ++++++++++ .../java25/DirectMemoryAccessBenchmark.java | 216 ++++++++++++++++++ 3 files changed, 363 insertions(+) create mode 100644 benchmarks/java25/README.md create mode 100644 benchmarks/java25/pom.xml create mode 100644 benchmarks/java25/src/main/java/org/apache/fory/benchmark/java25/DirectMemoryAccessBenchmark.java diff --git a/benchmarks/java25/README.md b/benchmarks/java25/README.md new file mode 100644 index 0000000000..c50663989d --- /dev/null +++ b/benchmarks/java25/README.md @@ -0,0 +1,32 @@ +# Java 25 Direct Memory Access Benchmark + +This temporary 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 +``` + +The benchmark class adds the required fork JVM options for the Unsafe path: + +```text +--add-opens=java.base/java.nio=ALL-UNNAMED +--sun-misc-unsafe-memory-access=allow +``` + +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); + } + } +} From 139431b5a9c61bd00f0e64cdc9dae38e3298c56a Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 24 May 2026 16:29:49 +0800 Subject: [PATCH 24/34] perf(java): add JDK25 direct copy benchmark --- benchmarks/java25/README.md | 8 ++ .../java25/DirectToHeapCopyBenchmark.java | 123 ++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 benchmarks/java25/src/main/java/org/apache/fory/benchmark/java25/DirectToHeapCopyBenchmark.java diff --git a/benchmarks/java25/README.md b/benchmarks/java25/README.md index c50663989d..a7edad7c00 100644 --- a/benchmarks/java25/README.md +++ b/benchmarks/java25/README.md @@ -21,6 +21,14 @@ java -jar target/java25-memory-access-benchmarks.jar \ -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 the required fork JVM options for the Unsafe path: ```text 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); + } + } +} From 79e7513a61e1540b4365aef33e26e7b95d681eaf Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 24 May 2026 17:16:22 +0800 Subject: [PATCH 25/34] refactor(java): isolate string unsafe operations --- .../fory/benchmark/Jdk25MrJarCheck.java | 11 +- java/fory-core/pom.xml | 18 +- .../apache/fory/meta/MetaStringEncoder.java | 6 +- .../fory/serializer/PlatformStringUtils.java | 126 ++ .../apache/fory/serializer/Serializers.java | 3 +- .../fory/serializer/SlicedStringUtil.java | 296 ----- .../StringEncodingUtils.java | 318 ++++- .../fory/serializer/StringSerializer.java | 144 +- .../org/apache/fory/util/StringUtils.java | 35 - .../fory/serializer/PlatformStringUtils.java | 139 ++ .../apache/fory/serializer/Serializers.java | 3 +- .../fory/serializer/SlicedStringUtil.java | 285 ---- .../fory/serializer/StringSerializer.java | 1171 ----------------- .../StringEncodingUtilsTest.java | 2 +- .../org/apache/fory/util/StringUtilsTest.java | 35 +- 15 files changed, 630 insertions(+), 1962 deletions(-) create mode 100644 java/fory-core/src/main/java/org/apache/fory/serializer/PlatformStringUtils.java delete mode 100644 java/fory-core/src/main/java/org/apache/fory/serializer/SlicedStringUtil.java rename java/fory-core/src/main/java/org/apache/fory/{util => serializer}/StringEncodingUtils.java (59%) create mode 100644 java/fory-core/src/main/java25/org/apache/fory/serializer/PlatformStringUtils.java delete mode 100644 java/fory-core/src/main/java25/org/apache/fory/serializer/SlicedStringUtil.java delete mode 100644 java/fory-core/src/main/java25/org/apache/fory/serializer/StringSerializer.java rename java/fory-core/src/test/java/org/apache/fory/{util => serializer}/StringEncodingUtilsTest.java (98%) 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 index 857b668e04..1bb2b38d91 100644 --- a/benchmarks/java/src/main/java/org/apache/fory/benchmark/Jdk25MrJarCheck.java +++ b/benchmarks/java/src/main/java/org/apache/fory/benchmark/Jdk25MrJarCheck.java @@ -23,7 +23,6 @@ import org.apache.fory.platform.UnsafeOps; import org.apache.fory.platform.internal._JDKAccess; import org.apache.fory.reflect.FieldAccessor; -import org.apache.fory.serializer.StringSerializer; /** Runtime smoke check that JDK25 benchmark runs load the multi-release Fory classes. */ public final class Jdk25MrJarCheck { @@ -34,12 +33,20 @@ public static void main(String[] args) { verifyClass(UnsafeOps.class); verifyClass(_JDKAccess.class); verifyClass(FieldAccessor.class); - verifyClass(StringSerializer.class); + verifyClass("org.apache.fory.serializer.PlatformStringUtils"); if (_JDKAccess.UNSAFE != null) { throw new IllegalStateException("JDK25 benchmark jar loaded Unsafe-backed _JDKAccess"); } } + 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)); diff --git a/java/fory-core/pom.xml b/java/fory-core/pom.xml index f5a9a699ea..99ccc7a058 100644 --- a/java/fory-core/pom.xml +++ b/java/fory-core/pom.xml @@ -174,6 +174,7 @@ + @@ -251,8 +252,7 @@ name="META-INF/versions/25/org/apache/fory/reflect/HiddenFieldAccessorFactory.class"/> - - + - + file="${jdk25.mrjar.check.dir}/META-INF/versions/25/org/apache/fory/serializer/PlatformStringUtils.class" + property="jdk25.platformstring.present"/> @@ -328,11 +325,8 @@ unless="jdk25.serializers.present" message="JDK25 multi-release Serializers class is missing from the packaged fory-core jar."/> - + unless="jdk25.platformstring.present" + message="JDK25 multi-release PlatformStringUtils class is missing from the packaged fory-core jar."/> 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/serializer/PlatformStringUtils.java b/java/fory-core/src/main/java/org/apache/fory/serializer/PlatformStringUtils.java new file mode 100644 index 0000000000..6ac082acdc --- /dev/null +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/PlatformStringUtils.java @@ -0,0 +1,126 @@ +/* + * 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.platform.AndroidSupport; +import org.apache.fory.platform.GraalvmSupport; +import org.apache.fory.platform.UnsafeOps; +import org.apache.fory.platform.internal._JDKAccess; + +/** Platform-owned 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 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 UnsafeOps.getObject(value, STRING_VALUE_FIELD_OFFSET); + } + + static byte getStringCoder(String value) { + return UnsafeOps.getByte(value, STRING_CODER_FIELD_OFFSET); + } + + static int getStringOffset(String value) { + return UnsafeOps.getInt(value, STRING_OFFSET_FIELD_OFFSET); + } + + static int getStringCount(String value) { + return UnsafeOps.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) { + return UnsafeOps.getLong(chars, UnsafeOps.CHAR_ARRAY_OFFSET + ((long) charIndex << 1)); + } + + static long getBytesLong(byte[] bytes, int byteIndex) { + return UnsafeOps.getLong(bytes, UnsafeOps.BYTE_ARRAY_OFFSET + byteIndex); + } + + static char getBytesChar(byte[] bytes, int byteIndex) { + return UnsafeOps.getChar(bytes, UnsafeOps.BYTE_ARRAY_OFFSET + byteIndex); + } + + static void copyCharsToBytes( + char[] chars, int charOffset, byte[] target, int byteOffset, int numBytes) { + UnsafeOps.UNSAFE.copyMemory( + chars, + UnsafeOps.CHAR_ARRAY_OFFSET + ((long) charOffset << 1), + target, + UnsafeOps.BYTE_ARRAY_OFFSET + byteOffset, + numBytes); + } + + static void putBytes(MemoryBuffer buffer, int writerIndex, byte[] bytes, int numBytes) { + long address = buffer._unsafeWriterAddress() + writerIndex - buffer.writerIndex(); + UnsafeOps.copyMemory(bytes, UnsafeOps.BYTE_ARRAY_OFFSET, null, address, numBytes); + } +} 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 2cca216fba..7d808985d2 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 @@ -69,7 +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; /** Serialization utils and common serializers. */ @SuppressWarnings({"rawtypes", "unchecked"}) @@ -459,7 +458,7 @@ public void write(WriteContext writeContext, T value) { buffer.writeBytes(v, 0, bytesLen); } else { char[] v = (char[]) GET_VALUE.apply(value); - if (StringUtils.isLatin(v)) { + if (StringEncodingUtils.isLatin(v)) { stringSerializer.writeCharsLatin1(buffer, v, value.length()); } else { stringSerializer.writeCharsUTF16(buffer, v, value.length()); diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/SlicedStringUtil.java b/java/fory-core/src/main/java/org/apache/fory/serializer/SlicedStringUtil.java deleted file mode 100644 index d8ecbee4ce..0000000000 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/SlicedStringUtil.java +++ /dev/null @@ -1,296 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.apache.fory.serializer; - -import org.apache.fory.memory.LittleEndian; -import org.apache.fory.memory.MemoryBuffer; -import org.apache.fory.memory.NativeByteOrder; -import org.apache.fory.platform.UnsafeOps; -import org.apache.fory.util.MathUtils; -import org.apache.fory.util.StringEncodingUtils; -import org.apache.fory.util.StringUtils; - -final class SlicedStringUtil { - private static final byte LATIN1 = 0; - private static final byte UTF16 = 1; - private static final byte UTF8 = 2; - - private SlicedStringUtil() {} - - static void writeCharsLatin1WithOffset( - StringSerializer serializer, MemoryBuffer buffer, char[] chars, int offset, int count) { - int writerIndex = buffer.writerIndex(); - long header = ((long) count << 2) | LATIN1; - buffer.ensure(writerIndex + 5 + count); - byte[] targetArray = buffer.getHeapMemory(); - if (targetArray != null) { - final int targetIndex = buffer._unsafeHeapWriterIndex(); - int arrIndex = targetIndex; - arrIndex += LittleEndian.putVarUint36Small(targetArray, arrIndex, header); - writerIndex += arrIndex - targetIndex; - for (int i = 0; i < count; i++) { - targetArray[arrIndex + i] = (byte) chars[offset + i]; - } - } else { - writerIndex += buffer._unsafePutVarUint36Small(writerIndex, header); - final byte[] tmpArray = serializer.getByteArray(count); - for (int i = 0; i < count; i++) { - tmpArray[i] = (byte) chars[offset + i]; - } - buffer.put(writerIndex, tmpArray, 0, count); - } - writerIndex += count; - buffer._unsafeWriterIndex(writerIndex); - } - - static void writeCharsUTF16WithOffset( - StringSerializer serializer, MemoryBuffer buffer, char[] chars, int offset, int count) { - int numBytes = MathUtils.doubleExact(count); - int writerIndex = buffer.writerIndex(); - long header = ((long) numBytes << 2) | UTF16; - buffer.ensure(writerIndex + 5 + numBytes); - final byte[] targetArray = buffer.getHeapMemory(); - if (targetArray != null) { - final int targetIndex = buffer._unsafeHeapWriterIndex(); - int arrIndex = targetIndex; - arrIndex += LittleEndian.putVarUint36Small(targetArray, arrIndex, header); - writerIndex += arrIndex - targetIndex + numBytes; - if (NativeByteOrder.IS_LITTLE_ENDIAN) { - // FIXME JDK11 utf16 string uses little-endian order. - UnsafeOps.UNSAFE.copyMemory( - chars, - UnsafeOps.CHAR_ARRAY_OFFSET + ((long) offset << 1), - targetArray, - UnsafeOps.BYTE_ARRAY_OFFSET + arrIndex, - numBytes); - } else { - writeCharsUTF16BEToHeap(chars, offset, arrIndex, numBytes, targetArray); - } - } else { - writerIndex += buffer._unsafePutVarUint36Small(writerIndex, header); - if (NativeByteOrder.IS_LITTLE_ENDIAN) { - writerIndex = - offHeapWriteCharsUTF16WithOffset( - serializer, buffer, chars, offset, writerIndex, numBytes); - } else { - writerIndex = - offHeapWriteCharsUTF16BEWithOffset( - serializer, buffer, chars, offset, writerIndex, numBytes); - } - } - buffer._unsafeWriterIndex(writerIndex); - } - - static void writeCharsUTF8WithOffset( - StringSerializer serializer, MemoryBuffer buffer, char[] chars, int offset, int count) { - int estimateMaxBytes = count * 3; - int approxNumBytes = (int) (count * 1.5) + 1; - int writerIndex = buffer.writerIndex(); - buffer.ensure(writerIndex + 9 + estimateMaxBytes); - byte[] targetArray = buffer.getHeapMemory(); - if (targetArray != null) { - int targetIndex = buffer._unsafeHeapWriterIndex(); - int headerPos = targetIndex; - int arrIndex = targetIndex; - long header = ((long) approxNumBytes << 2) | UTF8; - int headerBytesWritten = LittleEndian.putVarUint36Small(targetArray, arrIndex, header); - arrIndex += headerBytesWritten; - writerIndex += headerBytesWritten; - targetIndex = - StringEncodingUtils.convertUTF16ToUTF8(chars, offset, count, targetArray, arrIndex); - byte stashedByte = targetArray[arrIndex]; - int written = targetIndex - arrIndex; - header = ((long) written << 2) | UTF8; - int diff = - LittleEndian.putVarUint36Small(targetArray, headerPos, header) - headerBytesWritten; - if (diff != 0) { - handleWriteCharsUTF8UnalignedHeaderBytes(targetArray, arrIndex, diff, written, stashedByte); - } - buffer._unsafeWriterIndex(writerIndex + written + diff); - } else { - final byte[] tmpArray = serializer.getByteArray(estimateMaxBytes); - int written = StringEncodingUtils.convertUTF16ToUTF8(chars, offset, count, tmpArray, 0); - long header = ((long) written << 2) | UTF8; - writerIndex += buffer._unsafePutVarUint36Small(writerIndex, header); - buffer.put(writerIndex, tmpArray, 0, written); - buffer._unsafeWriterIndex(writerIndex + written); - } - } - - static void writeCharsUTF8PerfOptimizedWithOffset( - StringSerializer serializer, MemoryBuffer buffer, char[] chars, int offset, int count) { - int estimateMaxBytes = count * 3; - int numBytes = MathUtils.doubleExact(count); - int writerIndex = buffer.writerIndex(); - long header = ((long) numBytes << 2) | UTF8; - buffer.ensure(writerIndex + 9 + estimateMaxBytes); - byte[] targetArray = buffer.getHeapMemory(); - if (targetArray != null) { - int targetIndex = buffer._unsafeHeapWriterIndex(); - int arrIndex = targetIndex; - arrIndex += LittleEndian.putVarUint36Small(targetArray, arrIndex, header); - writerIndex += arrIndex - targetIndex; - targetIndex = - StringEncodingUtils.convertUTF16ToUTF8(chars, offset, count, targetArray, arrIndex + 4); - int written = targetIndex - arrIndex - 4; - buffer._unsafePutInt32(writerIndex, written); - buffer._unsafeWriterIndex(writerIndex + 4 + written); - } else { - final byte[] tmpArray = serializer.getByteArray(estimateMaxBytes); - int written = StringEncodingUtils.convertUTF16ToUTF8(chars, offset, count, tmpArray, 0); - writerIndex += buffer._unsafePutVarUint36Small(writerIndex, header); - buffer._unsafePutInt32(writerIndex, written); - writerIndex += 4; - buffer.put(writerIndex, tmpArray, 0, written); - buffer._unsafeWriterIndex(writerIndex + written); - } - } - - static boolean isLatin(char[] chars, int offset, int count) { - int end = offset + count; - int vectorizedChars = count & ~3; - int vectorEnd = offset + vectorizedChars; - long byteOffset = UnsafeOps.CHAR_ARRAY_OFFSET + ((long) offset << 1); - long endOffset = UnsafeOps.CHAR_ARRAY_OFFSET + ((long) vectorEnd << 1); - for (long off = byteOffset; off < endOffset; off += 8) { - long multiChars = UnsafeOps.getLong(chars, off); - if ((multiChars & StringUtils.MULTI_CHARS_NON_LATIN_MASK) != 0) { - return false; - } - } - for (int i = vectorEnd; i < end; i++) { - if (chars[i] > 0xFF) { - return false; - } - } - return true; - } - - static byte bestCoder(char[] chars, int offset, int count) { - int sampleNum = Math.min(64, count); - int vectorizedLen = sampleNum >> 2; - int vectorizedChars = vectorizedLen << 2; - long byteOffset = UnsafeOps.CHAR_ARRAY_OFFSET + ((long) offset << 1); - long endOffset = byteOffset + ((long) vectorizedChars << 1); - int asciiCount = 0; - int latin1Count = 0; - int charOffset = offset; - for (long off = byteOffset; off < endOffset; off += 8, charOffset += 4) { - long multiChars = UnsafeOps.getLong(chars, off); - if ((multiChars & StringUtils.MULTI_CHARS_NON_ASCII_MASK) == 0) { - latin1Count += 4; - asciiCount += 4; - } else if ((multiChars & StringUtils.MULTI_CHARS_NON_LATIN_MASK) == 0) { - latin1Count += 4; - for (int i = 0; i < 4; ++i) { - if (chars[charOffset + i] < 0x80) { - asciiCount++; - } - } - } else { - for (int i = 0; i < 4; ++i) { - char c = chars[charOffset + i]; - if (c < 0x80) { - latin1Count++; - asciiCount++; - } else if (c <= 0xFF) { - latin1Count++; - } - } - } - } - - for (int i = vectorizedChars; i < sampleNum; i++) { - char c = chars[offset + i]; - if (c < 0x80) { - latin1Count++; - asciiCount++; - } else if (c <= 0xFF) { - latin1Count++; - } - } - - if (latin1Count == count || (latin1Count == sampleNum && isLatin(chars, offset, count))) { - return LATIN1; - } else if (asciiCount >= sampleNum * 0.5) { - return UTF8; - } else { - return UTF16; - } - } - - private static void handleWriteCharsUTF8UnalignedHeaderBytes( - byte[] targetArray, int arrIndex, int diff, int written, byte stashed) { - if (diff == 1) { - System.arraycopy(targetArray, arrIndex + 1, targetArray, arrIndex + 2, written - 1); - targetArray[arrIndex + 1] = stashed; - } else { - System.arraycopy(targetArray, arrIndex, targetArray, arrIndex - 1, written); - } - } - - private static void writeCharsUTF16BEToHeap( - char[] chars, int offset, int arrIndex, int numBytes, byte[] targetArray) { - int charIndex = offset; - for (int i = arrIndex, end = i + numBytes; i < end; i += 2) { - char c = chars[charIndex++]; - targetArray[i] = (byte) c; - targetArray[i + 1] = (byte) (c >>> 8); - } - } - - private static int offHeapWriteCharsUTF16WithOffset( - StringSerializer serializer, - MemoryBuffer buffer, - char[] chars, - int offset, - int writerIndex, - int numBytes) { - byte[] tmpArray = serializer.getByteArray(numBytes); - UnsafeOps.UNSAFE.copyMemory( - chars, - UnsafeOps.CHAR_ARRAY_OFFSET + ((long) offset << 1), - tmpArray, - UnsafeOps.BYTE_ARRAY_OFFSET, - numBytes); - buffer.put(writerIndex, tmpArray, 0, numBytes); - writerIndex += numBytes; - return writerIndex; - } - - private static int offHeapWriteCharsUTF16BEWithOffset( - StringSerializer serializer, - MemoryBuffer buffer, - char[] chars, - int offset, - int writerIndex, - int numBytes) { - byte[] tmpArray = serializer.getByteArray(numBytes); - int charIndex = offset; - for (int i = 0; i < numBytes; i += 2) { - char c = chars[charIndex++]; - tmpArray[i] = (byte) c; - tmpArray[i + 1] = (byte) (c >>> 8); - } - buffer.put(writerIndex, tmpArray, 0, numBytes); - writerIndex += numBytes; - return writerIndex; - } -} diff --git a/java/fory-core/src/main/java/org/apache/fory/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 8dafbccdd9..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 @@ -36,13 +36,8 @@ 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.UnsafeOps; -import org.apache.fory.platform.internal._JDKAccess; import org.apache.fory.util.MathUtils; import org.apache.fory.util.Preconditions; -import org.apache.fory.util.StringEncodingUtils; -import org.apache.fory.util.StringUtils; /** * String serializer based on JDK-internal string access and byte-array accessors for speed. @@ -53,43 +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 UTF16 = 1; private static final byte UTF8 = 2; private static final int DEFAULT_BUFFER_SIZE = 1024; - 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; - - static { - if (!jdkInternalFieldAccess()) { - 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 { - STRING_VALUE_FIELD_IS_CHARS = _JDKAccess.STRING_VALUE_FIELD_IS_CHARS; - STRING_VALUE_FIELD_IS_BYTES = _JDKAccess.STRING_VALUE_FIELD_IS_BYTES; - STRING_VALUE_FIELD_OFFSET = _JDKAccess.STRING_VALUE_FIELD_OFFSET; - STRING_HAS_COUNT_OFFSET = _JDKAccess.STRING_HAS_COUNT_OFFSET; - STRING_COUNT_FIELD_OFFSET = _JDKAccess.STRING_COUNT_FIELD_OFFSET; - STRING_OFFSET_FIELD_OFFSET = _JDKAccess.STRING_OFFSET_FIELD_OFFSET; - } - } + private static final boolean STRING_HAS_COUNT_OFFSET = + PlatformStringUtils.STRING_HAS_COUNT_OFFSET; private static boolean jdkInternalFieldAccess() { - // Native-image runtime string layout is not a HotSpot Unsafe-offset contract; use public - // string copies there even when the image build can see JDK private fields. - return !AndroidSupport.IS_ANDROID - && !GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE - && _JDKAccess.JDK_STRING_FIELD_ACCESS; + return PlatformStringUtils.JDK_STRING_FIELD_ACCESS; } private final boolean compressString; @@ -380,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; } @@ -427,29 +400,20 @@ 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 UnsafeOps.getObject(value, STRING_VALUE_FIELD_OFFSET); + return PlatformStringUtils.getStringValue(value); } private static byte getStringCoder(String value) { - return UnsafeOps.getByte(value, _JDKAccess.STRING_CODER_FIELD_OFFSET); + return PlatformStringUtils.getStringCoder(value); } private static int getStringOffset(String value) { - return UnsafeOps.getInt(value, STRING_OFFSET_FIELD_OFFSET); + return PlatformStringUtils.getStringOffset(value); } private static int getStringCount(String value) { - return UnsafeOps.getInt(value, STRING_COUNT_FIELD_OFFSET); + return PlatformStringUtils.getStringCount(value); } @CodegenInvoke @@ -489,17 +453,18 @@ public void writeCompressedCharsStringWithOffset(MemoryBuffer buffer, String val final char[] chars = (char[]) getStringValue(value); final int offset = getStringOffset(value); final int count = getStringCount(value); - final byte coder = SlicedStringUtil.bestCoder(chars, offset, count); + 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); } } @@ -531,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); @@ -547,7 +506,7 @@ public static void writeBytesString(MemoryBuffer buffer, byte coder, byte[] byte @CodegenInvoke public void writeCharsString(MemoryBuffer buffer, String value) { final char[] chars = (char[]) getStringValue(value); - if (StringUtils.isLatin(chars)) { + if (StringEncodingUtils.isLatin(chars)) { writeCharsLatin1(buffer, chars, chars.length); } else { writeCharsUTF16(buffer, chars, chars.length); @@ -559,10 +518,10 @@ public void writeCharsStringWithOffset(MemoryBuffer buffer, String value) { final char[] chars = (char[]) getStringValue(value); final int offset = getStringOffset(value); final int count = getStringCount(value); - if (SlicedStringUtil.isLatin(chars, offset, count)) { - SlicedStringUtil.writeCharsLatin1WithOffset(this, buffer, chars, offset, count); + 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); } } @@ -729,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); @@ -896,37 +850,13 @@ private void writeBytesUTF8PerfOptimized(MemoryBuffer buffer, byte[] bytes) { } public static String newCharsStringZeroCopy(char[] data) { - if (!jdkInternalFieldAccess()) { - return newCharsStringSlow(data); - } - return _JDKAccess.newCharsStringZeroCopy(data); - } - - 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 (!jdkInternalFieldAccess()) { - 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); - } + return PlatformStringUtils.newBytesStringZeroCopy(coder, data); } private static void writeCharsUTF16BEToHeap( @@ -1045,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; @@ -1084,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 @@ -1100,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/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/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/java25/org/apache/fory/serializer/Serializers.java b/java/fory-core/src/main/java25/org/apache/fory/serializer/Serializers.java index 6e99a27848..627461be29 100644 --- a/java/fory-core/src/main/java25/org/apache/fory/serializer/Serializers.java +++ b/java/fory-core/src/main/java25/org/apache/fory/serializer/Serializers.java @@ -70,7 +70,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; /** Serialization utils and common serializers. */ @SuppressWarnings({"rawtypes", "unchecked"}) @@ -489,7 +488,7 @@ public void write(WriteContext writeContext, T value) { return; } char[] v = (char[]) rawValue; - if (StringUtils.isLatin(v)) { + if (StringEncodingUtils.isLatin(v)) { stringSerializer.writeCharsLatin1(buffer, v, value.length()); } else { stringSerializer.writeCharsUTF16(buffer, v, value.length()); diff --git a/java/fory-core/src/main/java25/org/apache/fory/serializer/SlicedStringUtil.java b/java/fory-core/src/main/java25/org/apache/fory/serializer/SlicedStringUtil.java deleted file mode 100644 index 924c16d1f6..0000000000 --- a/java/fory-core/src/main/java25/org/apache/fory/serializer/SlicedStringUtil.java +++ /dev/null @@ -1,285 +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) { - writeCharsUTF16BEToHeap(chars, offset, arrIndex, numBytes, targetArray); - } 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); - writeCharsUTF16BEToHeap(chars, offset, 0, numBytes, tmpArray); - 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/java25/org/apache/fory/serializer/StringSerializer.java b/java/fory-core/src/main/java25/org/apache/fory/serializer/StringSerializer.java deleted file mode 100644 index 92cbb62154..0000000000 --- a/java/fory-core/src/main/java25/org/apache/fory/serializer/StringSerializer.java +++ /dev/null @@ -1,1171 +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 static org.apache.fory.type.TypeUtils.STRING_TYPE; -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.nio.charset.StandardCharsets; -import java.util.Arrays; -import org.apache.fory.annotation.CodegenInvoke; -import org.apache.fory.codegen.Expression; -import org.apache.fory.codegen.Expression.Invoke; -import org.apache.fory.codegen.Expression.StaticInvoke; -import org.apache.fory.config.Config; -import org.apache.fory.context.ReadContext; -import org.apache.fory.context.WriteContext; -import org.apache.fory.memory.LittleEndian; -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.UnsafeOps; -import org.apache.fory.platform.internal._JDKAccess; -import org.apache.fory.util.MathUtils; -import org.apache.fory.util.Preconditions; -import org.apache.fory.util.StringEncodingUtils; -import org.apache.fory.util.StringUtils; - -/** - * String serializer based on method handles 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 - * manually. - */ -@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 byte LATIN1 = 0; - private static final byte UTF16 = 1; - private static final byte UTF8 = 2; - private static final int DEFAULT_BUFFER_SIZE = 1024; - - private static final boolean STRING_HAS_COUNT_OFFSET; - - static { - if (!jdkInternalFieldAccess()) { - STRING_VALUE_FIELD_IS_CHARS = false; - STRING_VALUE_FIELD_IS_BYTES = false; - STRING_HAS_COUNT_OFFSET = false; - } else { - STRING_VALUE_FIELD_IS_CHARS = _JDKAccess.STRING_VALUE_FIELD_IS_CHARS; - STRING_VALUE_FIELD_IS_BYTES = _JDKAccess.STRING_VALUE_FIELD_IS_BYTES; - STRING_HAS_COUNT_OFFSET = _JDKAccess.STRING_HAS_COUNT_OFFSET; - } - } - - private static boolean jdkInternalFieldAccess() { - // Native-image runtime string layout is not a HotSpot private-field contract; use public - // string copies there even when the image build can see JDK private fields. - return !AndroidSupport.IS_ANDROID - && !GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE - && _JDKAccess.JDK_STRING_FIELD_ACCESS; - } - - private final boolean compressString; - private final boolean writeNumUtf16BytesForUtf8Encoding; - private final boolean xlang; - - // set default length to 0, since char array and bytes array won't be used at the same time. - private static final byte[] EMPTY_BYTES_STUB = new byte[0]; - private static final char[] EMPTY_CHARS_STUB = new char[0]; - private byte[] byteArray = EMPTY_BYTES_STUB; - private int smoothByteArrayLength = DEFAULT_BUFFER_SIZE; - private char[] charArray = EMPTY_CHARS_STUB; - private int smoothCharArrayLength = DEFAULT_BUFFER_SIZE; - private byte[] byteArray2 = EMPTY_BYTES_STUB; - - public StringSerializer(Config config) { - super(config, String.class, config.trackingRef() && !config.isStringRefIgnored()); - compressString = config.compressString(); - xlang = config.isXlang(); - if (xlang) { - Preconditions.checkArgument(compressString, "compress string muse be enabled for xlang mode"); - } - writeNumUtf16BytesForUtf8Encoding = config.writeNumUtf16BytesForUtf8Encoding(); - } - - @Override - public void write(WriteContext writeContext, String value) { - writeString(writeContext.getBuffer(), value); - } - - @Override - public String read(ReadContext readContext) { - return readString(readContext.getBuffer()); - } - - 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); - } else { - return new StaticInvoke(StringSerializer.class, "writeBytesString", buffer, str); - } - } else { - if (!STRING_VALUE_FIELD_IS_CHARS) { - throw new UnsupportedOperationException(); - } - if (STRING_HAS_COUNT_OFFSET) { - if (compressString) { - return new Invoke(strSerializer, "writeCompressedCharsStringWithOffset", buffer, str); - } else { - return new Invoke(strSerializer, "writeCharsStringWithOffset", buffer, str); - } - } else { - if (compressString) { - return new Invoke(strSerializer, "writeCompressedCharsString", buffer, str); - } else { - return new Invoke(strSerializer, "writeCharsString", buffer, str); - } - } - } - } - - 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); - } else { - return new Invoke(strSerializer, "readBytesString", STRING_TYPE, buffer); - } - } else { - if (!STRING_VALUE_FIELD_IS_CHARS) { - throw new UnsupportedOperationException(); - } - if (compressString) { - return new Invoke(strSerializer, "readCompressedCharsString", STRING_TYPE, buffer); - } else { - return new Invoke(strSerializer, "readCharsString", STRING_TYPE, buffer); - } - } - } - - @CodegenInvoke - public String readBytesString(MemoryBuffer buffer) { - long header = buffer.readVarUint36Small(); - byte coder = (byte) (header & 0b11); - int numBytes = (int) (header >>> 2); - byte[] bytes; - if (!NativeByteOrder.IS_LITTLE_ENDIAN && coder == UTF16) { - bytes = readBytesUTF16BE(buffer, numBytes); - } else { - bytes = readBytesUnCompressedUTF16(buffer, numBytes); - } - if (coder != UTF8) { - return newBytesStringZeroCopy(coder, bytes); - } else { - return new String(bytes, 0, numBytes, StandardCharsets.UTF_8); - } - } - - @CodegenInvoke - public String readCharsString(MemoryBuffer buffer) { - long header = buffer.readVarUint36Small(); - byte coder = (byte) (header & 0b11); - int numBytes = (int) (header >>> 2); - char[] chars; - if (coder == LATIN1) { - chars = readCharsLatin1(buffer, numBytes); - } else if (coder == UTF16) { - chars = readCharsUTF16(buffer, numBytes); - } else { - throw new RuntimeException("Unknown coder type " + coder); - } - return newCharsStringZeroCopy(chars); - } - - @CodegenInvoke - public String readCompressedBytesString(MemoryBuffer buffer) { - long header = buffer.readVarUint36Small(); - byte coder = (byte) (header & 0b11); - int numBytes = (int) (header >>> 2); - if (coder == UTF8) { - byte[] data; - if (writeNumUtf16BytesForUtf8Encoding) { - data = readBytesUTF8PerfOptimized(buffer, numBytes); - } else { - if (xlang) { - return readBytesUTF8ForXlang(buffer, numBytes); - } - data = readBytesUTF8(buffer, numBytes); - } - return newBytesStringZeroCopy(UTF16, data); - } else if (coder == LATIN1) { - return newBytesStringZeroCopy(coder, readBytesUnCompressedUTF16(buffer, numBytes)); - } else if (coder == UTF16) { - byte[] bytes; - if (NativeByteOrder.IS_LITTLE_ENDIAN) { - bytes = readBytesUnCompressedUTF16(buffer, numBytes); - } else { - bytes = readBytesUTF16BE(buffer, numBytes); - } - return newBytesStringZeroCopy(coder, bytes); - } else { - throw new RuntimeException("Unknown coder type " + coder); - } - } - - // the utf8 data may can be encoded with latin1, so the read need to check whether it can be - // encoded by latin1, if true, the coder should be latin1 instead of utf16 - String readBytesUTF8ForXlang(MemoryBuffer buffer, int numBytes) { - buffer.checkReadableBytes(numBytes); - byte[] srcArray = buffer.getHeapMemory(); - - if (srcArray != null) { - int srcIndex = buffer._unsafeHeapReaderIndex(); - - // Fast path: vectorized ASCII check (8 bytes at a time) - if (StringEncodingUtils.isUTF8WithinAscii(srcArray, srcIndex, numBytes)) { - byte[] result = new byte[numBytes]; - System.arraycopy(srcArray, srcIndex, result, 0, numBytes); - buffer._increaseReaderIndexUnsafe(numBytes); - return newBytesStringZeroCopy(LATIN1, result); - } - - // Two-pass approach: scan first, then convert - boolean isLatin1 = StringEncodingUtils.isUTF8WithinLatin1(srcArray, srcIndex, numBytes); - buffer._increaseReaderIndexUnsafe(numBytes); - - if (isLatin1) { - byte[] latin1Buffer = getByteArray(numBytes); - int latin1Len = - StringEncodingUtils.convertUTF8ToLatin1(srcArray, srcIndex, numBytes, latin1Buffer); - return newBytesStringZeroCopy(LATIN1, Arrays.copyOf(latin1Buffer, latin1Len)); - } else { - byte[] utf16Buffer = getByteArray(numBytes << 1); - int utf16Len = - StringEncodingUtils.convertUTF8ToUTF16(srcArray, srcIndex, numBytes, utf16Buffer); - return newBytesStringZeroCopy(UTF16, Arrays.copyOf(utf16Buffer, utf16Len)); - } - } else { - // Off-heap path - byte[] srcBytes = getByteArray2(numBytes); - buffer.readBytes(srcBytes, 0, numBytes); - - // Fast path: vectorized ASCII check - if (StringEncodingUtils.isUTF8WithinAscii(srcBytes, 0, numBytes)) { - // Must copy to exact size since srcBytes is a reusable buffer - return newBytesStringZeroCopy(LATIN1, Arrays.copyOf(srcBytes, numBytes)); - } - - // Two-pass approach: scan first, then convert - boolean isLatin1 = StringEncodingUtils.isUTF8WithinLatin1(srcBytes, 0, numBytes); - - if (isLatin1) { - byte[] latin1Buffer = getByteArray(numBytes); - int latin1Len = - StringEncodingUtils.convertUTF8ToLatin1(srcBytes, 0, numBytes, latin1Buffer); - return newBytesStringZeroCopy(LATIN1, Arrays.copyOf(latin1Buffer, latin1Len)); - } else { - byte[] utf16Buffer = getByteArray(numBytes << 1); - int utf16Len = StringEncodingUtils.convertUTF8ToUTF16(srcBytes, 0, numBytes, utf16Buffer); - return newBytesStringZeroCopy(UTF16, Arrays.copyOf(utf16Buffer, utf16Len)); - } - } - } - - @CodegenInvoke - public String readCompressedCharsString(MemoryBuffer buffer) { - long header = buffer.readVarUint36Small(); - byte coder = (byte) (header & 0b11); - int numBytes = (int) (header >>> 2); - char[] chars; - if (coder == LATIN1) { - chars = readCharsLatin1(buffer, numBytes); - } else if (coder == UTF8) { - return writeNumUtf16BytesForUtf8Encoding - ? readCharsUTF8PerfOptimized(buffer, numBytes) - : readCharsUTF8(buffer, numBytes); - } else if (coder == UTF16) { - chars = readCharsUTF16(buffer, numBytes); - } else { - throw new RuntimeException("Unknown coder type " + coder); - } - return newCharsStringZeroCopy(chars); - } - - // Invoked by fory JIT - public void writeString(MemoryBuffer buffer, String value) { - if (!jdkInternalFieldAccess()) { - writeStringSlow(buffer, value); - return; - } - if (STRING_VALUE_FIELD_IS_BYTES) { - if (compressString) { - writeCompressedBytesString(buffer, value); - } else { - writeBytesString(buffer, value); - } - } else { - writeJava8String(buffer, value); - } - } - - private void writeJava8String(MemoryBuffer buffer, String value) { - assert STRING_VALUE_FIELD_IS_CHARS; - if (STRING_HAS_COUNT_OFFSET) { - if (compressString) { - writeCompressedCharsStringWithOffset(buffer, value); - } else { - writeCharsStringWithOffset(buffer, value); - } - } else { - if (compressString) { - writeCompressedCharsString(buffer, value); - } else { - writeCharsString(buffer, value); - } - } - } - - // Invoked by fory JIT - public String readString(MemoryBuffer buffer) { - if (!jdkInternalFieldAccess()) { - return readStringSlow(buffer); - } - if (STRING_VALUE_FIELD_IS_BYTES) { - if (compressString) { - return readCompressedBytesString(buffer); - } else { - return readBytesString(buffer); - } - } else { - assert STRING_VALUE_FIELD_IS_CHARS; - if (compressString) { - return readCompressedCharsString(buffer); - } else { - return readCharsString(buffer); - } - } - } - - private void writeStringSlow(MemoryBuffer buffer, String value) { - char[] chars = value.toCharArray(); - if (isLatin(chars)) { - writeCharsLatin1(buffer, chars, chars.length); - return; - } - if (compressString) { - byte[] utf8Bytes = value.getBytes(StandardCharsets.UTF_8); - int utf16Bytes = chars.length << 1; - if (utf8Bytes.length < utf16Bytes) { - writeStringUtf8Slow(buffer, utf8Bytes, utf16Bytes); - return; - } - } - writeCharsUTF16(buffer, chars, chars.length); - } - - private String readStringSlow(MemoryBuffer buffer) { - long header = buffer.readVarUint36Small(); - byte coder = (byte) (header & 0b11); - int numBytes = (int) (header >>> 2); - if (coder == LATIN1) { - return new String(readBytesUnCompressedUTF16(buffer, numBytes), StandardCharsets.ISO_8859_1); - } else if (coder == UTF16) { - return new String(readCharsUTF16(buffer, numBytes)); - } else if (coder == UTF8) { - int utf8Bytes = writeNumUtf16BytesForUtf8Encoding ? buffer.readInt32() : numBytes; - return new String(buffer.readBytes(utf8Bytes), StandardCharsets.UTF_8); - } else { - throw new RuntimeException("Unknown coder type " + coder); - } - } - - private void writeStringUtf8Slow(MemoryBuffer buffer, byte[] utf8Bytes, int utf16Bytes) { - int headerLength = writeNumUtf16BytesForUtf8Encoding ? utf16Bytes : utf8Bytes.length; - writeVarUint36Small(buffer, ((long) headerLength << 2) | UTF8); - if (writeNumUtf16BytesForUtf8Encoding) { - buffer.writeInt32(utf8Bytes.length); - } - buffer.writeBytes(utf8Bytes); - } - - private static void writeVarUint36Small(MemoryBuffer buffer, long value) { - int writerIndex = buffer.writerIndex(); - buffer.ensure(writerIndex + 9); - writerIndex += buffer._unsafePutVarUint36Small(writerIndex, 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 _JDKAccess.getStringValue(value); - } - - private static byte getStringCoder(String value) { - return _JDKAccess.getStringCoder(value); - } - - private static int getStringOffset(String value) { - return _JDKAccess.getStringOffset(value); - } - - private static int getStringCount(String value) { - return _JDKAccess.getStringCount(value); - } - - @CodegenInvoke - public void writeCompressedBytesString(MemoryBuffer buffer, String value) { - final byte[] bytes = (byte[]) getStringValue(value); - final byte coder = getStringCoder(value); - if (coder == LATIN1 || bestCoder(bytes) == UTF16) { - writeBytesString(buffer, coder, bytes); - } else { - if (writeNumUtf16BytesForUtf8Encoding) { - writeBytesUTF8PerfOptimized(buffer, bytes); - } else { - writeBytesUTF8(buffer, bytes); - } - } - } - - @CodegenInvoke - public void writeCompressedCharsString(MemoryBuffer buffer, String value) { - final char[] chars = (char[]) getStringValue(value); - final byte coder = bestCoder(chars); - if (coder == LATIN1) { - writeCharsLatin1(buffer, chars, chars.length); - } else if (coder == UTF8) { - if (writeNumUtf16BytesForUtf8Encoding) { - writeCharsUTF8PerfOptimized(buffer, chars); - } else { - writeCharsUTF8(buffer, chars); - } - } else { - writeCharsUTF16(buffer, chars, chars.length); - } - } - - @CodegenInvoke - public void writeCompressedCharsStringWithOffset(MemoryBuffer buffer, String value) { - final char[] chars = (char[]) getStringValue(value); - final int offset = getStringOffset(value); - final int count = getStringCount(value); - final byte coder = SlicedStringUtil.bestCoder(chars, offset, count); - if (coder == LATIN1) { - SlicedStringUtil.writeCharsLatin1WithOffset(this, buffer, chars, offset, count); - } else if (coder == UTF8) { - if (writeNumUtf16BytesForUtf8Encoding) { - SlicedStringUtil.writeCharsUTF8PerfOptimizedWithOffset(this, buffer, chars, offset, count); - } else { - SlicedStringUtil.writeCharsUTF8WithOffset(this, buffer, chars, offset, count); - } - } else { - SlicedStringUtil.writeCharsUTF16WithOffset(this, buffer, chars, offset, count); - } - } - - @CodegenInvoke - public static void writeBytesString(MemoryBuffer buffer, String value) { - byte[] bytes = (byte[]) getStringValue(value); - byte coder = getStringCoder(value); - writeBytesString(buffer, coder, bytes); - } - - public static void writeBytesString(MemoryBuffer buffer, byte coder, byte[] bytes) { - if (!NativeByteOrder.IS_LITTLE_ENDIAN && coder == UTF16) { - writeBytesStringUTF16BE(buffer, bytes); - return; - } - int bytesLen = bytes.length; - long header = ((long) bytesLen << 2) | coder; - int writerIndex = buffer.writerIndex(); - // The `ensure` ensure next operations are safe without bound checks, - // and inner heap buffer doesn't change. - buffer.ensure(writerIndex + 9 + bytesLen); // 1 byte coder + varint max 8 bytes - final byte[] targetArray = buffer.getHeapMemory(); - if (targetArray != null) { - // Some JDK11 Unsafe.copyMemory will `copyMemoryChecks`, and - // jvm doesn't eliminate well in some jdk. - final int targetIndex = buffer._unsafeHeapWriterIndex(); - int arrIndex = targetIndex; - arrIndex += LittleEndian.putVarUint36Small(targetArray, arrIndex, header); - writerIndex += arrIndex - targetIndex; - System.arraycopy(bytes, 0, targetArray, arrIndex, bytesLen); - } else { - final int headerBytes = buffer._unsafePutVarUint36Small(writerIndex, header); - writerIndex += headerBytes; - buffer.put(writerIndex, bytes, 0, bytesLen); - } - writerIndex += bytesLen; - buffer._unsafeWriterIndex(writerIndex); - } - - @CodegenInvoke - public void writeCharsString(MemoryBuffer buffer, String value) { - final char[] chars = (char[]) getStringValue(value); - if (StringUtils.isLatin(chars)) { - writeCharsLatin1(buffer, chars, chars.length); - } else { - writeCharsUTF16(buffer, chars, chars.length); - } - } - - @CodegenInvoke - public void writeCharsStringWithOffset(MemoryBuffer buffer, String value) { - final char[] chars = (char[]) getStringValue(value); - final int offset = getStringOffset(value); - final int count = getStringCount(value); - if (SlicedStringUtil.isLatin(chars, offset, count)) { - SlicedStringUtil.writeCharsLatin1WithOffset(this, buffer, chars, offset, count); - } else { - SlicedStringUtil.writeCharsUTF16WithOffset(this, buffer, chars, offset, count); - } - } - - public char[] readCharsLatin1(MemoryBuffer buffer, int numBytes) { - buffer.checkReadableBytes(numBytes); - byte[] srcArray = buffer.getHeapMemory(); - char[] chars = new char[numBytes]; - if (srcArray != null) { - int srcIndex = buffer._unsafeHeapReaderIndex(); - for (int i = 0; i < numBytes; i++) { - chars[i] = (char) (srcArray[srcIndex++] & 0xff); - } - buffer._increaseReaderIndexUnsafe(numBytes); - } else { - byte[] tmpArray = getByteArray(numBytes); - buffer.readBytes(tmpArray, 0, numBytes); - for (int i = 0; i < numBytes; i++) { - chars[i] = (char) (tmpArray[i] & 0xff); - } - } - return chars; - } - - public byte[] readBytesUTF8(MemoryBuffer buffer, int numBytes) { - byte[] tmpArray = getByteArray(numBytes << 1); - buffer.checkReadableBytes(numBytes); - int utf16NumBytes; - byte[] srcArray = buffer.getHeapMemory(); - if (srcArray != null) { - int srcIndex = buffer._unsafeHeapReaderIndex(); - utf16NumBytes = - StringEncodingUtils.convertUTF8ToUTF16(srcArray, srcIndex, numBytes, tmpArray); - buffer._increaseReaderIndexUnsafe(numBytes); - } else { - byte[] byteArray2 = getByteArray2(numBytes); - buffer.readBytes(byteArray2, 0, numBytes); - utf16NumBytes = StringEncodingUtils.convertUTF8ToUTF16(byteArray2, 0, numBytes, tmpArray); - } - return Arrays.copyOf(tmpArray, utf16NumBytes); - } - - private byte[] readBytesUTF8PerfOptimized(MemoryBuffer buffer, int numBytes) { - int udf8Bytes = buffer.readInt32(); - byte[] bytes = new byte[numBytes]; - // noinspection Duplicates - buffer.checkReadableBytes(udf8Bytes); - byte[] srcArray = buffer.getHeapMemory(); - if (srcArray != null) { - int srcIndex = buffer._unsafeHeapReaderIndex(); - int readLen = StringEncodingUtils.convertUTF8ToUTF16(srcArray, srcIndex, udf8Bytes, bytes); - assert readLen == numBytes : "Decode UTF8 to UTF16 failed"; - buffer._increaseReaderIndexUnsafe(udf8Bytes); - } else { - byte[] tmpArray = getByteArray(udf8Bytes); - buffer.readBytes(tmpArray, 0, udf8Bytes); - int readLen = StringEncodingUtils.convertUTF8ToUTF16(tmpArray, 0, udf8Bytes, bytes); - assert readLen == numBytes : "Decode UTF8 to UTF16 failed"; - } - return bytes; - } - - public byte[] readBytesUnCompressedUTF16(MemoryBuffer buffer, int numBytes) { - buffer.checkReadableBytes(numBytes); - byte[] bytes; - byte[] heapMemory = buffer.getHeapMemory(); - if (heapMemory != null) { - final int arrIndex = buffer._unsafeHeapReaderIndex(); - buffer.increaseReaderIndex(numBytes); - bytes = new byte[numBytes]; - System.arraycopy(heapMemory, arrIndex, bytes, 0, numBytes); - } else { - bytes = buffer.readBytes(numBytes); - } - return bytes; - } - - public char[] readCharsUTF16(MemoryBuffer buffer, int numBytes) { - if (NativeByteOrder.IS_LITTLE_ENDIAN) { - char[] chars = new char[numBytes >> 1]; - // FIXME JDK11 utf16 string uses little-endian order. - buffer.readChars(chars, numBytes >> 1); - return chars; - } else { - return readCharsUTF16BE(buffer, numBytes); - } - } - - public String readCharsUTF8(MemoryBuffer buffer, int numBytes) { - char[] chars = getCharArray(numBytes); - int charsLen; - buffer.checkReadableBytes(numBytes); - byte[] srcArray = buffer.getHeapMemory(); - if (srcArray != null) { - int srcIndex = buffer._unsafeHeapReaderIndex(); - charsLen = StringEncodingUtils.convertUTF8ToUTF16(srcArray, srcIndex, numBytes, chars); - buffer._increaseReaderIndexUnsafe(numBytes); - } else { - byte[] tmpArray = getByteArray(numBytes); - buffer.readBytes(tmpArray, 0, numBytes); - charsLen = StringEncodingUtils.convertUTF8ToUTF16(tmpArray, 0, numBytes, chars); - } - return new String(chars, 0, charsLen); - } - - public String readCharsUTF8PerfOptimized(MemoryBuffer buffer, int numBytes) { - int udf16Chars = numBytes >> 1; - int udf8Bytes = buffer.readInt32(); - char[] chars = new char[udf16Chars]; - // noinspection Duplicates - buffer.checkReadableBytes(udf8Bytes); - byte[] srcArray = buffer.getHeapMemory(); - if (srcArray != null) { - int srcIndex = buffer._unsafeHeapReaderIndex(); - int readLen = StringEncodingUtils.convertUTF8ToUTF16(srcArray, srcIndex, udf8Bytes, chars); - assert readLen == udf16Chars : "Decode UTF8 to UTF16 failed"; - buffer._increaseReaderIndexUnsafe(udf8Bytes); - } else { - byte[] tmpArray = getByteArray(udf8Bytes); - buffer.readBytes(tmpArray, 0, udf8Bytes); - int readLen = StringEncodingUtils.convertUTF8ToUTF16(tmpArray, 0, udf8Bytes, chars); - assert readLen == udf16Chars : "Decode UTF8 to UTF16 failed"; - } - return newCharsStringZeroCopy(chars); - } - - public void writeCharsLatin1(MemoryBuffer buffer, char[] chars, int numBytes) { - int writerIndex = buffer.writerIndex(); - long header = ((long) numBytes << 2) | LATIN1; - buffer.ensure(writerIndex + 5 + numBytes); - 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 < numBytes; i++) { - targetArray[arrIndex + i] = (byte) chars[i]; - } - } else { - writerIndex += buffer._unsafePutVarUint36Small(writerIndex, header); - final byte[] tmpArray = getByteArray(numBytes); - for (int i = 0; i < numBytes; i++) { - tmpArray[i] = (byte) chars[i]; - } - buffer.put(writerIndex, tmpArray, 0, numBytes); - } - writerIndex += numBytes; - buffer._unsafeWriterIndex(writerIndex); - } - - public void writeCharsUTF16(MemoryBuffer buffer, char[] chars, int numChars) { - int numBytes = MathUtils.doubleExact(numChars); - 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) { - if (AndroidSupport.IS_ANDROID) { - writeCharsUTF16ToHeapSlow(chars, arrIndex, numBytes, targetArray); - } else { - writeCharsUTF16ToHeapSlow(chars, arrIndex, numBytes, targetArray); - } - } else { - writeCharsUTF16BEToHeap(chars, arrIndex, numBytes, targetArray); - } - } else { - writerIndex += buffer._unsafePutVarUint36Small(writerIndex, header); - if (NativeByteOrder.IS_LITTLE_ENDIAN) { - writerIndex = offHeapWriteCharsUTF16(buffer, chars, writerIndex, numBytes); - } else { - writerIndex = offHeapWriteCharsUTF16BE(buffer, chars, writerIndex, numBytes); - } - } - buffer._unsafeWriterIndex(writerIndex); - } - - public void writeCharsUTF8(MemoryBuffer buffer, char[] chars) { - int estimateMaxBytes = chars.length * 3; - // num bytes of utf8 should be smaller than utf16, otherwise we should - // utf16 instead. - // We can't use length in header since we don't know num chars in go/c++ - int approxNumBytes = (int) (chars.length * 1.5) + 1; - int writerIndex = buffer.writerIndex(); - // 9 for max bytes of header - buffer.ensure(writerIndex + 9 + estimateMaxBytes); - byte[] targetArray = buffer.getHeapMemory(); - if (targetArray != null) { - // noinspection Duplicates - int targetIndex = buffer._unsafeHeapWriterIndex(); - // keep this index in case actual num utf8 bytes need different bytes for header - int headerPos = targetIndex; - int arrIndex = targetIndex; - long header = ((long) approxNumBytes << 2) | UTF8; - int headerBytesWritten = LittleEndian.putVarUint36Small(targetArray, arrIndex, header); - arrIndex += headerBytesWritten; - writerIndex += headerBytesWritten; - // noinspection Duplicates - targetIndex = StringEncodingUtils.convertUTF16ToUTF8(chars, 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 { - // noinspection Duplicates - final byte[] tmpArray = getByteArray(estimateMaxBytes); - int written = StringEncodingUtils.convertUTF16ToUTF8(chars, tmpArray, 0); - long header = ((long) written << 2) | UTF8; - writerIndex += buffer._unsafePutVarUint36Small(writerIndex, header); - buffer.put(writerIndex, tmpArray, 0, written); - buffer._unsafeWriterIndex(writerIndex + written); - } - } - - public void writeCharsUTF8PerfOptimized(MemoryBuffer buffer, char[] chars) { - int estimateMaxBytes = chars.length * 3; - int numBytes = MathUtils.doubleExact(chars.length); - // noinspection Duplicates - 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, targetArray, arrIndex + 4); - int written = targetIndex - arrIndex - 4; - buffer._unsafePutInt32(writerIndex, written); - buffer._unsafeWriterIndex(writerIndex + 4 + written); - } else { - final byte[] tmpArray = getByteArray(estimateMaxBytes); - int written = StringEncodingUtils.convertUTF16ToUTF8(chars, tmpArray, 0); - writerIndex += buffer._unsafePutVarUint36Small(writerIndex, header); - buffer._unsafePutInt32(writerIndex, written); - writerIndex += 4; - buffer.put(writerIndex, tmpArray, 0, written); - buffer._unsafeWriterIndex(writerIndex + written); - } - } - - private 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 void writeBytesUTF8(MemoryBuffer buffer, byte[] bytes) { - int numBytes = bytes.length; - int estimateMaxBytes = bytes.length / 2 * 3; - int writerIndex = buffer.writerIndex(); - buffer.ensure(writerIndex + 9 + estimateMaxBytes); - byte[] targetArray = buffer.getHeapMemory(); - if (targetArray != null) { - // noinspection Duplicates - int targetIndex = buffer._unsafeHeapWriterIndex(); - // keep this index in case actual num utf8 bytes need different bytes for header - int headerPos = targetIndex; - int arrIndex = targetIndex; - long header = ((long) numBytes << 2) | UTF8; - int headerBytesWritten = LittleEndian.putVarUint36Small(targetArray, arrIndex, header); - arrIndex += headerBytesWritten; - writerIndex += arrIndex - targetIndex; - // noinspection Duplicates - targetIndex = StringEncodingUtils.convertUTF16ToUTF8(bytes, 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 { - // noinspection Duplicates - final byte[] tmpArray = getByteArray(estimateMaxBytes); - int written = StringEncodingUtils.convertUTF16ToUTF8(bytes, tmpArray, 0); - long header = ((long) written << 2) | UTF8; - writerIndex += buffer._unsafePutVarUint36Small(writerIndex, header); - buffer.put(writerIndex, tmpArray, 0, written); - buffer._unsafeWriterIndex(writerIndex + written); - } - } - - private void writeBytesUTF8PerfOptimized(MemoryBuffer buffer, byte[] bytes) { - int numBytes = bytes.length; - int estimateMaxBytes = bytes.length / 2 * 3; - 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(bytes, targetArray, arrIndex + 4); - int written = targetIndex - arrIndex - 4; - buffer._unsafePutInt32(writerIndex, written); - buffer._unsafeWriterIndex(writerIndex + 4 + written); - } else { - final byte[] tmpArray = getByteArray(estimateMaxBytes); - int written = StringEncodingUtils.convertUTF16ToUTF8(bytes, tmpArray, 0); - writerIndex += buffer._unsafePutVarUint36Small(writerIndex, header); - buffer._unsafePutInt32(writerIndex, written); - writerIndex += 4; - buffer.put(writerIndex, tmpArray, 0, written); - buffer._unsafeWriterIndex(writerIndex + written); - } - } - - public static String newCharsStringZeroCopy(char[] data) { - if (!jdkInternalFieldAccess()) { - return newCharsStringSlow(data); - } - return _JDKAccess.newCharsStringZeroCopy(data); - } - - private static String newCharsStringSlow(char[] data) { - return new String(data); - } - - // coder param first to make inline call args - // `(buffer.readByte(), buffer.readBytesWithSizeEmbedded())` work. - public static String newBytesStringZeroCopy(byte coder, byte[] data) { - if (!jdkInternalFieldAccess()) { - 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); - } - } - - private static void writeCharsUTF16BEToHeap( - char[] chars, int arrIndex, int numBytes, byte[] targetArray) { - // Write to heap memory then copy is 250% faster than unsafe write to direct memory. - int charIndex = 0; - 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 void writeCharsUTF16ToHeapSlow( - char[] chars, int arrIndex, int numBytes, byte[] targetArray) { - writeCharsUTF16BEToHeap(chars, arrIndex, numBytes, targetArray); - } - - private int offHeapWriteCharsUTF16( - MemoryBuffer buffer, char[] chars, int writerIndex, int numBytes) { - byte[] tmpArray = getByteArray(numBytes); - int charIndex = 0; - for (int i = 0; i < numBytes; i += 2) { - char c = chars[charIndex++]; - tmpArray[i] = (byte) (c >> StringUTF16.HI_BYTE_SHIFT); - tmpArray[i + 1] = (byte) (c >> StringUTF16.LO_BYTE_SHIFT); - } - buffer.put(writerIndex, tmpArray, 0, numBytes); - writerIndex += numBytes; - return writerIndex; - } - - private int offHeapWriteCharsUTF16BE( - MemoryBuffer buffer, char[] chars, int writerIndex, int numBytes) { - byte[] tmpArray = getByteArray(numBytes); - int charIndex = 0; - 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; - } - - private char[] readCharsUTF16BE(MemoryBuffer buffer, int numBytes) { - buffer.checkReadableBytes(numBytes); - final byte[] targetArray = buffer.getHeapMemory(); - char[] chars = new char[numBytes >> 1]; - if (targetArray != null) { - int charIndex = 0; - for (int i = buffer._unsafeHeapReaderIndex(), end = i + numBytes; i < end; i += 2) { - int lo = targetArray[i] & 0xff; - int hi = targetArray[i + 1] & 0xff; - chars[charIndex++] = (char) (lo | (hi << 8)); - } - buffer._increaseReaderIndexUnsafe(numBytes); - } else { - final byte[] tmpArray = getByteArray(numBytes); - buffer.readBytes(tmpArray, 0, numBytes); - int charIndex = 0; - for (int i = 0; i < numBytes; i += 2) { - int lo = tmpArray[i] & 0xff; - int hi = tmpArray[i + 1] & 0xff; - chars[charIndex++] = (char) (lo | (hi << 8)); - } - } - return chars; - } - - private byte[] readBytesUTF16BE(MemoryBuffer buffer, int numBytes) { - byte[] bytes = readBytesUnCompressedUTF16(buffer, numBytes); - swapUTF16BytesInPlace(bytes); - return bytes; - } - - private static void swapUTF16BytesInPlace(byte[] bytes) { - for (int i = 0; i < bytes.length; i += 2) { - byte tmp = bytes[i]; - bytes[i] = bytes[i + 1]; - bytes[i + 1] = tmp; - } - } - - private static void writeBytesStringUTF16BE(MemoryBuffer buffer, byte[] bytes) { - int bytesLen = bytes.length; - long header = ((long) bytesLen << 2) | UTF16; - int writerIndex = buffer.writerIndex(); - buffer.ensure(writerIndex + 9 + bytesLen); - 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; - for (int i = 0; i < bytesLen; i += 2) { - targetArray[arrIndex + i] = bytes[i + 1]; - targetArray[arrIndex + i + 1] = bytes[i]; - } - } else { - writerIndex += buffer._unsafePutVarUint36Small(writerIndex, header); - byte[] tmpArray = new byte[bytesLen]; - for (int i = 0; i < bytesLen; i += 2) { - tmpArray[i] = bytes[i + 1]; - tmpArray[i + 1] = bytes[i]; - } - buffer.put(writerIndex, tmpArray, 0, bytesLen); - } - buffer._unsafeWriterIndex(writerIndex + bytesLen); - } - - private static byte bestCoder(char[] chars) { - int numChars = chars.length; - // sample 64 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); - 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) { - if (chars[charOffset + i] < 0x80) { - latin1Count++; - asciiCount++; - } else if (chars[charOffset + i] <= 0xFF) { - latin1Count++; - } - } - } - } - - for (int i = vectorizedChars; i < sampleNum; i++) { - if (chars[i] < 0x80) { - latin1Count++; - asciiCount++; - } else if (chars[i] <= 0xFF) { - latin1Count++; - } - } - - if (latin1Count == numChars - || (latin1Count == sampleNum && StringUtils.isLatin(chars, sampleNum))) { - return LATIN1; - } else if (asciiCount >= sampleNum * 0.5) { - // ascii number > 50%, choose UTF-8 - return UTF8; - } else { - return UTF16; - } - } - - private static byte bestCoder(byte[] bytes) { - int numBytes = bytes.length; - // sample 64 chars - 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); - 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) { - asciiCount++; - } - } - } - } - for (int i = vectorizedBytes; vectorizedBytes < sampleNum; vectorizedBytes += 2) { - if (UnsafeOps.getChar(bytes, UnsafeOps.BYTE_ARRAY_OFFSET + i) < 0x80) { - asciiCount++; - } - } - // ascii number > 50%, choose UTF-8 - if (asciiCount >= sampleNum * 0.5) { - return UTF8; - } else { - return UTF16; - } - } - - private char[] getCharArray(int numElements) { - char[] charArray = this.charArray; - if (charArray.length < numElements) { - charArray = new char[numElements]; - this.charArray = charArray; - } - if (charArray.length > DEFAULT_BUFFER_SIZE) { - smoothCharArrayLength = - Math.max(((int) (smoothCharArrayLength * 0.9 + numElements * 0.1)), DEFAULT_BUFFER_SIZE); - if (smoothByteArrayLength <= DEFAULT_BUFFER_SIZE) { - this.charArray = new char[DEFAULT_BUFFER_SIZE]; - } - } - return charArray; - } - - byte[] getByteArray(int numElements) { - byte[] byteArray = this.byteArray; - if (byteArray.length < numElements) { - byteArray = new byte[numElements]; - this.byteArray = byteArray; - } - if (byteArray.length > DEFAULT_BUFFER_SIZE) { - smoothByteArrayLength = - Math.max(((int) (smoothByteArrayLength * 0.9 + numElements * 0.1)), DEFAULT_BUFFER_SIZE); - if (smoothByteArrayLength <= DEFAULT_BUFFER_SIZE) { - this.byteArray = new byte[DEFAULT_BUFFER_SIZE]; - } - } - return byteArray; - } - - private byte[] getByteArray2(int numElements) { - byte[] byteArray2 = this.byteArray2; - if (byteArray2.length < numElements) { - byteArray2 = new byte[numElements]; - this.byteArray = byteArray2; - } - if (byteArray2.length > DEFAULT_BUFFER_SIZE) { - smoothByteArrayLength = - Math.max(((int) (smoothByteArrayLength * 0.9 + numElements * 0.1)), DEFAULT_BUFFER_SIZE); - if (smoothByteArrayLength <= DEFAULT_BUFFER_SIZE) { - this.byteArray2 = new byte[DEFAULT_BUFFER_SIZE]; - } - } - return byteArray2; - } - - public void clearBuffer(int size) { - if (byteArray.length >= size) { - byteArray = EMPTY_BYTES_STUB; - } - if (byteArray2.length >= size) { - byteArray2 = EMPTY_BYTES_STUB; - } - if (charArray.length >= size) { - this.charArray = EMPTY_CHARS_STUB; - } - } -} 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/util/StringUtilsTest.java b/java/fory-core/src/test/java/org/apache/fory/util/StringUtilsTest.java index 7aa1893ee7..4d639a4236 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 @@ -26,6 +26,7 @@ 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 { @@ -153,23 +154,23 @@ private boolean isLatin(char[] chars, boolean isLittle) { @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())); } } From d602cbfc02b51f7ad2ce186fb74e50c5f6c7743e Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 24 May 2026 17:46:30 +0800 Subject: [PATCH 26/34] refactor(java): remove versioned lambda serializer --- java/fory-core/pom.xml | 7 - .../fory/platform/internal/_JDKAccess.java | 5 + .../SerializedLambdaSerializer.java | 3 +- .../fory/platform/internal/_JDKAccess.java | 24 ++ .../SerializedLambdaSerializer.java | 216 ------------------ 5 files changed, 30 insertions(+), 225 deletions(-) delete mode 100644 java/fory-core/src/main/java25/org/apache/fory/serializer/SerializedLambdaSerializer.java diff --git a/java/fory-core/pom.xml b/java/fory-core/pom.xml index 99ccc7a058..4fbcb35b27 100644 --- a/java/fory-core/pom.xml +++ b/java/fory-core/pom.xml @@ -250,7 +250,6 @@ - @@ -282,9 +281,6 @@ - @@ -318,9 +314,6 @@ - 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 index 7019213ed0..8584f722f4 100644 --- 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 @@ -220,6 +220,11 @@ public static Lookup _trustedLookup(Class 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; 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 ec8c3a604e..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 @@ -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/java25/org/apache/fory/platform/internal/_JDKAccess.java b/java/fory-core/src/main/java25/org/apache/fory/platform/internal/_JDKAccess.java index 306b782642..0c057eb336 100644 --- 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 @@ -29,6 +29,7 @@ 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; @@ -341,6 +342,29 @@ public static Lookup _trustedLookup(Class 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; diff --git a/java/fory-core/src/main/java25/org/apache/fory/serializer/SerializedLambdaSerializer.java b/java/fory-core/src/main/java25/org/apache/fory/serializer/SerializedLambdaSerializer.java deleted file mode 100644 index a744b99fff..0000000000 --- a/java/fory-core/src/main/java25/org/apache/fory/serializer/SerializedLambdaSerializer.java +++ /dev/null @@ -1,216 +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 java.lang.invoke.MethodHandle; -import java.lang.invoke.SerializedLambda; -import java.lang.reflect.Method; -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.platform.AndroidSupport; -import org.apache.fory.platform.internal._JDKAccess; -import org.apache.fory.resolver.TypeResolver; -import org.apache.fory.util.Preconditions; - -/** - * Serializer for {@link SerializedLambda}. It writes the JDK lambda payload through the public - * getter API, applies {@code readResolve} on read, and preserves unresolved {@code - * SerializedLambda} form on direct copy. - */ -@SuppressWarnings({"rawtypes", "unchecked"}) -public class SerializedLambdaSerializer extends Serializer { - static final Class SERIALIZED_LAMBDA = SerializedLambda.class; - private static final MethodHandle READ_RESOLVE_HANDLE; - private final TypeResolver typeResolver; - - static { - MethodHandle readResolveHandle = null; - if (AndroidSupport.IS_ANDROID) { - // Lambda serialization is unsupported on Android. - } else { - try { - Method readResolveMethod = JavaSerializer.getReadResolveMethod(SERIALIZED_LAMBDA); - Preconditions.checkNotNull( - readResolveMethod, "Missing readResolve for " + SERIALIZED_LAMBDA); - try { - readResolveHandle = - _JDKAccess._trustedLookup(SERIALIZED_LAMBDA).unreflect(readResolveMethod); - } catch (RuntimeException e) { - // JDK25 rejects privateLookupIn for java.lang.invoke.SerializedLambda itself. With - // java.lang.invoke opened, reflective access still exposes the JDK readResolve method - // without using Unsafe. - readResolveMethod.setAccessible(true); - readResolveHandle = java.lang.invoke.MethodHandles.lookup().unreflect(readResolveMethod); - } - } catch (IllegalAccessException | RuntimeException e) { - // Keep serializer registration available; readResolve reports the missing open if used. - } - } - READ_RESOLVE_HANDLE = readResolveHandle; - } - - public SerializedLambdaSerializer(TypeResolver typeResolver, Class cls) { - super(typeResolver.getConfig(), cls); - this.typeResolver = typeResolver; - Preconditions.checkArgument(cls == SERIALIZED_LAMBDA); - } - - @Override - public void write(WriteContext writeContext, Object value) { - throwIfAndroid(); - MemoryBuffer buffer = writeContext.getBuffer(); - SerializedLambda serializedLambda = (SerializedLambda) value; - writeContext.writeStringRef(serializedLambda.getCapturingClass()); - writeContext.writeStringRef(serializedLambda.getFunctionalInterfaceClass()); - writeContext.writeStringRef(serializedLambda.getFunctionalInterfaceMethodName()); - writeContext.writeStringRef(serializedLambda.getFunctionalInterfaceMethodSignature()); - writeContext.writeStringRef(serializedLambda.getImplClass()); - writeContext.writeStringRef(serializedLambda.getImplMethodName()); - writeContext.writeStringRef(serializedLambda.getImplMethodSignature()); - buffer.writeVarInt32(serializedLambda.getImplMethodKind()); - writeContext.writeStringRef(serializedLambda.getInstantiatedMethodType()); - int capturedArgCount = serializedLambda.getCapturedArgCount(); - buffer.writeVarUInt32Small7(capturedArgCount); - for (int i = 0; i < capturedArgCount; i++) { - writeContext.writeRef(serializedLambda.getCapturedArg(i)); - } - } - - @Override - public Object copy(CopyContext copyContext, Object value) { - throwIfAndroid(); - SerializedLambda serializedLambda = (SerializedLambda) value; - int capturedArgCount = serializedLambda.getCapturedArgCount(); - Object[] capturedArgs = new Object[capturedArgCount]; - for (int i = 0; i < capturedArgCount; i++) { - capturedArgs[i] = copyContext.copyObject(serializedLambda.getCapturedArg(i)); - } - return newSerializedLambda( - serializedLambda.getCapturingClass(), - serializedLambda.getFunctionalInterfaceClass(), - serializedLambda.getFunctionalInterfaceMethodName(), - serializedLambda.getFunctionalInterfaceMethodSignature(), - serializedLambda.getImplMethodKind(), - serializedLambda.getImplClass(), - serializedLambda.getImplMethodName(), - serializedLambda.getImplMethodSignature(), - serializedLambda.getInstantiatedMethodType(), - capturedArgs); - } - - @Override - public Object read(ReadContext readContext) { - throwIfAndroid(); - return readResolve(readUnresolved(readContext)); - } - - Object readUnresolved(ReadContext readContext) { - throwIfAndroid(); - MemoryBuffer buffer = readContext.getBuffer(); - String capturingClass = readContext.readStringRef(); - String functionalInterfaceClass = readContext.readStringRef(); - String functionalInterfaceMethodName = readContext.readStringRef(); - String functionalInterfaceMethodSignature = readContext.readStringRef(); - String implClass = readContext.readStringRef(); - String implMethodName = readContext.readStringRef(); - String implMethodSignature = readContext.readStringRef(); - int implMethodKind = buffer.readVarInt32(); - String instantiatedMethodType = readContext.readStringRef(); - int capturedArgCount = buffer.readVarUInt32Small7(); - Object[] capturedArgs = new Object[capturedArgCount]; - for (int i = 0; i < capturedArgCount; i++) { - capturedArgs[i] = readContext.readRef(); - } - return newSerializedLambda( - capturingClass, - functionalInterfaceClass, - functionalInterfaceMethodName, - functionalInterfaceMethodSignature, - implMethodKind, - implClass, - implMethodName, - implMethodSignature, - instantiatedMethodType, - capturedArgs); - } - - static Object readResolve(Object replacement) { - throwIfAndroid(); - if (READ_RESOLVE_HANDLE == null) { - throw new ForyException( - "SerializedLambda.readResolve is inaccessible. On JDK25+, deserialize lambdas only " - + "with --add-opens=java.base/java.lang.invoke=" - + "org.apache.fory.core,org.apache.fory.format."); - } - try { - return READ_RESOLVE_HANDLE.invoke(replacement); - } catch (Throwable e) { - throw new RuntimeException("Can't deserialize lambda", e); - } - } - - private static void throwIfAndroid() { - if (AndroidSupport.IS_ANDROID) { - throw new UnsupportedOperationException( - "Lambda serialization is unsupported on Android; serialize explicit data objects instead."); - } - } - - private SerializedLambda newSerializedLambda( - String capturingClass, - String functionalInterfaceClass, - String functionalInterfaceMethodName, - String functionalInterfaceMethodSignature, - int implMethodKind, - String implClass, - String implMethodName, - String implMethodSignature, - String instantiatedMethodType, - Object[] capturedArgs) { - return new SerializedLambda( - loadCapturingClass(capturingClass), - functionalInterfaceClass, - functionalInterfaceMethodName, - functionalInterfaceMethodSignature, - implMethodKind, - implClass, - implMethodName, - implMethodSignature, - instantiatedMethodType, - capturedArgs); - } - - private Class loadCapturingClass(String className) { - String binaryClassName = className.replace('/', '.'); - try { - return Class.forName(binaryClassName, false, typeResolver.getClassLoader()); - } catch (ClassNotFoundException e) { - try { - return Class.forName( - binaryClassName, false, Thread.currentThread().getContextClassLoader()); - } catch (ClassNotFoundException ex) { - throw new RuntimeException("Can't load capturing class " + binaryClassName, ex); - } - } - } -} From a17ab3fd824ae15d06efc8c9fdf84150a558f630 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 24 May 2026 17:55:46 +0800 Subject: [PATCH 27/34] refactor(java): remove versioned serializers registry --- benchmarks/java/pom.xml | 34 +- java/fory-core/pom.xml | 7 - .../apache/fory/serializer/Serializers.java | 864 ------------------ 3 files changed, 5 insertions(+), 900 deletions(-) delete mode 100644 java/fory-core/src/main/java25/org/apache/fory/serializer/Serializers.java diff --git a/benchmarks/java/pom.xml b/benchmarks/java/pom.xml index 07b793301b..2c8e5f33a1 100644 --- a/benchmarks/java/pom.xml +++ b/benchmarks/java/pom.xml @@ -279,13 +279,7 @@ - - - + name="META-INF/versions/25/org/apache/fory/serializer/PlatformStringUtils.class"/> - - - + file="${jdk25.benchmark.check.dir}/META-INF/versions/25/org/apache/fory/serializer/PlatformStringUtils.class" + property="jdk25.benchmark.platformstring.present"/> @@ -343,17 +328,8 @@ unless="jdk25.benchmark.objectcodecbuilder.present" message="JDK25 benchmark jar is missing the versioned ObjectCodecBuilder class."/> - - - + unless="jdk25.benchmark.platformstring.present" + message="JDK25 benchmark jar is missing the versioned PlatformStringUtils class."/> - @@ -281,9 +280,6 @@ - @@ -314,9 +310,6 @@ - diff --git a/java/fory-core/src/main/java25/org/apache/fory/serializer/Serializers.java b/java/fory-core/src/main/java25/org/apache/fory/serializer/Serializers.java deleted file mode 100644 index 627461be29..0000000000 --- a/java/fory-core/src/main/java25/org/apache/fory/serializer/Serializers.java +++ /dev/null @@ -1,864 +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 static org.apache.fory.util.function.Functions.makeGetterFunction; - -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.MethodType; -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.math.BigDecimal; -import java.math.BigInteger; -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; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Function; -import java.util.function.ToIntFunction; -import java.util.regex.Pattern; -import org.apache.fory.Fory; -import org.apache.fory.collection.Cache; -import org.apache.fory.collection.CacheBuilder; -import org.apache.fory.collection.Tuple2; -import org.apache.fory.config.Config; -import org.apache.fory.context.CopyContext; -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; -import org.apache.fory.resolver.TypeResolver; -import org.apache.fory.serializer.CodegenSerializer.LazyInitBeanSerializer; -import org.apache.fory.serializer.collection.ChildContainerSerializers; -import org.apache.fory.serializer.collection.CollectionSerializer; -import org.apache.fory.serializer.collection.CollectionSerializers; -import org.apache.fory.serializer.collection.MapSerializer; -import org.apache.fory.serializer.collection.MapSerializers; -import org.apache.fory.serializer.scala.SingletonCollectionSerializer; -import org.apache.fory.serializer.scala.SingletonMapSerializer; -import org.apache.fory.serializer.scala.SingletonObjectSerializer; -import org.apache.fory.util.ExceptionUtils; - -/** Serialization utils and common serializers. */ -@SuppressWarnings({"rawtypes", "unchecked"}) -public class Serializers { - // avoid duplicate reflect inspection and cache for graalvm support too. - private static final Cache> CTR_MAP; - - static { - if (GraalvmSupport.isGraalBuildTime()) { - CTR_MAP = CacheBuilder.newBuilder().concurrencyLevel(32).build(); - } else { - CTR_MAP = CacheBuilder.newBuilder().weakKeys().softValues().build(); - } - } - - private static final MethodType SIG1 = - MethodType.methodType(void.class, TypeResolver.class, Class.class); - private static final MethodType SIG2 = MethodType.methodType(void.class, TypeResolver.class); - private static final MethodType SIG3 = - MethodType.methodType(void.class, Config.class, Class.class); - private static final MethodType SIG4 = MethodType.methodType(void.class, Config.class); - private static final MethodType SIG5 = MethodType.methodType(void.class, Class.class); - private static final MethodType SIG6 = MethodType.methodType(void.class); - - /** - * Serializer subclass must have a constructor which take parameters of type {@link TypeResolver} - * and {@link Class}, or {@link TypeResolver}, or {@link Config} and {@link Class}, or {@link - * Config}, or {@link Class}, or no-arg constructor. - */ - public static Serializer newSerializer( - Fory fory, Class type, Class serializerClass) { - return newSerializer(fory.getTypeResolver(), type, serializerClass); - } - - /** - * Serializer subclass must have a constructor which take parameters of type {@link TypeResolver} - * and {@link Class}, or {@link TypeResolver}, or {@link Config} and {@link Class}, or {@link - * Config}, or {@link Class}, or no-arg constructor. - */ - public static Serializer newSerializer( - TypeResolver typeResolver, Class type, Class serializerClass) { - TypeInfo typeInfo = typeResolver.getTypeInfo(type, false); - Serializer serializer = typeInfo == null ? null : typeInfo.getSerializer(); - try { - return buildSerializer(typeResolver, type, serializerClass); - } catch (Throwable t) { - // Some serializer may set itself in constructor as serializer, but the - // constructor failed later. For example, some final type field doesn't - // support serialization. - typeResolver.resetSerializer(type, serializer); - if (t instanceof java.lang.reflect.InvocationTargetException && t.getCause() != null) { - ExceptionUtils.throwException(t.getCause()); - } - ExceptionUtils.throwException(t); - } - throw new IllegalStateException("unreachable"); - } - - private static Serializer buildSerializer( - TypeResolver typeResolver, Class type, Class serializerClass) { - try { - Config config = typeResolver.getConfig(); - Serializer serializer = - buildBuiltinSerializer(typeResolver, config, type, serializerClass); - if (serializer != null) { - return serializer; - } - Tuple2 ctrInfo = CTR_MAP.getIfPresent(serializerClass); - if (ctrInfo != null) { - MethodType sig = ctrInfo.f0; - MethodHandle handle = ctrInfo.f1; - if (sig.equals(SIG1)) { - return (Serializer) handle.invoke(typeResolver, type); - } else if (sig.equals(SIG2)) { - return (Serializer) handle.invoke(typeResolver); - } else if (sig.equals(SIG3)) { - return (Serializer) handle.invoke(config, type); - } else if (sig.equals(SIG4)) { - return (Serializer) handle.invoke(config); - } else if (sig.equals(SIG5)) { - return (Serializer) handle.invoke(type); - } else { - return (Serializer) handle.invoke(); - } - } - return createSerializer(typeResolver, type, serializerClass); - } catch (Throwable t) { - ExceptionUtils.throwException(t); - throw new IllegalStateException("unreachable"); - } - } - - private static Serializer buildBuiltinSerializer( - TypeResolver typeResolver, - Config config, - Class type, - Class serializerClass) { - if (serializerClass == ObjectSerializer.class) { - return new ObjectSerializer(typeResolver, type); - } - if (serializerClass == ArraySerializers.ObjectArraySerializer.class) { - return (Serializer) new ArraySerializers.ObjectArraySerializer(typeResolver, type); - } - if (serializerClass == ObjectStreamSerializer.class) { - return new ObjectStreamSerializer(typeResolver, type); - } - if (serializerClass == ExceptionSerializers.ExceptionSerializer.class) { - return new ExceptionSerializers.ExceptionSerializer(typeResolver, type); - } - if (serializerClass == ExceptionSerializers.StackTraceElementSerializer.class) { - return (Serializer) new ExceptionSerializers.StackTraceElementSerializer(config); - } - if (serializerClass == CompatibleSerializer.class) { - TypeDef typeDef = typeResolver.getTypeDef(type, true); - return new CompatibleSerializer(typeResolver, type, typeDef); - } - if (serializerClass == EnumSerializer.class) { - return (Serializer) new EnumSerializer(config, type); - } - if (serializerClass == LambdaSerializer.class) { - return new LambdaSerializer(typeResolver, type); - } - if (serializerClass == JdkProxySerializer.class) { - return new JdkProxySerializer(typeResolver, type); - } - if (serializerClass == ReplaceResolveSerializer.class) { - return new ReplaceResolveSerializer(typeResolver, type); - } - if (serializerClass == ExternalizableSerializer.class) { - return new ExternalizableSerializer(typeResolver, type); - } - if (serializerClass == LazyInitBeanSerializer.class) { - return new LazyInitBeanSerializer(typeResolver, type); - } - if (serializerClass == TimeSerializers.CalendarSerializer.class) { - return (Serializer) new TimeSerializers.CalendarSerializer(config, type); - } - if (serializerClass == TimeSerializers.ZoneIdSerializer.class) { - return (Serializer) new TimeSerializers.ZoneIdSerializer(config, type); - } - if (serializerClass == TimeSerializers.TimeZoneSerializer.class) { - return (Serializer) new TimeSerializers.TimeZoneSerializer(config, type); - } - if (serializerClass == BufferSerializers.ByteBufferSerializer.class) { - return (Serializer) new BufferSerializers.ByteBufferSerializer(typeResolver, type); - } - if (serializerClass == CharsetSerializer.class) { - return new CharsetSerializer(config, type); - } - if (serializerClass == CollectionSerializers.EnumSetSerializer.class) { - return (Serializer) new CollectionSerializers.EnumSetSerializer(typeResolver, type); - } - if (serializerClass == CollectionSerializer.class) { - return new CollectionSerializer(typeResolver, type); - } - if (serializerClass == CollectionSerializers.DefaultJavaCollectionSerializer.class) { - return new CollectionSerializers.DefaultJavaCollectionSerializer(typeResolver, type); - } - if (serializerClass == CollectionSerializers.JDKCompatibleCollectionSerializer.class) { - return new CollectionSerializers.JDKCompatibleCollectionSerializer(typeResolver, type); - } - if (serializerClass == MapSerializer.class) { - return new MapSerializer(typeResolver, type); - } - if (serializerClass == MapSerializers.DefaultJavaMapSerializer.class) { - return new MapSerializers.DefaultJavaMapSerializer(typeResolver, type); - } - if (serializerClass == MapSerializers.JDKCompatibleMapSerializer.class) { - return new MapSerializers.JDKCompatibleMapSerializer(typeResolver, type); - } - if (serializerClass == ChildContainerSerializers.ChildCollectionSerializer.class) { - return new ChildContainerSerializers.ChildCollectionSerializer(typeResolver, type); - } - if (serializerClass == ChildContainerSerializers.ChildArrayListSerializer.class) { - return new ChildContainerSerializers.ChildArrayListSerializer(typeResolver, type); - } - if (serializerClass == ChildContainerSerializers.ChildMapSerializer.class) { - return new ChildContainerSerializers.ChildMapSerializer(typeResolver, type); - } - if (serializerClass == ChildContainerSerializers.ChildSortedSetSerializer.class) { - return new ChildContainerSerializers.ChildSortedSetSerializer(typeResolver, type); - } - if (serializerClass == ChildContainerSerializers.ChildPriorityQueueSerializer.class) { - return new ChildContainerSerializers.ChildPriorityQueueSerializer(typeResolver, type); - } - if (serializerClass == ChildContainerSerializers.ChildSortedMapSerializer.class) { - return new ChildContainerSerializers.ChildSortedMapSerializer(typeResolver, type); - } - if (serializerClass == SingletonCollectionSerializer.class) { - return new SingletonCollectionSerializer(typeResolver, type); - } - if (serializerClass == SingletonMapSerializer.class) { - return new SingletonMapSerializer(typeResolver, type); - } - if (serializerClass == SingletonObjectSerializer.class) { - return new SingletonObjectSerializer(typeResolver, type); - } - return null; - } - - private static Serializer createSerializer( - TypeResolver typeResolver, Class type, Class serializerClass) { - if (AndroidSupport.IS_ANDROID) { - return createSerializerReflectively(typeResolver, type, serializerClass); - } - try { - Config config = typeResolver.getConfig(); - try { - MethodHandle ctr = findConstructor(serializerClass, SIG1); - CTR_MAP.put(serializerClass, Tuple2.of(SIG1, ctr)); - return (Serializer) ctr.invoke(typeResolver, type); - } catch (NoSuchMethodException e) { - ExceptionUtils.ignore(e); - } - try { - MethodHandle ctr = findConstructor(serializerClass, SIG2); - CTR_MAP.put(serializerClass, Tuple2.of(SIG2, ctr)); - return (Serializer) ctr.invoke(typeResolver); - } catch (NoSuchMethodException e) { - ExceptionUtils.ignore(e); - } - try { - MethodHandle ctr = findConstructor(serializerClass, SIG3); - CTR_MAP.put(serializerClass, Tuple2.of(SIG3, ctr)); - return (Serializer) ctr.invoke(config, type); - } catch (NoSuchMethodException e) { - ExceptionUtils.ignore(e); - } - try { - MethodHandle ctr = findConstructor(serializerClass, SIG4); - CTR_MAP.put(serializerClass, Tuple2.of(SIG4, ctr)); - return (Serializer) ctr.invoke(config); - } catch (NoSuchMethodException e) { - ExceptionUtils.ignore(e); - } - try { - MethodHandle ctr = findConstructor(serializerClass, SIG5); - CTR_MAP.put(serializerClass, Tuple2.of(SIG5, ctr)); - return (Serializer) ctr.invoke(type); - } catch (NoSuchMethodException e) { - ExceptionUtils.ignore(e); - } - 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"); - } - } - - private static MethodHandle findConstructor(Class cls, MethodType sig) - throws NoSuchMethodException, IllegalAccessException { - if (Modifier.isPublic(cls.getModifiers())) { - try { - return MethodHandles.publicLookup().findConstructor(cls, sig); - } catch (IllegalAccessException ignored) { - // The class may be public in a non-exported package. Fall back to the private lookup path so - // named-module users can still enable access with opens. - } - } - return _JDKAccess._trustedLookup(cls).findConstructor(cls, sig); - } - - private static Serializer createSerializerReflectively( - TypeResolver typeResolver, Class type, Class serializerClass) { - Config config = typeResolver.getConfig(); - try { - Constructor ctr = - serializerClass.getDeclaredConstructor(TypeResolver.class, Class.class); - ctr.setAccessible(true); - return (Serializer) ctr.newInstance(typeResolver, type); - } catch (NoSuchMethodException e) { - ExceptionUtils.ignore(e); - } catch (Throwable t) { - ExceptionUtils.throwException(t); - throw new IllegalStateException("unreachable"); - } - try { - Constructor ctr = - serializerClass.getDeclaredConstructor(TypeResolver.class); - ctr.setAccessible(true); - return (Serializer) ctr.newInstance(typeResolver); - } catch (NoSuchMethodException e) { - ExceptionUtils.ignore(e); - } catch (Throwable t) { - ExceptionUtils.throwException(t); - throw new IllegalStateException("unreachable"); - } - try { - Constructor ctr = - serializerClass.getDeclaredConstructor(Config.class, Class.class); - ctr.setAccessible(true); - return (Serializer) ctr.newInstance(config, type); - } catch (NoSuchMethodException e) { - ExceptionUtils.ignore(e); - } catch (Throwable t) { - ExceptionUtils.throwException(t); - throw new IllegalStateException("unreachable"); - } - try { - Constructor ctr = serializerClass.getDeclaredConstructor(Config.class); - ctr.setAccessible(true); - return (Serializer) ctr.newInstance(config); - } catch (NoSuchMethodException e) { - ExceptionUtils.ignore(e); - } catch (Throwable t) { - ExceptionUtils.throwException(t); - throw new IllegalStateException("unreachable"); - } - try { - Constructor ctr = serializerClass.getDeclaredConstructor(Class.class); - ctr.setAccessible(true); - return (Serializer) ctr.newInstance(type); - } catch (NoSuchMethodException e) { - ExceptionUtils.ignore(e); - } catch (Throwable t) { - ExceptionUtils.throwException(t); - throw new IllegalStateException("unreachable"); - } - try { - Constructor ctr = serializerClass.getDeclaredConstructor(); - ctr.setAccessible(true); - return (Serializer) ctr.newInstance(); - } catch (NoSuchMethodException e) { - throw new IllegalArgumentException( - "Serializer " - + serializerClass.getName() - + " doesn't define a supported constructor for " - + type, - e); - } catch (Throwable t) { - ExceptionUtils.throwException(t); - throw new IllegalStateException("unreachable"); - } - } - - public static void write(WriteContext writeContext, Serializer serializer, T obj) { - serializer.write(writeContext, obj); - } - - public static T read(ReadContext readContext, Serializer serializer) { - return serializer.read(readContext); - } - - private static final ToIntFunction GET_CODER; - private static final Function GET_VALUE; - - static { - if (AndroidSupport.IS_ANDROID) { - GET_VALUE = null; - GET_CODER = null; - } else { - Function getValue; - ToIntFunction getCoder; - try { - getValue = (Function) makeGetterFunction(StringBuilder.class.getSuperclass(), "getValue"); - } catch (Throwable e) { - getValue = null; - } - try { - Method getCoderMethod = StringBuilder.class.getSuperclass().getDeclaredMethod("getCoder"); - getCoder = (ToIntFunction) makeGetterFunction(getCoderMethod, int.class); - } catch (NoSuchMethodException e) { - getCoder = null; - } catch (Throwable e) { - getCoder = null; - } - GET_VALUE = getValue; - GET_CODER = getCoder; - } - } - - public abstract static class AbstractStringBuilderSerializer - extends Serializer { - private final Config config; - - public AbstractStringBuilderSerializer(Config config, Class type) { - super(config, type); - this.config = config; - } - - @Override - public void write(WriteContext writeContext, T value) { - MemoryBuffer buffer = writeContext.getBuffer(); - StringSerializer stringSerializer = writeContext.getStringSerializer(); - if (config.isXlang()) { - stringSerializer.writeString(buffer, value.toString()); - return; - } - if (AndroidSupport.IS_ANDROID) { - stringSerializer.writeString(buffer, value.toString()); - return; - } - if (GET_VALUE == null) { - stringSerializer.writeString(buffer, value.toString()); - return; - } - if (GET_CODER != null) { - int coder = GET_CODER.applyAsInt(value); - byte[] v = (byte[]) GET_VALUE.apply(value); - int bytesLen = value.length(); - if (coder != 0) { - if (coder != 1) { - throw new UnsupportedOperationException("Unsupported coder " + coder); - } - bytesLen <<= 1; - } - long header = ((long) bytesLen << 2) | coder; - buffer.writeVarUInt64(header); - buffer.writeBytes(v, 0, bytesLen); - } else { - Object rawValue = GET_VALUE.apply(value); - if (!(rawValue instanceof char[])) { - stringSerializer.writeString(buffer, value.toString()); - return; - } - char[] v = (char[]) rawValue; - if (StringEncodingUtils.isLatin(v)) { - stringSerializer.writeCharsLatin1(buffer, v, value.length()); - } else { - stringSerializer.writeCharsUTF16(buffer, v, value.length()); - } - } - } - } - - public static final class StringBuilderSerializer - extends AbstractStringBuilderSerializer { - - public StringBuilderSerializer(Config config) { - super(config, StringBuilder.class); - } - - @Override - public StringBuilder copy(CopyContext copyContext, StringBuilder origin) { - return new StringBuilder(origin); - } - - @Override - public StringBuilder read(ReadContext readContext) { - return new StringBuilder(readContext.readString()); - } - } - - public static final class StringBufferSerializer - extends AbstractStringBuilderSerializer { - - public StringBufferSerializer(Config config) { - super(config, StringBuffer.class); - } - - @Override - public StringBuffer copy(CopyContext copyContext, StringBuffer origin) { - return new StringBuffer(origin); - } - - @Override - public StringBuffer read(ReadContext readContext) { - return new StringBuffer(readContext.readString()); - } - } - - 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 { - - public AtomicBooleanSerializer(Config config) { - super(config, AtomicBoolean.class); - } - - @Override - public void write(WriteContext writeContext, AtomicBoolean value) { - writeContext.getBuffer().writeBoolean(value.get()); - } - - @Override - public AtomicBoolean copy(CopyContext copyContext, AtomicBoolean origin) { - return new AtomicBoolean(origin.get()); - } - - @Override - public AtomicBoolean read(ReadContext readContext) { - return new AtomicBoolean(readContext.getBuffer().readBoolean()); - } - } - - public static final class AtomicIntegerSerializer extends Serializer - implements Shareable { - - public AtomicIntegerSerializer(Config config) { - super(config, AtomicInteger.class); - } - - @Override - public void write(WriteContext writeContext, AtomicInteger value) { - writeContext.getBuffer().writeInt32(value.get()); - } - - @Override - public AtomicInteger copy(CopyContext copyContext, AtomicInteger origin) { - return new AtomicInteger(origin.get()); - } - - @Override - public AtomicInteger read(ReadContext readContext) { - return new AtomicInteger(readContext.getBuffer().readInt32()); - } - } - - public static final class AtomicLongSerializer extends Serializer - implements Shareable { - - public AtomicLongSerializer(Config config) { - super(config, AtomicLong.class); - } - - @Override - public void write(WriteContext writeContext, AtomicLong value) { - writeContext.getBuffer().writeInt64(value.get()); - } - - @Override - public AtomicLong copy(CopyContext copyContext, AtomicLong origin) { - return new AtomicLong(origin.get()); - } - - @Override - public AtomicLong read(ReadContext readContext) { - return new AtomicLong(readContext.getBuffer().readInt64()); - } - } - - public static final class AtomicReferenceSerializer extends Serializer - implements Shareable { - - public AtomicReferenceSerializer(Config config) { - super(config, AtomicReference.class); - } - - @Override - public void write(WriteContext writeContext, AtomicReference value) { - writeContext.writeRef(value.get()); - } - - @Override - public AtomicReference copy(CopyContext copyContext, AtomicReference origin) { - return new AtomicReference(copyContext.copyObject(origin.get())); - } - - @Override - public AtomicReference read(ReadContext readContext) { - return new AtomicReference(readContext.readRef()); - } - } - - public static final class CurrencySerializer extends ImmutableSerializer - implements Shareable { - public CurrencySerializer(Config config) { - super(config, Currency.class); - } - - @Override - public void write(WriteContext writeContext, Currency object) { - writeContext.writeString(object.getCurrencyCode()); - } - - @Override - public Currency read(ReadContext readContext) { - return Currency.getInstance(readContext.readString()); - } - } - - /** Serializer for {@link Charset}. */ - public static final class CharsetSerializer extends ImmutableSerializer - implements Shareable { - public CharsetSerializer(Config config, Class type) { - super(config, type); - } - - public void write(WriteContext writeContext, T object) { - writeContext.writeString(object.name()); - } - - public T read(ReadContext readContext) { - return (T) Charset.forName(readContext.readString()); - } - } - - public static final class URISerializer extends ImmutableSerializer - implements Shareable { - - public URISerializer(Config config) { - super(config, URI.class); - } - - @Override - public void write(WriteContext writeContext, final URI uri) { - writeContext.writeString(uri.toString()); - } - - @Override - public URI read(ReadContext readContext) { - return URI.create(readContext.readString()); - } - } - - public static final class RegexSerializer extends ImmutableSerializer - implements Shareable { - public RegexSerializer(Config config) { - super(config, Pattern.class); - } - - @Override - public void write(WriteContext writeContext, Pattern pattern) { - MemoryBuffer buffer = writeContext.getBuffer(); - writeContext.writeString(pattern.pattern()); - buffer.writeInt32(pattern.flags()); - } - - @Override - public Pattern read(ReadContext readContext) { - MemoryBuffer buffer = readContext.getBuffer(); - String regex = readContext.readString(); - int flags = buffer.readInt32(); - return Pattern.compile(regex, flags); - } - } - - public static final class UUIDSerializer extends ImmutableSerializer implements Shareable { - - public UUIDSerializer(Config config) { - super(config, UUID.class); - } - - @Override - public void write(WriteContext writeContext, final UUID uuid) { - MemoryBuffer buffer = writeContext.getBuffer(); - buffer.writeInt64(uuid.getMostSignificantBits()); - buffer.writeInt64(uuid.getLeastSignificantBits()); - } - - @Override - public UUID read(ReadContext readContext) { - MemoryBuffer buffer = readContext.getBuffer(); - return new UUID(buffer.readInt64(), buffer.readInt64()); - } - } - - public static final class ClassSerializer extends ImmutableSerializer - implements Shareable { - public ClassSerializer(Config config) { - super(config, Class.class); - } - - @Override - public void write(WriteContext writeContext, Class value) { - ((ClassResolver) writeContext.getTypeResolver()).writeClassInternal(writeContext, value); - } - - @Override - public Class read(ReadContext readContext) { - return ((ClassResolver) readContext.getTypeResolver()).readClassInternal(readContext); - } - } - - /** - * Serializer for empty object of type {@link Object}. Fory disabled serialization for jdk - * internal types which doesn't implement {@link java.io.Serializable} for security, but empty - * object is safe and used sometimes, so fory should support its serialization without disable - * serializable or class registration checks. - */ - // Use a separate serializer to avoid codegen for empty object. - public static final class EmptyObjectSerializer extends ImmutableSerializer - implements Shareable { - - public EmptyObjectSerializer(Config config) { - super(config, Object.class); - } - - @Override - public void write(WriteContext writeContext, Object value) {} - - @Override - public Object read(ReadContext readContext) { - return new Object(); - } - } - - public static void registerDefaultSerializers(TypeResolver resolver) { - Config config = resolver.getConfig(); - 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)); - resolver.registerInternalSerializer(AtomicInteger.class, new AtomicIntegerSerializer(config)); - resolver.registerInternalSerializer(AtomicLong.class, new AtomicLongSerializer(config)); - resolver.registerInternalSerializer( - AtomicReference.class, new AtomicReferenceSerializer(config)); - resolver.registerInternalSerializer(Currency.class, new CurrencySerializer(config)); - resolver.registerInternalSerializer(URI.class, new URISerializer(config)); - resolver.registerInternalSerializer(Pattern.class, new RegexSerializer(config)); - resolver.registerInternalSerializer(UUID.class, new UUIDSerializer(config)); - resolver.registerInternalSerializer(Object.class, new EmptyObjectSerializer(config)); - } -} From 1daa83ec1ed3a293c4a666e6ad1434fc9a2e643c Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 24 May 2026 18:00:33 +0800 Subject: [PATCH 28/34] docs(java): update final field mutation guidance --- docs/guide/java/troubleshooting.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/guide/java/troubleshooting.md b/docs/guide/java/troubleshooting.md index f4837e211c..a28518ffa7 100644 --- a/docs/guide/java/troubleshooting.md +++ b/docs/guide/java/troubleshooting.md @@ -183,10 +183,15 @@ For example, direct `ByteBuffer` wrapping on the module path requires: --add-opens=java.base/java.nio=ALL-UNNAMED,org.apache.fory.core,org.apache.fory.format ``` -Normal classes with final instance fields need a constructor that covers those final fields when -Unsafe allocation is denied. Annotate the constructor with -`java.beans.ConstructorProperties`, or compile the class with `-parameters` so Fory can bind -constructor parameters to fields. Non-final fields can still be restored after construction. +Normal classes with final instance fields require final-field mutation to be enabled for Fory core +when Unsafe allocation is denied: + +```bash +--enable-final-field-mutation=org.apache.fory.core +``` + +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 From 7272e5db25ee8c71be07c5f7422a7cfeea2f1082 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 24 May 2026 18:30:05 +0800 Subject: [PATCH 29/34] fix(java): restore final fields with method handles on JDK25 --- .../apache/fory/reflect/ObjectCreators.java | 64 +---- .../apache/fory/reflect/FieldAccessor.java | 240 +++++++++++++----- .../fory/reflect/FieldAccessorTest.java | 24 ++ .../fory/serializer/ObjectSerializerTest.java | 109 ++++++++ 4 files changed, 310 insertions(+), 127 deletions(-) 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 a9aa8d2da9..682a9546be 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 @@ -28,7 +28,6 @@ import java.lang.reflect.Modifier; import java.lang.reflect.Parameter; import java.util.ArrayList; -import java.util.Arrays; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -110,9 +109,6 @@ private static ObjectCreator creategetObjectCreator(Class type) { if (JdkVersion.MAJOR_VERSION >= 25 && noArgConstructor == null) { return new ConstructorObjectCreator<>(type); } - if (JdkVersion.MAJOR_VERSION >= 25 && hasFinalFields(type)) { - return new ConstructorObjectCreator<>(type); - } if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { if (noArgConstructor != null) { return new DeclaredNoArgCtrObjectCreator<>(type); @@ -126,15 +122,6 @@ private static ObjectCreator creategetObjectCreator(Class type) { return new DeclaredNoArgCtrObjectCreator<>(type); } - private static boolean hasFinalFields(Class type) { - for (Field field : Descriptor.getFields(type)) { - if (Modifier.isFinal(field.getModifiers())) { - return true; - } - } - return false; - } - public static boolean supportsJdk25Creation(Class type) { if (JdkVersion.MAJOR_VERSION < 25 || RecordUtils.isRecord(type)) { return true; @@ -179,7 +166,6 @@ private static ConstructorMatch findConstructor(Class type) { Map fieldsById = new LinkedHashMap<>(); Set duplicateNames = new LinkedHashSet<>(); Set duplicateIds = new LinkedHashSet<>(); - Set finalFields = new LinkedHashSet<>(); for (Field field : fields) { fieldsByNameList.computeIfAbsent(field.getName(), name -> new ArrayList<>()).add(field); Field previous = fieldsByName.put(field.getName(), field); @@ -193,9 +179,6 @@ private static ConstructorMatch findConstructor(Class type) { duplicateIds.add(foryField.id()); } } - if (Modifier.isFinal(field.getModifiers())) { - finalFields.add(field); - } } ConstructorMatch best = null; for (Constructor constructor : type.getDeclaredConstructors()) { @@ -210,20 +193,15 @@ private static ConstructorMatch findConstructor(Class type) { fieldsByNameList, fieldsById, duplicateNames, - duplicateIds, - finalFields); + duplicateIds); if (match != null && (best == null || match.score > best.score)) { best = match; } } if (best == null) { - String requirement = - finalFields.isEmpty() - ? "a bindable constructor because no no-arg constructor is available" - : "a constructor covering final fields " + finalFields; throw new ForyException( "JDK25 zero-Unsafe mode requires " - + requirement + + "a bindable constructor because no no-arg constructor is available" + " for " + type + ". Annotate the constructor with java.beans.ConstructorProperties or compile " @@ -239,22 +217,18 @@ private static ConstructorMatch matchConstructor( Map> fieldsByNameList, Map fieldsById, Set duplicateNames, - Set duplicateIds, - Set finalFields) { + Set duplicateIds) { Field[] fields = constructorFields( constructor, fieldsByName, fieldsByNameList, fieldsById, duplicateNames, duplicateIds); if (fields == null) { return null; } - return matchConstructorFields(constructor, finalFields, fields); + return matchConstructorFields(constructor, fields); } private static ConstructorMatch matchConstructorFields( - Constructor constructor, Set finalFields, Field[] fields) { - if (!containsAllFinalFields(finalFields, fields)) { - return null; - } + Constructor constructor, Field[] fields) { Class[] parameterTypes = constructor.getParameterTypes(); String[] names = new String[fields.length]; Class[] declaringClasses = new Class[fields.length]; @@ -274,34 +248,6 @@ private static ConstructorMatch matchConstructorFields( constructor, names, declaringClasses, fieldTypes, finalFieldFlags, 300 - fields.length); } - private static boolean containsAllFinalFields(Set finalFields, Field[] fields) { - Set selectedFields = new LinkedHashSet<>(Arrays.asList(fields)); - for (Field finalField : finalFields) { - if (selectedFields.contains(finalField)) { - continue; - } - if (coveredBySyntheticField(finalField, selectedFields)) { - continue; - } - return false; - } - return true; - } - - private static boolean coveredBySyntheticField(Field finalField, Set selectedFields) { - if (!finalField.isSynthetic()) { - return false; - } - for (Field selectedField : selectedFields) { - if (selectedField.isSynthetic() - && selectedField.getName().equals(finalField.getName()) - && selectedField.getType() == finalField.getType()) { - return true; - } - } - return false; - } - private static Field[] constructorFields( Constructor constructor, Map fieldsByName, 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 index 82c8cc21e9..1c06e64634 100644 --- 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 @@ -407,6 +407,16 @@ private static UnsupportedOperationException unsupportedWrite(Field field, Throw "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); } @@ -450,102 +460,124 @@ public void set(Object obj, Object value) { 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()); } - protected void setReflectively(Object obj, Object value, Throwable cause) { + private static MethodHandle createFinalSetter(Field field) { try { - prepareReflectiveWrite(cause); - field.set(target(obj), value); + field.setAccessible(true); + return privateLookup(field).unreflectSetter(field); } catch (IllegalAccessException | RuntimeException e) { - throw unsupportedWrite(field, e); + throw finalMutationFailure(field, e); } } - private Object target(Object obj) { - return isStatic ? null : obj; + 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; } - private void prepareReflectiveWrite(Throwable cause) { - if (field.getDeclaringClass().getName().startsWith("java.")) { - throw unsupportedWrite(field, cause); + 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); } - field.setAccessible(true); } - protected void setBooleanReflectively(Object obj, boolean value, Throwable cause) { + protected void setFinalBoolean(Object obj, boolean value, Throwable cause) { + MethodHandle setter = finalSetter(cause); + checkObj(obj); try { - prepareReflectiveWrite(cause); - field.setBoolean(target(obj), value); - } catch (IllegalAccessException | RuntimeException e) { - throw unsupportedWrite(field, e); + setter.invoke(obj, value); + } catch (Throwable e) { + throw finalMutationFailure(field, e); } } - protected void setByteReflectively(Object obj, byte value, Throwable cause) { + protected void setFinalByte(Object obj, byte value, Throwable cause) { + MethodHandle setter = finalSetter(cause); + checkObj(obj); try { - prepareReflectiveWrite(cause); - field.setByte(target(obj), value); - } catch (IllegalAccessException | RuntimeException e) { - throw unsupportedWrite(field, e); + setter.invoke(obj, value); + } catch (Throwable e) { + throw finalMutationFailure(field, e); } } - protected void setCharReflectively(Object obj, char value, Throwable cause) { + protected void setFinalChar(Object obj, char value, Throwable cause) { + MethodHandle setter = finalSetter(cause); + checkObj(obj); try { - prepareReflectiveWrite(cause); - field.setChar(target(obj), value); - } catch (IllegalAccessException | RuntimeException e) { - throw unsupportedWrite(field, e); + setter.invoke(obj, value); + } catch (Throwable e) { + throw finalMutationFailure(field, e); } } - protected void setShortReflectively(Object obj, short value, Throwable cause) { + protected void setFinalShort(Object obj, short value, Throwable cause) { + MethodHandle setter = finalSetter(cause); + checkObj(obj); try { - prepareReflectiveWrite(cause); - field.setShort(target(obj), value); - } catch (IllegalAccessException | RuntimeException e) { - throw unsupportedWrite(field, e); + setter.invoke(obj, value); + } catch (Throwable e) { + throw finalMutationFailure(field, e); } } - protected void setIntReflectively(Object obj, int value, Throwable cause) { + protected void setFinalInt(Object obj, int value, Throwable cause) { + MethodHandle setter = finalSetter(cause); + checkObj(obj); try { - prepareReflectiveWrite(cause); - field.setInt(target(obj), value); - } catch (IllegalAccessException | RuntimeException e) { - throw unsupportedWrite(field, e); + setter.invoke(obj, value); + } catch (Throwable e) { + throw finalMutationFailure(field, e); } } - protected void setLongReflectively(Object obj, long value, Throwable cause) { + protected void setFinalLong(Object obj, long value, Throwable cause) { + MethodHandle setter = finalSetter(cause); + checkObj(obj); try { - prepareReflectiveWrite(cause); - field.setLong(target(obj), value); - } catch (IllegalAccessException | RuntimeException e) { - throw unsupportedWrite(field, e); + setter.invoke(obj, value); + } catch (Throwable e) { + throw finalMutationFailure(field, e); } } - protected void setFloatReflectively(Object obj, float value, Throwable cause) { + protected void setFinalFloat(Object obj, float value, Throwable cause) { + MethodHandle setter = finalSetter(cause); + checkObj(obj); try { - prepareReflectiveWrite(cause); - field.setFloat(target(obj), value); - } catch (IllegalAccessException | RuntimeException e) { - throw unsupportedWrite(field, e); + setter.invoke(obj, value); + } catch (Throwable e) { + throw finalMutationFailure(field, e); } } - protected void setDoubleReflectively(Object obj, double value, Throwable cause) { + protected void setFinalDouble(Object obj, double value, Throwable cause) { + MethodHandle setter = finalSetter(cause); + checkObj(obj); try { - prepareReflectiveWrite(cause); - field.setDouble(target(obj), value); - } catch (IllegalAccessException | RuntimeException e) { - throw unsupportedWrite(field, e); + setter.invoke(obj, value); + } catch (Throwable e) { + throw finalMutationFailure(field, e); } } } @@ -578,6 +610,10 @@ public void set(Object obj, Object value) { @Override public void putBoolean(Object obj, boolean value) { + if (isFinal) { + setFinalBoolean(obj, value, null); + return; + } try { if (isStatic) { handle.set(value); @@ -586,7 +622,7 @@ public void putBoolean(Object obj, boolean value) { handle.set(obj, value); } } catch (UnsupportedOperationException e) { - setBooleanReflectively(obj, value, e); + setFinalBoolean(obj, value, e); } } } @@ -656,6 +692,10 @@ public void set(Object obj, Object value) { @Override public void putByte(Object obj, byte value) { + if (isFinal) { + setFinalByte(obj, value, null); + return; + } try { if (isStatic) { handle.set(value); @@ -664,7 +704,7 @@ public void putByte(Object obj, byte value) { handle.set(obj, value); } } catch (UnsupportedOperationException e) { - setByteReflectively(obj, value, e); + setFinalByte(obj, value, e); } } } @@ -735,6 +775,10 @@ public void set(Object obj, Object value) { @Override public void putChar(Object obj, char value) { + if (isFinal) { + setFinalChar(obj, value, null); + return; + } try { if (isStatic) { handle.set(value); @@ -743,7 +787,7 @@ public void putChar(Object obj, char value) { handle.set(obj, value); } } catch (UnsupportedOperationException e) { - setCharReflectively(obj, value, e); + setFinalChar(obj, value, e); } } } @@ -813,6 +857,10 @@ public void set(Object obj, Object value) { @Override public void putShort(Object obj, short value) { + if (isFinal) { + setFinalShort(obj, value, null); + return; + } try { if (isStatic) { handle.set(value); @@ -821,7 +869,7 @@ public void putShort(Object obj, short value) { handle.set(obj, value); } } catch (UnsupportedOperationException e) { - setShortReflectively(obj, value, e); + setFinalShort(obj, value, e); } } } @@ -891,6 +939,10 @@ public void set(Object obj, Object value) { @Override public void putInt(Object obj, int value) { + if (isFinal) { + setFinalInt(obj, value, null); + return; + } try { if (isStatic) { handle.set(value); @@ -899,7 +951,7 @@ public void putInt(Object obj, int value) { handle.set(obj, value); } } catch (UnsupportedOperationException e) { - setIntReflectively(obj, value, e); + setFinalInt(obj, value, e); } } } @@ -969,6 +1021,10 @@ public void set(Object obj, Object value) { @Override public void putLong(Object obj, long value) { + if (isFinal) { + setFinalLong(obj, value, null); + return; + } try { if (isStatic) { handle.set(value); @@ -977,7 +1033,7 @@ public void putLong(Object obj, long value) { handle.set(obj, value); } } catch (UnsupportedOperationException e) { - setLongReflectively(obj, value, e); + setFinalLong(obj, value, e); } } } @@ -1047,6 +1103,10 @@ public void set(Object obj, Object value) { @Override public void putFloat(Object obj, float value) { + if (isFinal) { + setFinalFloat(obj, value, null); + return; + } try { if (isStatic) { handle.set(value); @@ -1055,7 +1115,7 @@ public void putFloat(Object obj, float value) { handle.set(obj, value); } } catch (UnsupportedOperationException e) { - setFloatReflectively(obj, value, e); + setFinalFloat(obj, value, e); } } } @@ -1125,6 +1185,10 @@ public void set(Object obj, Object value) { @Override public void putDouble(Object obj, double value) { + if (isFinal) { + setFinalDouble(obj, value, null); + return; + } try { if (isStatic) { handle.set(value); @@ -1133,7 +1197,7 @@ public void putDouble(Object obj, double value) { handle.set(obj, value); } } catch (UnsupportedOperationException e) { - setDoubleReflectively(obj, value, e); + setFinalDouble(obj, value, e); } } } @@ -1193,6 +1257,10 @@ public Object get(Object obj) { @Override public void set(Object obj, Object value) { + if (isFinal) { + setFinal(obj, value, null); + return; + } try { if (isStatic) { handle.set(value); @@ -1201,7 +1269,7 @@ public void set(Object obj, Object value) { handle.set(obj, value); } } catch (UnsupportedOperationException e) { - setReflectively(obj, value, e); + setFinal(obj, value, e); } } } @@ -1289,6 +1357,10 @@ public Object get(Object obj) { @Override public void set(Object obj, Object value) { + if (isFinal) { + setFinal(obj, value, null); + return; + } try { if (isStatic) { handle.set(value); @@ -1297,7 +1369,7 @@ public void set(Object obj, Object value) { handle.set(obj, value); } } catch (UnsupportedOperationException e) { - setReflectively(obj, value, e); + setFinal(obj, value, e); } catch (RuntimeException e) { throw accessorFailure(field, e); } @@ -1318,6 +1390,10 @@ public boolean getBoolean(Object obj) { @Override public void putBoolean(Object obj, boolean value) { + if (isFinal) { + setFinalBoolean(obj, value, null); + return; + } try { if (isStatic) { handle.set(value); @@ -1326,7 +1402,7 @@ public void putBoolean(Object obj, boolean value) { handle.set(obj, value); } } catch (UnsupportedOperationException e) { - setBooleanReflectively(obj, value, e); + setFinalBoolean(obj, value, e); } catch (RuntimeException e) { throw accessorFailure(field, e); } @@ -1347,6 +1423,10 @@ public byte getByte(Object obj) { @Override public void putByte(Object obj, byte value) { + if (isFinal) { + setFinalByte(obj, value, null); + return; + } try { if (isStatic) { handle.set(value); @@ -1355,7 +1435,7 @@ public void putByte(Object obj, byte value) { handle.set(obj, value); } } catch (UnsupportedOperationException e) { - setByteReflectively(obj, value, e); + setFinalByte(obj, value, e); } catch (RuntimeException e) { throw accessorFailure(field, e); } @@ -1376,6 +1456,10 @@ public char getChar(Object obj) { @Override public void putChar(Object obj, char value) { + if (isFinal) { + setFinalChar(obj, value, null); + return; + } try { if (isStatic) { handle.set(value); @@ -1384,7 +1468,7 @@ public void putChar(Object obj, char value) { handle.set(obj, value); } } catch (UnsupportedOperationException e) { - setCharReflectively(obj, value, e); + setFinalChar(obj, value, e); } catch (RuntimeException e) { throw accessorFailure(field, e); } @@ -1405,6 +1489,10 @@ public short getShort(Object obj) { @Override public void putShort(Object obj, short value) { + if (isFinal) { + setFinalShort(obj, value, null); + return; + } try { if (isStatic) { handle.set(value); @@ -1413,7 +1501,7 @@ public void putShort(Object obj, short value) { handle.set(obj, value); } } catch (UnsupportedOperationException e) { - setShortReflectively(obj, value, e); + setFinalShort(obj, value, e); } catch (RuntimeException e) { throw accessorFailure(field, e); } @@ -1434,6 +1522,10 @@ public int getInt(Object obj) { @Override public void putInt(Object obj, int value) { + if (isFinal) { + setFinalInt(obj, value, null); + return; + } try { if (isStatic) { handle.set(value); @@ -1442,7 +1534,7 @@ public void putInt(Object obj, int value) { handle.set(obj, value); } } catch (UnsupportedOperationException e) { - setIntReflectively(obj, value, e); + setFinalInt(obj, value, e); } catch (RuntimeException e) { throw accessorFailure(field, e); } @@ -1463,6 +1555,10 @@ public long getLong(Object obj) { @Override public void putLong(Object obj, long value) { + if (isFinal) { + setFinalLong(obj, value, null); + return; + } try { if (isStatic) { handle.set(value); @@ -1471,7 +1567,7 @@ public void putLong(Object obj, long value) { handle.set(obj, value); } } catch (UnsupportedOperationException e) { - setLongReflectively(obj, value, e); + setFinalLong(obj, value, e); } catch (RuntimeException e) { throw accessorFailure(field, e); } @@ -1492,6 +1588,10 @@ public float getFloat(Object obj) { @Override public void putFloat(Object obj, float value) { + if (isFinal) { + setFinalFloat(obj, value, null); + return; + } try { if (isStatic) { handle.set(value); @@ -1500,7 +1600,7 @@ public void putFloat(Object obj, float value) { handle.set(obj, value); } } catch (UnsupportedOperationException e) { - setFloatReflectively(obj, value, e); + setFinalFloat(obj, value, e); } catch (RuntimeException e) { throw accessorFailure(field, e); } @@ -1521,6 +1621,10 @@ public double getDouble(Object obj) { @Override public void putDouble(Object obj, double value) { + if (isFinal) { + setFinalDouble(obj, value, null); + return; + } try { if (isStatic) { handle.set(value); @@ -1529,7 +1633,7 @@ public void putDouble(Object obj, double value) { handle.set(obj, value); } } catch (UnsupportedOperationException e) { - setDoubleReflectively(obj, value, e); + setFinalDouble(obj, value, e); } catch (RuntimeException e) { throw accessorFailure(field, e); } 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 c86faa6bf5..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 @@ -90,6 +90,20 @@ public void testHiddenAccessor() throws Exception { } } + @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); } @@ -180,4 +194,14 @@ private static final class HiddenFields { 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/serializer/ObjectSerializerTest.java b/java/fory-core/src/test/java/org/apache/fory/serializer/ObjectSerializerTest.java index 8eb70dffa5..178a1cac9e 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 @@ -239,6 +239,115 @@ 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"); From d5098bc0a631b323be521daec3c98aac7ad858df Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 24 May 2026 19:06:25 +0800 Subject: [PATCH 30/34] refactor(java): fold JDK25 codec builders into root --- .../android/AndroidForyRuntimeScenarios.java | 8 +- java/fory-core/pom.xml | 14 - .../org/apache/fory/builder/CodecBuilder.java | 69 +- .../fory/builder/ObjectCodecBuilder.java | 602 ++++++- .../org/apache/fory/memory/MemoryBuffer.java | 134 +- .../org/apache/fory/memory/MemoryOps.java | 102 ++ .../org/apache/fory/builder/CodecBuilder.java | 732 -------- .../fory/builder/ObjectCodecBuilder.java | 1465 ----------------- .../org/apache/fory/memory/MemoryBuffer.java | 101 +- .../apache/fory/memory/MemoryBufferTest.java | 69 +- .../fory/format/row/binary/BinaryArray.java | 14 +- .../fory/format/row/binary/BinaryMap.java | 7 +- 12 files changed, 1061 insertions(+), 2256 deletions(-) delete mode 100644 java/fory-core/src/main/java25/org/apache/fory/builder/CodecBuilder.java delete mode 100644 java/fory-core/src/main/java25/org/apache/fory/builder/ObjectCodecBuilder.java 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/java/fory-core/pom.xml b/java/fory-core/pom.xml index 63ee5f6c89..6e74e2c2a4 100644 --- a/java/fory-core/pom.xml +++ b/java/fory-core/pom.xml @@ -243,8 +243,6 @@ - - @@ -262,12 +260,6 @@ - - @@ -292,12 +284,6 @@ - - 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 5c598b72ce..3bcad7a701 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 @@ -58,6 +58,7 @@ import org.apache.fory.platform.GraalvmSupport; import org.apache.fory.platform.JdkVersion; import org.apache.fory.platform.UnsafeOps; +import org.apache.fory.reflect.FieldAccessor; import org.apache.fory.reflect.ObjectCreator; import org.apache.fory.reflect.ObjectCreators; import org.apache.fory.reflect.ReflectionUtils; @@ -310,6 +311,20 @@ private Expression reflectAccessField( private Expression unsafeAccessField( Expression inputObject, Class cls, Descriptor descriptor) { String fieldName = descriptor.getName(); + 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()) { @@ -345,7 +360,7 @@ private Expression fieldOffsetExpr(Class cls, Descriptor descriptor) { () -> { Expression classExpr = beanClassExpr(field.getDeclaringClass()); new Invoke(classExpr, "getDeclaredField", TypeRef.of(Field.class)); - Expression reflectFieldRef = getReflectField(cls, field, false); + Expression reflectFieldRef = getReflectField(field.getDeclaringClass(), field, false); return new StaticInvoke( UnsafeOps.class, "objectFieldOffset", PRIMITIVE_LONG_TYPE, reflectFieldRef) .inline(); @@ -355,6 +370,27 @@ private Expression fieldOffsetExpr(Class cls, Descriptor descriptor) { } } + 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}. @@ -423,6 +459,16 @@ 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 = fieldOffsetExpr(beanClass, descriptor); if (descriptor.getTypeRef().isPrimitive()) { @@ -442,7 +488,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"; } @@ -452,7 +502,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 = @@ -486,12 +540,15 @@ 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()); } 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 2220a53013..f23801f948 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; @@ -60,6 +64,7 @@ import org.apache.fory.logging.Logger; import org.apache.fory.logging.LoggerFactory; import org.apache.fory.meta.TypeDef; +import org.apache.fory.platform.JdkVersion; import org.apache.fory.platform.UnsafeOps; import org.apache.fory.reflect.ObjectCreator; import org.apache.fory.reflect.ObjectCreators; @@ -376,6 +381,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); @@ -469,6 +477,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; @@ -617,6 +628,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) @@ -869,6 +1198,7 @@ protected Expression createRecord(SortedMap recordComponent 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) { @@ -876,9 +1206,16 @@ protected Expression createConstructorObject(FieldsArray fieldValues) { } 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); - ObjectCreators.getObjectCreator(beanClass); // trigger cache and make error raised early Expression newInstance = new Invoke(getObjectCreator(beanClass), "newInstanceWithArguments", OBJECT_TYPE, args); return sourcePublicAccessible(beanClass) ? new Cast(newInstance, beanType) : newInstance; @@ -894,6 +1231,15 @@ protected Expression defaultConstructorValue(int constructorParameterIndex) { "constructorFieldClass" + constructorParameterIndex + "_")); } + private boolean constructorParamsAccessible() { + for (Class constructorFieldType : constructorFieldTypes) { + if (!sourcePublicAccessible(constructorFieldType)) { + return false; + } + } + return true; + } + private void addNonConstructorFieldSetters( ListExpression expressions, Expression bean, FieldsArray fieldValues) { for (Descriptor descriptor : objectCodecOptimizer.descriptorGrouper.getSortedDescriptors()) { @@ -1109,6 +1455,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); @@ -1213,6 +1562,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) { @@ -1360,6 +1712,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))); @@ -1372,4 +1965,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/memory/MemoryBuffer.java b/java/fory-core/src/main/java/org/apache/fory/memory/MemoryBuffer.java index 47d69b6ee2..743ccd32bc 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 @@ -3625,18 +3625,134 @@ 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); + UNSAFE.copyMemory( + heapMemory, + address + offset, + target, + UnsafeOps.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); + UNSAFE.copyMemory( + heapMemory, + address + offset, + target, + UnsafeOps.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); + UNSAFE.copyMemory( + heapMemory, + address + offset, + target, + UnsafeOps.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); + UNSAFE.copyMemory( + heapMemory, + address + offset, + target, + UnsafeOps.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); + UNSAFE.copyMemory( + heapMemory, + address + offset, + target, + UnsafeOps.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); + UNSAFE.copyMemory( + heapMemory, + address + offset, + target, + UnsafeOps.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); + UNSAFE.copyMemory( + heapMemory, + address + offset, + target, + UnsafeOps.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); + UNSAFE.copyMemory( + heapMemory, + address + offset, + target, + UnsafeOps.DOUBLE_ARRAY_OFFSET + arrayCopyOffset(targetOffset, 3), + 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; } /** 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..8b1879f250 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 @@ -1259,6 +1259,108 @@ 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); + } + } + + 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/java25/org/apache/fory/builder/CodecBuilder.java b/java/fory-core/src/main/java25/org/apache/fory/builder/CodecBuilder.java deleted file mode 100644 index b74b523c0f..0000000000 --- a/java/fory-core/src/main/java25/org/apache/fory/builder/CodecBuilder.java +++ /dev/null @@ -1,732 +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.builder; - -import static org.apache.fory.codegen.Expression.Invoke.inlineInvoke; -import static org.apache.fory.type.TypeUtils.CLASS_TYPE; -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_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; -import static org.apache.fory.type.TypeUtils.PRIMITIVE_VOID_TYPE; -import static org.apache.fory.type.TypeUtils.getRawType; - -import java.lang.invoke.MethodHandle; -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.function.Supplier; -import java.util.stream.Collectors; -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; -import org.apache.fory.codegen.Expression.Literal; -import org.apache.fory.codegen.Expression.Reference; -import org.apache.fory.codegen.Expression.StaticInvoke; -import org.apache.fory.collection.Tuple2; -import org.apache.fory.memory.MemoryBuffer; -import org.apache.fory.memory.NativeByteOrder; -import org.apache.fory.platform.GraalvmSupport; -import org.apache.fory.platform.UnsafeOps; -import org.apache.fory.reflect.FieldAccessor; -import org.apache.fory.reflect.ObjectCreator; -import org.apache.fory.reflect.ObjectCreators; -import org.apache.fory.reflect.ReflectionUtils; -import org.apache.fory.reflect.TypeRef; -import org.apache.fory.resolver.TypeInfo; -import org.apache.fory.resolver.TypeInfoHolder; -import org.apache.fory.type.Descriptor; -import org.apache.fory.util.Preconditions; -import org.apache.fory.util.StringUtils; -import org.apache.fory.util.function.Functions; -import org.apache.fory.util.record.RecordComponent; -import org.apache.fory.util.record.RecordUtils; - -/** - * Base builder for generating code to serialize java bean in row-format or object stream format. - * - *
    - * This builder has following requirements for the class of java bean: - *
  • public - *
  • For instance inner class, ignore outer class field. - *
  • For instance inner class, deserialized outer class field is null - *
- */ -@SuppressWarnings("UnstableApiUsage") -public abstract class CodecBuilder { - protected static final String ROOT_OBJECT_NAME = "_f_obj"; - static TypeRef objectArrayTypeRef = TypeRef.of(Object[].class); - static TypeRef bufferTypeRef = TypeRef.of(MemoryBuffer.class); - static TypeRef classInfoTypeRef = TypeRef.of(TypeInfo.class); - static TypeRef classInfoHolderTypeRef = TypeRef.of(TypeInfoHolder.class); - - protected final CodegenContext ctx; - protected final TypeRef beanType; - protected final Class beanClass; - protected final boolean isRecord; - protected final boolean isInterface; - private final Set duplicatedFields; - public static final Reference recordComponentDefaultValues = - new Reference("recordComponentDefaultValues", OBJECT_ARRAY_TYPE); - protected final Map fieldMap = new HashMap<>(); - protected boolean recordCtrAccessible; - - public CodecBuilder(CodegenContext ctx, TypeRef beanType) { - this.ctx = ctx; - this.beanType = beanType; - this.beanClass = getRawType(beanType); - isRecord = RecordUtils.isRecord(beanClass); - isInterface = beanClass.isInterface(); - if (isRecord) { - recordCtrAccessible = recordCtrAccessible(beanClass); - } - duplicatedFields = Descriptor.getSortedDuplicatedMembers(beanClass).keySet(); - // don't ctx.addImport beanClass, because it maybe causes name collide. - ctx.reserveName(ROOT_OBJECT_NAME); - // Don't import other packages to avoid class conflicts. - // For example user class named as `Date`/`List`/`MemoryBuffer` - // Skip Java reserved words since they can't be used as variable names anyway - // (e.g., Kotlin allows field names like "new" which are valid at bytecode level) - ReflectionUtils.getFields(beanType.getRawType(), true).stream() - .map(Field::getName) - .filter(name -> !CodegenContext.JAVA_RESERVED_WORDS.contains(name)) - .collect(Collectors.toSet()) - .forEach(ctx::reserveName); - } - - public abstract String codecClassName(Class cls); - - /** Generate codec class code. */ - public abstract String genCode(); - - /** Returns an expression that serialize java bean of type {@link CodecBuilder#beanClass}. */ - public abstract Expression buildEncodeExpression(); - - protected boolean sourcePublicAccessible(Class cls) { - return ctx.sourcePublicAccessible(cls); - } - - protected boolean fieldNullable(Descriptor descriptor) { - return false; - } - - protected Expression tryInlineCast(Expression expression, TypeRef targetType) { - return tryCastIfPublic(expression, targetType, true); - } - - protected Expression tryCastIfPublic(Expression expression, TypeRef targetType) { - return tryCastIfPublic(expression, targetType, false); - } - - protected Expression tryCastIfPublic( - Expression expression, TypeRef targetType, boolean inline) { - Class rawType = getRawType(targetType); - if (inline) { - if (sourcePublicAccessible(rawType)) { - return new Cast(expression, targetType); - } else { - return new Cast(expression, ReflectionUtils.getPublicSuperType(TypeRef.of(rawType))); - } - } - return tryCastIfPublic(expression, targetType, "castedValue"); - } - - protected Expression tryCastIfPublic( - Expression expression, TypeRef targetType, String valuePrefix) { - Class rawType = getRawType(targetType); - Class expressionRawType = getRawType(expression.type()); - // Source casts use erased Java types. Captured wildcard metadata can fail full generic subtype - // checks even when the emitted local variable already has an assignable raw type. - boolean rawTypeAlreadyAssignable = rawType.isAssignableFrom(expressionRawType); - if (sourcePublicAccessible(rawType) - && !expression.type().wrap().isSubtypeOf(targetType.wrap()) - && !rawTypeAlreadyAssignable) { - return new Cast(expression, targetType, valuePrefix); - } - if (rawType.isArray()) { - return new Cast(expression, OBJECT_ARRAY_TYPE, valuePrefix); - } - return expression; - } - - protected Reference getRecordCtrHandle() { - String fieldName = "_record_ctr_"; - Reference fieldRef = fieldMap.get(fieldName); - if (fieldRef == null) { - // trigger cache for graalvm - RecordUtils.getRecordCtrHandle(beanClass); - StaticInvoke getRecordCtrHandle = - new StaticInvoke( - RecordUtils.class, - "getRecordCtrHandle", - TypeRef.of(MethodHandle.class), - beanClassExpr()); - ctx.addField(ctx.type(MethodHandle.class), fieldName, getRecordCtrHandle); - fieldRef = new Reference(fieldName, TypeRef.of(MethodHandle.class)); - fieldMap.put(fieldName, fieldRef); - } - return fieldRef; - } - - protected Expression buildDefaultComponentsArray() { - return new StaticInvoke( - UnsafeOps.class, "copyObjectArray", OBJECT_ARRAY_TYPE, recordComponentDefaultValues); - } - - /** Returns an expression that get field value from bean. */ - protected Expression getFieldValue(Expression inputBeanExpr, Descriptor descriptor) { - TypeRef fieldType = descriptor.getTypeRef(); - Class rawType = descriptor.getRawType(); - String fieldName = descriptor.getName(); - boolean fieldNullable = fieldNullable(descriptor); - if (isInterface) { - return new Invoke(inputBeanExpr, descriptor.getName(), fieldName, fieldType, fieldNullable); - } - if (isRecord) { - return getRecordFieldValue(inputBeanExpr, descriptor); - } - if (duplicatedFields.contains(fieldName) || !Modifier.isPublic(beanClass.getModifiers())) { - return unsafeAccessField(inputBeanExpr, beanClass, descriptor); - } - if (!sourcePublicAccessible(rawType)) { - fieldType = OBJECT_TYPE; - } - // public field or non-private non-java field access field directly. - if (Modifier.isPublic(descriptor.getModifiers())) { - return new Expression.FieldValue(inputBeanExpr, fieldName, fieldType, fieldNullable, false); - } else if (descriptor.getReadMethod() != null - && Modifier.isPublic(descriptor.getReadMethod().getModifiers())) { - return new Invoke( - inputBeanExpr, descriptor.getReadMethod().getName(), fieldName, fieldType, fieldNullable); - } else { - if (!Modifier.isPrivate(descriptor.getModifiers())) { - if (AccessorHelper.defineAccessor(descriptor.getField())) { - return new StaticInvoke( - AccessorHelper.getAccessorClass(descriptor.getField()), - fieldName, - fieldType, - fieldNullable, - inputBeanExpr); - } - } - if (descriptor.getReadMethod() != null - && !Modifier.isPrivate(descriptor.getReadMethod().getModifiers())) { - if (AccessorHelper.defineAccessor(descriptor.getReadMethod())) { - return new StaticInvoke( - AccessorHelper.getAccessorClass(descriptor.getReadMethod()), - descriptor.getReadMethod().getName(), - fieldType, - fieldNullable, - inputBeanExpr); - } - } - return unsafeAccessField(inputBeanExpr, beanClass, descriptor); - } - } - - private Expression getRecordFieldValue(Expression inputBeanExpr, Descriptor descriptor) { - TypeRef fieldType = descriptor.getTypeRef(); - if (!sourcePublicAccessible(descriptor.getRawType())) { - fieldType = OBJECT_TYPE; - } - String fieldName = descriptor.getName(); - boolean fieldNullable = fieldNullable(descriptor); - if (Modifier.isPublic(beanClass.getModifiers())) { - Preconditions.checkNotNull(descriptor.getReadMethod()); - return new Invoke( - inputBeanExpr, descriptor.getReadMethod().getName(), fieldName, fieldType, fieldNullable); - } else { - String key = "_" + fieldName + "_getter_"; - Reference ref = fieldMap.get(key); - Tuple2, String> methodInfo = Functions.getterMethodInfo(descriptor.getRawType()); - if (ref == null) { - Class funcInterface = methodInfo.f0; - TypeRef getterType = TypeRef.of(funcInterface); - if (GraalvmSupport.isGraalBuildTime()) { - // generate getter ahead at native image build time. - Functions.makeGetterFunction(beanClass, fieldName); - } - Expression getter = - new StaticInvoke( - Functions.class, - "makeGetterFunction", - OBJECT_TYPE, - beanClassExpr(), - Literal.ofString(fieldName)); - getter = new Cast(getter, getterType); - ctx.addField(funcInterface, key, getter); - ref = new Reference(key, getterType); - fieldMap.put(key, ref); - } - if (!fieldType.isPrimitive()) { - Expression v = inlineInvoke(ref, methodInfo.f1, OBJECT_TYPE, fieldNullable, inputBeanExpr); - return tryCastIfPublic(v, descriptor.getTypeRef(), fieldName); - } else { - return new Invoke(ref, methodInfo.f1, fieldType, fieldNullable, inputBeanExpr); - } - } - } - - /** Returns an expression that get field value> from bean using reflection. */ - private Expression reflectAccessField( - Expression inputObject, Class cls, Descriptor descriptor) { - Reference fieldRef = getReflectField(cls, descriptor.getField()); - // boolean fieldNullable = !descriptor.getTypeToken().isPrimitive(); - Invoke getObj = - new Invoke(fieldRef, "get", OBJECT_TYPE, fieldNullable(descriptor), inputObject); - return new Cast(getObj, descriptor.getTypeRef(), descriptor.getName()); - } - - /** Returns an expression that get field value> from bean using `Unsafe`. */ - private Expression unsafeAccessField( - Expression inputObject, Class cls, Descriptor descriptor) { - String fieldName = descriptor.getName(); - Reference fieldAccessor = getFieldAccessor(cls, 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); - } - } - - private Reference getFieldAccessor(Class cls, 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}. - */ - public abstract Expression buildDecodeExpression(); - - /** Returns an expression that set field value to bean. */ - protected Expression setFieldValue(Expression bean, Descriptor d, Expression value) { - String fieldName = d.getName(); - if (value instanceof Inlineable) { - ((Inlineable) value).inline(); - } - if (duplicatedFields.contains(fieldName) || !sourcePublicAccessible(beanClass)) { - return unsafeSetField(bean, d, value); - } - if (!d.isFinalField() - && Modifier.isPublic(d.getModifiers()) - && Modifier.isPublic(d.getRawType().getModifiers())) { - if (!d.getRawType().isAssignableFrom(value.type().getRawType())) { - value = tryInlineCast(value, d.getTypeRef()); - } - return new Expression.SetField(bean, fieldName, value); - } else if (d.getWriteMethod() != null && Modifier.isPublic(d.getWriteMethod().getModifiers())) { - if (!d.getRawType().isAssignableFrom(value.type().getRawType())) { - value = tryInlineCast(value, d.getTypeRef()); - } - return new Invoke(bean, d.getWriteMethod().getName(), value); - } else { - if (!d.isFinalField() && !Modifier.isPrivate(d.getModifiers())) { - if (AccessorHelper.defineSetter(d.getField())) { - Class accessorClass = AccessorHelper.getAccessorClass(d.getField()); - if (!d.getRawType().isAssignableFrom(value.type().getRawType())) { - value = tryInlineCast(value, d.getTypeRef()); - } - return new StaticInvoke( - accessorClass, d.getName(), PRIMITIVE_VOID_TYPE, false, bean, value); - } - } - if (d.getWriteMethod() != null && !Modifier.isPrivate(d.getWriteMethod().getModifiers())) { - if (AccessorHelper.defineSetter(d.getWriteMethod())) { - Class accessorClass = AccessorHelper.getAccessorClass(d.getWriteMethod()); - if (!d.getRawType().isAssignableFrom(value.type().getRawType())) { - value = tryInlineCast(value, d.getTypeRef()); - } - return new StaticInvoke( - accessorClass, d.getWriteMethod().getName(), PRIMITIVE_VOID_TYPE, false, bean, value); - } - } - return unsafeSetField(bean, d, value); - } - } - - /** - * Returns an expression that set field value to bean using reflection. - */ - private Expression reflectSetField(Expression bean, Field field, Expression value) { - // Class maybe have getter, but don't have setter, so we can't rely on reflectAccessField to - // populate fieldMap - Reference fieldRef = getReflectField(getRawType(bean.type()), field); - Preconditions.checkNotNull(fieldRef); - return new Invoke(fieldRef, "set", bean, value); - } - - /** - * Returns an expression that set field value to bean using `Unsafe`. - */ - private Expression unsafeSetField(Expression bean, Descriptor descriptor, Expression value) { - TypeRef fieldType = descriptor.getTypeRef(); - Reference fieldAccessor = getFieldAccessor(beanClass, 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); - } - } - - private Reference getReflectField(Class cls, Field field) { - return getReflectField(cls, field, true); - } - - private Reference getReflectField(Class cls, Field field, boolean setAccessible) { - String fieldName = field.getName(); - String fieldRefName; - if (duplicatedFields.contains(fieldName)) { - fieldRefName = - field.getDeclaringClass().getName().replaceAll("\\.|\\$", "_") + "_" + fieldName + "_Field"; - } else { - fieldRefName = fieldName + "_Field"; - } - return getOrCreateField( - true, - Field.class, - fieldRefName, - () -> { - TypeRef fieldTypeRef = TypeRef.of(Field.class); - Class declaringClass = field.getDeclaringClass(); - Expression classExpr = - staticClassFieldExpr( - declaringClass, declaringClass.getName().replaceAll("\\.|\\$", "_") + "__class__"); - Expression fieldExpr; - if (GraalvmSupport.isGraalBuildTime()) { - fieldExpr = - inlineInvoke( - classExpr, "getDeclaredField", fieldTypeRef, Literal.ofString(fieldName)); - } else { - fieldExpr = - reflectionUtilsInvoke( - "getField", fieldTypeRef, classExpr, Literal.ofString(fieldName)); - } - if (!setAccessible) { - return fieldExpr; - } - Invoke setAccess = new Invoke(fieldExpr, "setAccessible", Literal.True); - return new ListExpression(setAccess, fieldExpr); - }); - } - - protected Reference getOrCreateField( - boolean isStatic, Class type, String fieldName, Supplier value) { - Reference fieldRef = fieldMap.get(fieldName); - if (fieldRef == null) { - fieldName = ctx.newName(fieldName); - ctx.addField(isStatic, true, ctx.type(type), fieldName, value.get()); - fieldRef = new Reference(fieldName, TypeRef.of(type)); - fieldMap.put(fieldName, fieldRef); - } - return fieldRef; - } - - /** 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) && ReflectionUtils.hasPublicNoArgConstructor(beanClass)) { - return new Expression.NewInstance(beanType); - } else { - ObjectCreators.getObjectCreator(beanClass); // trigger cache - Invoke newInstance = new Invoke(getObjectCreator(beanClass), "newInstance", OBJECT_TYPE); - return sourcePublicAccessible(beanClass) ? new Cast(newInstance, beanType) : newInstance; - } - } - - protected Expression getObjectCreator(Class type) { - ObjectCreators.getObjectCreator(type); // trigger cache - return getOrCreateField( - true, - ObjectCreator.class, - ctx.newName("objectCreator_" + type.getSimpleName()), - () -> - new StaticInvoke( - ObjectCreators.class, - "getObjectCreator", - TypeRef.of(ObjectCreator.class), - staticBeanClassExpr())); - } - - protected void buildRecordComponentDefaultValues() { - ctx.reserveName(recordComponentDefaultValues.name()); - StaticInvoke expr = - new StaticInvoke( - RecordUtils.class, - "buildRecordComponentDefaultValues", - OBJECT_ARRAY_TYPE, - beanClassExpr()); - ctx.addField(Object[].class, recordComponentDefaultValues.name(), expr); - } - - static boolean recordCtrAccessible(Class cls) { - // support unexported packages in module - if (!Modifier.isPublic(cls.getModifiers())) { - return false; - } - for (RecordComponent component : Objects.requireNonNull(RecordUtils.getRecordComponents(cls))) { - if (!Modifier.isPublic(component.getType().getModifiers())) { - return false; - } - } - return true; - } - - protected Expression beanClassExpr(Class cls) { - if (cls == beanClass) { - return staticBeanClassExpr(); - } - if (GraalvmSupport.isGraalBuildTime()) { - String name = cls.getName().replaceAll("\\.|\\$", "_") + "__class__"; - return getOrCreateField( - true, - Class.class, - name, - () -> - inlineReflectionUtilsInvoke( - "loadClass", CLASS_TYPE, Literal.ofString(cls.getName()))); - } - throw new UnsupportedOperationException(); - } - - protected Expression beanClassExpr() { - if (GraalvmSupport.isGraalBuildTime()) { - return staticBeanClassExpr(); - } - throw new UnsupportedOperationException(); - } - - protected Expression staticBeanClassExpr() { - if (sourcePublicAccessible(beanClass)) { - return Literal.ofClass(beanClass); - } - return staticClassFieldExpr(beanClass, "__class__"); - } - - protected Expression staticClassFieldExpr(Class cls, String fieldName) { - if (sourcePublicAccessible(cls)) { - return Literal.ofClass(cls); - } - return getOrCreateField( - true, - Class.class, - fieldName, - () -> - inlineReflectionUtilsInvoke("loadClass", CLASS_TYPE, Literal.ofString(cls.getName()))); - } - - private StaticInvoke reflectionUtilsInvoke( - String methodName, TypeRef returnType, Expression... arguments) { - return new StaticInvoke( - ReflectionUtils.class, methodName, "", returnType, false, false, false, arguments); - } - - private StaticInvoke inlineReflectionUtilsInvoke( - String methodName, TypeRef returnType, Expression... arguments) { - return new StaticInvoke( - ReflectionUtils.class, methodName, "", returnType, false, true, false, arguments); - } - - /** Build unsafePut operation. */ - protected Expression unsafePut(Expression base, Expression pos, Expression value) { - return new StaticInvoke(UnsafeOps.class, "putByte", base, pos, value); - } - - protected Expression unsafePutBoolean(Expression base, Expression pos, Expression value) { - return new StaticInvoke(UnsafeOps.class, "putBoolean", base, pos, value); - } - - protected Expression unsafePutChar(Expression base, Expression pos, Expression value) { - return new StaticInvoke(UnsafeOps.class, "putChar", base, pos, value); - } - - protected Expression unsafePutShort(Expression base, Expression pos, Expression value) { - return new StaticInvoke(UnsafeOps.class, "putShort", base, pos, value); - } - - protected Expression unsafePutInt(Expression base, Expression pos, Expression value) { - return new StaticInvoke(UnsafeOps.class, "putInt", base, pos, value); - } - - protected Expression unsafePutLong(Expression base, Expression pos, Expression value) { - return new StaticInvoke(UnsafeOps.class, "putLong", base, pos, value); - } - - protected Expression unsafePutFloat(Expression base, Expression pos, Expression value) { - return new StaticInvoke(UnsafeOps.class, "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); - } - - /** Build unsafeGet operation. */ - protected Expression unsafeGet(Expression base, Expression pos) { - return new StaticInvoke(UnsafeOps.class, "getByte", PRIMITIVE_BYTE_TYPE, base, pos); - } - - protected Expression unsafeGetBoolean(Expression base, Expression pos) { - return new StaticInvoke(UnsafeOps.class, "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); - if (!NativeByteOrder.IS_LITTLE_ENDIAN) { - expr = new StaticInvoke(Character.class, "reverseBytes", PRIMITIVE_CHAR_TYPE, expr.inline()); - } - return expr; - } - - protected Expression unsafeGetShort(Expression base, Expression pos) { - StaticInvoke expr = - new StaticInvoke(UnsafeOps.class, "getShort", PRIMITIVE_SHORT_TYPE, base, pos); - if (!NativeByteOrder.IS_LITTLE_ENDIAN) { - expr = new StaticInvoke(Short.class, "reverseBytes", PRIMITIVE_SHORT_TYPE, expr.inline()); - } - return expr; - } - - protected Expression unsafeGetInt(Expression base, Expression pos) { - StaticInvoke expr = new StaticInvoke(UnsafeOps.class, "getInt", PRIMITIVE_INT_TYPE, base, pos); - if (!NativeByteOrder.IS_LITTLE_ENDIAN) { - expr = new StaticInvoke(Integer.class, "reverseBytes", PRIMITIVE_INT_TYPE, expr.inline()); - } - return expr; - } - - protected Expression unsafeGetLong(Expression base, Expression pos) { - StaticInvoke expr = - new StaticInvoke(UnsafeOps.class, "getLong", PRIMITIVE_LONG_TYPE, base, pos); - if (!NativeByteOrder.IS_LITTLE_ENDIAN) { - expr = new StaticInvoke(Long.class, "reverseBytes", PRIMITIVE_LONG_TYPE, expr.inline()); - } - return expr; - } - - protected Expression unsafeGetFloat(Expression base, Expression pos) { - StaticInvoke expr = new StaticInvoke(UnsafeOps.class, "getInt", PRIMITIVE_INT_TYPE, base, pos); - if (!NativeByteOrder.IS_LITTLE_ENDIAN) { - expr = new StaticInvoke(Integer.class, "reverseBytes", PRIMITIVE_INT_TYPE, expr.inline()); - } - return new StaticInvoke(Float.class, "intBitsToFloat", PRIMITIVE_FLOAT_TYPE, expr.inline()); - } - - protected Expression unsafeGetDouble(Expression base, Expression pos) { - StaticInvoke expr = - new StaticInvoke(UnsafeOps.class, "getLong", PRIMITIVE_LONG_TYPE, base, pos); - if (!NativeByteOrder.IS_LITTLE_ENDIAN) { - expr = new StaticInvoke(Long.class, "reverseBytes", PRIMITIVE_LONG_TYPE, expr.inline()); - } - return new StaticInvoke(Double.class, "longBitsToDouble", PRIMITIVE_DOUBLE_TYPE, expr.inline()); - } - - protected Expression readChar(Expression buffer) { - return new Invoke(buffer, "readChar", PRIMITIVE_CHAR_TYPE); - } - - protected Expression readInt16(Expression buffer) { - String func = NativeByteOrder.IS_LITTLE_ENDIAN ? "_readInt16OnLE" : "_readInt16OnBE"; - return new Invoke(buffer, func, PRIMITIVE_SHORT_TYPE); - } - - protected Expression readInt32(Expression buffer) { - String func = NativeByteOrder.IS_LITTLE_ENDIAN ? "_readInt32OnLE" : "_readInt32OnBE"; - return new Invoke(buffer, func, PRIMITIVE_INT_TYPE); - } - - public static String readIntFunc() { - return NativeByteOrder.IS_LITTLE_ENDIAN ? "_readInt32OnLE" : "_readInt32OnBE"; - } - - protected Expression readVarInt32(Expression buffer) { - String func = NativeByteOrder.IS_LITTLE_ENDIAN ? "_readVarInt32OnLE" : "_readVarInt32OnBE"; - return new Invoke(buffer, func, PRIMITIVE_INT_TYPE); - } - - protected Expression readInt64(Expression buffer) { - return new Invoke(buffer, readLongFunc(), PRIMITIVE_LONG_TYPE); - } - - public static String readLongFunc() { - return NativeByteOrder.IS_LITTLE_ENDIAN ? "_readInt64OnLE" : "_readInt64OnBE"; - } - - public static String readInt16Func() { - return NativeByteOrder.IS_LITTLE_ENDIAN ? "_readInt16OnLE" : "_readInt16OnBE"; - } - - public static String readVarInt32Func() { - return NativeByteOrder.IS_LITTLE_ENDIAN ? "_readVarInt32OnLE" : "_readVarInt32OnBE"; - } - - public static String readFloat32Func() { - return NativeByteOrder.IS_LITTLE_ENDIAN ? "_readFloat32OnLE" : "_readFloat32OnBE"; - } - - public static String readFloat64Func() { - return NativeByteOrder.IS_LITTLE_ENDIAN ? "_readFloat64OnLE" : "_readFloat64OnBE"; - } - - protected Expression readFloat32(Expression buffer) { - return new Invoke(buffer, readFloat32Func(), PRIMITIVE_FLOAT_TYPE); - } - - protected Expression readFloat64(Expression buffer) { - return new Invoke(buffer, readFloat64Func(), PRIMITIVE_DOUBLE_TYPE); - } -} diff --git a/java/fory-core/src/main/java25/org/apache/fory/builder/ObjectCodecBuilder.java b/java/fory-core/src/main/java25/org/apache/fory/builder/ObjectCodecBuilder.java deleted file mode 100644 index b5c5f1ca59..0000000000 --- a/java/fory-core/src/main/java25/org/apache/fory/builder/ObjectCodecBuilder.java +++ /dev/null @@ -1,1465 +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.builder; - -import static org.apache.fory.codegen.Code.LiteralValue.FalseLiteral; -import static org.apache.fory.codegen.Expression.Invoke.inlineInvoke; -import static org.apache.fory.codegen.ExpressionUtils.add; -import static org.apache.fory.codegen.ExpressionUtils.cast; -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; -import static org.apache.fory.type.TypeUtils.PRIMITIVE_VOID_TYPE; -import static org.apache.fory.type.TypeUtils.SHORT_TYPE; -import static org.apache.fory.type.TypeUtils.getRawType; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.IdentityHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.SortedMap; -import java.util.TreeMap; -import org.apache.fory.Fory; -import org.apache.fory.codegen.Code; -import org.apache.fory.codegen.CodegenContext; -import org.apache.fory.codegen.Expression; -import org.apache.fory.codegen.Expression.Cast; -import org.apache.fory.codegen.Expression.Inlineable; -import org.apache.fory.codegen.Expression.Invoke; -import org.apache.fory.codegen.Expression.ListExpression; -import org.apache.fory.codegen.Expression.Literal; -import org.apache.fory.codegen.Expression.NewInstance; -import org.apache.fory.codegen.Expression.Reference; -import org.apache.fory.codegen.Expression.ReplaceStub; -import org.apache.fory.codegen.Expression.StaticInvoke; -import org.apache.fory.codegen.ExpressionVisitor; -import org.apache.fory.logging.Logger; -import org.apache.fory.logging.LoggerFactory; -import org.apache.fory.meta.TypeDef; -import org.apache.fory.platform.UnsafeOps; -import org.apache.fory.reflect.ObjectCreator; -import org.apache.fory.reflect.ObjectCreators; -import org.apache.fory.reflect.TypeRef; -import org.apache.fory.serializer.ObjectSerializer; -import org.apache.fory.serializer.AbstractObjectSerializer; -import org.apache.fory.type.BFloat16; -import org.apache.fory.type.Descriptor; -import org.apache.fory.type.DescriptorGrouper; -import org.apache.fory.type.DispatchId; -import org.apache.fory.type.Float16; -import org.apache.fory.type.TypeUtils; -import org.apache.fory.type.Types; -import org.apache.fory.util.StringUtils; -import org.apache.fory.util.function.SerializableSupplier; -import org.apache.fory.util.record.RecordUtils; - -/** - * Generate sequential read/write code for java serialization to speed up performance. It also - * reduces space overhead introduced by aligning. Codegen only for time-consuming field, others - * delegate to fory. - * - *

In order to improve jit-compile and inline, serialization code should be spilt groups to avoid - * huge/big methods. - * - *

With meta context share enabled and compatible mode, this serializer will take all non-inner - * final types as non-final, so that fory can write class definition when write class info for those - * types. - * - * @see ObjectCodecOptimizer for code stats and split heuristics. - */ -public class ObjectCodecBuilder extends BaseObjectCodecBuilder { - private static final Logger LOG = LoggerFactory.getLogger(ObjectCodecBuilder.class); - - 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); - Collection descriptors; - DescriptorGrouper grouper; - boolean shareMeta = fory.getConfig().isMetaShareEnabled(); - if (shareMeta) { - TypeDef typeDef = typeResolver(r -> r.getTypeDef(beanClass, true)); - descriptors = typeResolver(r -> typeDef.getDescriptors(r, beanClass)); - grouper = typeResolver(r -> r.createDescriptorGrouper(typeDef, beanClass)); - } else { - grouper = typeResolver(r -> r.getFieldDescriptorGrouper(beanClass, true, false)); - descriptors = grouper.getSortedDescriptors(); - } - if (org.apache.fory.util.Utils.DEBUG_OUTPUT_ENABLED) { - LOG.info( - "========== {} sorted descriptors for {} ==========", - descriptors.size(), - beanClass.getSimpleName()); - List sortedDescriptors = grouper.getSortedDescriptors(); - for (Descriptor d : sortedDescriptors) { - LOG.info( - " {} -> {}, ref {}, nullable {}", - StringUtils.toSnakeCase(d.getName()), - d.getTypeName(), - d.isTrackingRef(), - d.isNullable()); - } - } - classVersionHash = - typeResolver.checkClassVersion() - ? new Literal( - ObjectSerializer.computeStructHash(typeResolver, grouper), PRIMITIVE_INT_TYPE) - : null; - objectCodecOptimizer = new ObjectCodecOptimizer(beanClass, grouper, false, ctx); - if (isRecord) { - if (!recordCtrAccessible) { - buildRecordComponentDefaultValues(); - } - recordReversedMapping = RecordUtils.buildFieldToComponentMapping(beanClass); - } else { - initConstructorFields(grouper.getSortedDescriptors(), true); - } - } - - protected ObjectCodecBuilder(TypeRef beanType, Fory fory, Class superSerializerClass) { - super(beanType, fory, superSerializerClass); - this.classVersionHash = null; - if (isRecord) { - if (!recordCtrAccessible) { - buildRecordComponentDefaultValues(); - } - recordReversedMapping = RecordUtils.buildFieldToComponentMapping(beanClass); - } - } - - 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()); - } - - @Override - protected String codecSuffix() { - return ""; - } - - @Override - protected void addCommonImports() { - super.addCommonImports(); - ctx.addImport(Generated.GeneratedObjectSerializer.class); - } - - /** - * Return an expression that serialize java bean of type {@link CodecBuilder#beanClass} to buffer. - */ - @Override - public Expression buildEncodeExpression() { - Reference inputObject = new Reference(ROOT_OBJECT_NAME, OBJECT_TYPE, false); - Reference buffer = new Reference(BUFFER_NAME, bufferTypeRef, false); - - ListExpression expressions = new ListExpression(); - Expression bean = tryCastIfPublic(inputObject, beanType, ctx.newName(beanClass)); - expressions.add(bean); - if (typeResolver.checkClassVersion()) { - expressions.add(new Invoke(buffer, "writeInt32", classVersionHash)); - } - expressions.addAll(serializePrimitives(bean, buffer, objectCodecOptimizer.primitiveGroups)); - int numGroups = getNumGroups(objectCodecOptimizer); - addGroupExpressions( - objectCodecOptimizer.boxedWriteGroups, numGroups, expressions, bean, buffer); - addGroupExpressions( - objectCodecOptimizer.nonPrimitiveWriteGroups, numGroups, expressions, bean, buffer); - return expressions; - } - - private void addGroupExpressions( - List> writeGroup, - int numGroups, - ListExpression expressions, - Expression bean, - Reference buffer) { - for (List group : writeGroup) { - if (group.isEmpty()) { - continue; - } - boolean inline = hasFewFields() || (group.size() == 1 && numGroups < 10); - expressions.add(serializeGroup(group, bean, buffer, inline)); - } - } - - protected boolean hasFewFields() { - return objectCodecOptimizer.descriptorGrouper.getNumDescriptors() < 6; - } - - protected int getNumGroups(ObjectCodecOptimizer objectCodecOptimizer) { - return objectCodecOptimizer.boxedWriteGroups.size() - + objectCodecOptimizer.nonPrimitiveWriteGroups.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; - } - - private Expression serializeGroup( - List group, Expression bean, Expression buffer, boolean inline) { - SerializableSupplier expressionSupplier = - () -> { - ListExpression groupExpressions = new ListExpression(); - for (Descriptor d : group) { - // `bean` will be replaced by `Reference` to cut-off expr dependency. - Expression fieldValue = getFieldValue(bean, d); - walkPath.add(d.getDeclaringClass() + d.getName()); - Expression fieldExpr = serializeField(fieldValue, buffer, d); - walkPath.removeLast(); - groupExpressions.add(fieldExpr); - } - return groupExpressions; - }; - if (inline) { - return expressionSupplier.get(); - } - return objectCodecOptimizer.invokeGenerated( - writeCutPoints(bean, buffer), expressionSupplier.get(), "writeFields"); - } - - /** - * Return a list of expressions that serialize all primitive fields. This can reduce unnecessary - * grow call and increment writerIndex in writeXXX. - */ - private List serializePrimitives( - Expression bean, Expression buffer, List> primitiveGroups) { - int totalSize = getTotalSizeOfPrimitives(primitiveGroups); - if (totalSize == 0) { - return new ArrayList<>(); - } - if (config.compressInt() || config.compressLong()) { - return serializePrimitivesCompressed(bean, buffer, primitiveGroups, totalSize); - } else { - return serializePrimitivesUnCompressed(bean, buffer, primitiveGroups, totalSize); - } - } - - protected int getNumPrimitiveFields(List> primitiveGroups) { - return primitiveGroups.stream().mapToInt(List::size).sum(); - } - - private List serializePrimitivesUnCompressed( - Expression bean, Expression buffer, List> primitiveGroups, int totalSize) { - List expressions = new ArrayList<>(); - int numPrimitiveFields = getNumPrimitiveFields(primitiveGroups); - Literal totalSizeLiteral = new Literal(totalSize, PRIMITIVE_INT_TYPE); - // After this grow, following writes can be unsafe without checks. - 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(); - // use Reference to cut-off expr dependency. - for (Descriptor descriptor : group) { - int dispatchId = getNumericDescriptorDispatchId(descriptor); - // `bean` will be replaced by `Reference` to cut-off expr dependency. - Expression fieldValue = getFieldValue(bean, descriptor); - if (fieldValue instanceof Inlineable) { - ((Inlineable) fieldValue).inline(); - } - if (dispatchId == DispatchId.BOOL) { - groupExpressions.add(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 serializePrimitivesCompressed( - Expression bean, Expression buffer, List> primitiveGroups, int totalSize) { - List expressions = new ArrayList<>(); - // int/long may need extra one-byte for writing. - 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) { - // varint may be written as 5bytes, use 8bytes for written as long to reduce cost. - 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; // long use 1~9 bytes. - } - } - } - int growSize = totalSize + extraSize; - // After this grow, following writes can be unsafe without checks. - 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); - // use Reference to cut-off expr dependency. - int acc = 0; - boolean compressStarted = false; - for (Descriptor descriptor : group) { - int dispatchId = getNumericDescriptorDispatchId(descriptor); - // `bean` will be replaced by `Reference` to cut-off expr dependency. - Expression fieldValue = getFieldValue(bean, descriptor); - if (fieldValue instanceof Inlineable) { - ((Inlineable) fieldValue).inline(); - } - if (dispatchId == DispatchId.BOOL) { - groupExpressions.add(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) { - // int/long are sorted in the last. - addIncWriterIndexExpr(groupExpressions, buffer, acc); - } - if (hasFewFields() || numPrimitiveFields < 4) { - expressions.add(groupExpressions); - } else { - expressions.add( - objectCodecOptimizer.invokeGenerated( - ofHashSet(bean, buffer, 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) - : new Invoke(boxedNumericValue(fieldValue, descriptor), "byteValue", PRIMITIVE_BYTE_TYPE); - } - - private Expression primitiveShortValue(Expression fieldValue, Descriptor descriptor) { - return fieldValue.type().isPrimitive() - ? cast(fieldValue, PRIMITIVE_SHORT_TYPE) - : new Invoke(boxedNumericValue(fieldValue, descriptor), "shortValue", PRIMITIVE_SHORT_TYPE); - } - - private Expression primitiveIntValue(Expression fieldValue, Descriptor descriptor) { - return fieldValue.type().isPrimitive() - ? cast(fieldValue, PRIMITIVE_INT_TYPE) - : new Invoke(boxedNumericValue(fieldValue, descriptor), "intValue", PRIMITIVE_INT_TYPE); - } - - private Expression boxedNumericValue(Expression fieldValue, Descriptor descriptor) { - return Number.class.isAssignableFrom(getRawType(fieldValue.type())) - ? fieldValue - : cast(fieldValue, descriptor.getTypeRef()); - } - - private void addIncWriterIndexExpr(ListExpression expressions, Expression buffer, int diff) { - if (diff != 0) { - expressions.add(new Invoke(buffer, "_increaseWriterIndexUnsafe", Literal.ofInt(diff))); - } - } - - private int getTotalSizeOfPrimitives(List> primitiveGroups) { - return primitiveGroups.stream() - .flatMap(Collection::stream) - .mapToInt( - d -> { - Class rawType = d.getRawType(); - if (TypeUtils.isPrimitive(rawType) || TypeUtils.isBoxed(rawType)) { - return TypeUtils.getSizeOfPrimitiveType(TypeUtils.unwrap(rawType)); - } - return Types.getPrimitiveTypeSize(Types.getDescriptorTypeId(typeResolver, d)); - }) - .sum(); - } - - private Expression getWriterPos(Expression writerPos, long acc) { - if (acc == 0) { - return writerPos; - } - return add(writerPos, Literal.ofLong(acc)); - } - - public Expression buildDecodeExpression() { - Reference buffer = new Reference(BUFFER_NAME, bufferTypeRef, false); - ListExpression expressions = new ListExpression(); - if (typeResolver.checkClassVersion()) { - expressions.add(checkClassVersion(buffer)); - } - if (!isRecord && constructorFieldIndexes != null) { - return buildConstructorDecodeExpression(buffer, expressions); - } - Expression bean; - if (!isRecord) { - 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(); - } else { - bean = buildComponentsArray(); - } - } - expressions.addAll(deserializePrimitives(bean, buffer, objectCodecOptimizer.primitiveGroups)); - int numGroups = getNumGroups(objectCodecOptimizer); - deserializeReadGroup( - objectCodecOptimizer.boxedReadGroups, numGroups, expressions, bean, buffer); - deserializeReadGroup( - objectCodecOptimizer.nonPrimitiveReadGroups, numGroups, expressions, bean, buffer); - if (isRecord) { - if (recordCtrAccessible) { - assert bean instanceof FieldsCollector; - FieldsCollector collector = (FieldsCollector) bean; - bean = createRecord(collector.recordValuesMap); - } else { - ObjectCreators.getObjectCreator(beanClass); // trigger cache and make error raised early - bean = - new Invoke(getObjectCreator(beanClass), "newInstanceWithArguments", OBJECT_TYPE, bean); - } - } - 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 StaticInvoke( - UnsafeOps.class, "copyObjectArray", OBJECT_ARRAY_TYPE, recordComponentDefaultValues); - } - - 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 (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; - } - - private void addNonConstructorFieldSetters( - ListExpression expressions, Expression bean, FieldsArray fieldValues) { - for (Descriptor descriptor : objectCodecOptimizer.descriptorGrouper.getSortedDescriptors()) { - int index = fieldIndexes.get(descriptor); - if (constructorFieldMask[index]) { - continue; - } - 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)); - } - } - - 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)); - } - } - - 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 { - private final TreeMap recordValuesMap = new TreeMap<>(); - - protected FieldsCollector() { - super(new Expression[0]); - } - - @Override - public TypeRef type() { - return beanType; - } - - @Override - public Code.ExprCode doGenCode(CodegenContext ctx) { - return new Code.ExprCode(FalseLiteral, Code.variable(getRawType(beanType), "null")); - } - } - - 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) { - ((Inlineable) value).inline(false); - } - int index = recordReversedMapping.get(d.getName()); - FieldsCollector collector = (FieldsCollector) bean; - collector.recordValuesMap.put(index, value); - return value; - } else { - int index = recordReversedMapping.get(d.getName()); - return new Expression.AssignArrayElem(bean, value, Literal.ofInt(index)); - } - } - return super.setFieldValue(bean, d, value); - } - - protected Expression deserializeGroup( - List group, Expression bean, Expression buffer, boolean inline) { - if (isRecord) { - return deserializeGroupForRecord(group, bean, buffer); - } - SerializableSupplier exprSupplier = - () -> { - ListExpression groupExpressions = new ListExpression(); - // use Reference to cut-off expr dependency. - for (Descriptor d : group) { - ExpressionVisitor.ExprHolder exprHolder = ExpressionVisitor.ExprHolder.of("bean", bean); - walkPath.add(d.getDeclaringClass() + d.getName()); - TypeRef castTypeRef = - hasCompatibleCollectionArrayRead(d) - ? compatibleReadTargetTypeRef(d) - : d.getTypeRef(); - Expression action = - deserializeField( - buffer, - d, - // `bean` will be replaced by `Reference` to cut-off expr - // dependency. - expr -> - setFieldValue(exprHolder.get("bean"), d, tryInlineCast(expr, castTypeRef))); - walkPath.removeLast(); - if (needsGeneratedReadFieldMethod(d)) { - action = - objectCodecOptimizer.invokeGenerated( - readCutPoints(bean, buffer), action, "readField"); - } - groupExpressions.add(action); - } - return groupExpressions; - }; - if (inline) { - return exprSupplier.get(); - } else { - return objectCodecOptimizer.invokeGenerated( - readCutPoints(bean, buffer), exprSupplier.get(), "readFields"); - } - } - - private boolean needsGeneratedReadFieldMethod(Descriptor descriptor) { - return !hasFewFields() - && !isMonomorphic(descriptor) - && !useCollectionSerialization(descriptor) - && !useMapSerialization(descriptor.getTypeRef()); - } - - protected Expression deserializeGroupForRecord( - List group, Expression bean, Expression buffer) { - ListExpression groupExpressions = new ListExpression(); - // use Reference to cut-off expr dependency. - for (Descriptor d : group) { - TypeRef castTypeRef = - hasCompatibleCollectionArrayRead(d) ? compatibleReadTargetTypeRef(d) : d.getTypeRef(); - Expression value = deserializeField(buffer, d, expr -> expr); - Expression action = setFieldValue(bean, d, tryInlineCast(value, castTypeRef)); - groupExpressions.add(action); - } - return groupExpressions; - } - - private Expression checkClassVersion(Expression buffer) { - return new StaticInvoke( - ObjectSerializer.class, - "checkClassVersion", - PRIMITIVE_VOID_TYPE, - false, - beanClassExpr(), - inlineInvoke(buffer, readIntFunc(), PRIMITIVE_INT_TYPE), - Objects.requireNonNull(classVersionHash)); - } - - /** - * Return a list of expressions that deserialize all primitive fields. This can reduce unnecessary - * check call and increment readerIndex in writeXXX. - */ - protected List deserializePrimitives( - Expression bean, Expression buffer, List> primitiveGroups) { - int totalSize = getTotalSizeOfPrimitives(primitiveGroups); - if (totalSize == 0) { - return new ArrayList<>(); - } - if (config.compressInt() || config.compressLong()) { - return deserializeCompressedPrimitives(bean, buffer, primitiveGroups); - } else { - return deserializeUnCompressedPrimitives(bean, buffer, primitiveGroups, totalSize); - } - } - - private List deserializeUnCompressedPrimitives( - Expression bean, Expression buffer, List> primitiveGroups, int totalSize) { - List expressions = new ArrayList<>(); - int numPrimitiveFields = getNumPrimitiveFields(primitiveGroups); - Literal totalSizeLiteral = Literal.ofInt(totalSize); - // After this check, following read can be totally unsafe without checks - 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); - } - // `bean` will be replaced by `Reference` to cut-off expr dependency. - groupExpressions.add(setFieldValue(bean, descriptor, fieldValue)); - } - if (hasFewFields() || numPrimitiveFields < 4 || isRecord) { - expressions.add(groupExpressions); - } else { - expressions.add( - objectCodecOptimizer.invokeGenerated( - ofHashSet(bean, buffer, readerIndex), groupExpressions, "readFields")); - } - } - Expression increaseReaderIndex = - new Invoke( - buffer, "increaseReaderIndex", new Literal(totalSizeLiteral, PRIMITIVE_INT_TYPE)); - expressions.add(increaseReaderIndex); - return expressions; - } - - private List deserializeCompressedPrimitives( - Expression bean, Expression buffer, List> primitiveGroups) { - List expressions = new ArrayList<>(); - int numPrimitiveFields = getNumPrimitiveFields(primitiveGroups); - for (List group : primitiveGroups) { - // After this check, following read can be totally unsafe without checks. - // checkReadableBytes first, `fillBuffer` may create a new heap buffer. - ReplaceStub checkReadableBytesStub = new ReplaceStub(); - expressions.add(checkReadableBytesStub); - Expression 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); - } - // `bean` will be replaced by `Reference` to cut-off expr dependency. - groupExpressions.add(setFieldValue(bean, descriptor, fieldValue)); - } - if (acc != 0) { - checkReadableBytesStub.setTargetObject( - new Invoke(buffer, "checkReadableBytes", Literal.ofInt(acc))); - } - if (!compressStarted) { - addIncReaderIndexExpr(groupExpressions, buffer, acc); - } - if (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))); - } - } - - private Expression getReaderAddress(Expression readerPos, long acc) { - if (acc == 0) { - return readerPos; - } - 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/java25/org/apache/fory/memory/MemoryBuffer.java b/java/fory-core/src/main/java25/org/apache/fory/memory/MemoryBuffer.java index 30c5d7b0e5..5232671e14 100644 --- 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 @@ -3844,21 +3844,102 @@ 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 { - checkArgument(target != null, "Raw native-address target copy is unsupported on JDK25"); - final long thisPointer = this.address + offset; - checkArgument(thisPointer + numBytes <= addressLimit); - memoryAccess.copyMemory(this.heapMemory, thisPointer, target, targetPointer, numBytes); + checkArrayCopy(offset, targetOffset, target.length, numBytes, 0); + memoryAccess.copyMemory(heapMemory, address + offset, target, 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); + memoryAccess.copyMemory(heapMemory, address + offset, target, 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); + memoryAccess.copyMemory( + heapMemory, address + offset, target, 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); + memoryAccess.copyMemory( + heapMemory, address + offset, target, 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); + memoryAccess.copyMemory( + heapMemory, address + offset, target, 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); + memoryAccess.copyMemory( + heapMemory, address + offset, target, 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); + memoryAccess.copyMemory( + heapMemory, address + offset, target, 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); + memoryAccess.copyMemory( + heapMemory, address + offset, target, arrayCopyOffset(targetOffset, 3), 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; + } + /** * JVM-only bulk copy method. Copies {@code numBytes} bytes from source unsafe object and pointer. * Throws on Android before executing unsafe memory access. 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 8eced95b1e..f395e60467 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 @@ -215,8 +215,9 @@ 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)); + byte[] bytes = new byte[4]; + source.copyToByteArray(0, bytes, 0, 4); + check(bytes, new byte[] {1, 2, 3, 4}); assertThrows( UnsupportedOperationException.class, () -> target.copyFromUnsafe(0, new byte[4], 0, 4)); } @@ -440,6 +441,70 @@ public void testDirectPrimitiveArrays() { 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-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..093c61f71f 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 @@ -187,43 +187,43 @@ 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); + buffer.copyToShortArray(elementOffset, values, 0, numElements * 2); return values; } public int[] toIntArray() { int[] values = new int[numElements]; - buffer.copyToUnsafe(elementOffset, values, UnsafeOps.INT_ARRAY_OFFSET, numElements * 4); + buffer.copyToIntArray(elementOffset, values, 0, numElements * 4); return values; } public long[] toLongArray() { long[] values = new long[numElements]; - buffer.copyToUnsafe(elementOffset, values, UnsafeOps.LONG_ARRAY_OFFSET, numElements * 8); + buffer.copyToLongArray(elementOffset, values, 0, numElements * 8); return values; } public float[] toFloatArray() { float[] values = new float[numElements]; - buffer.copyToUnsafe(elementOffset, values, UnsafeOps.FLOAT_ARRAY_OFFSET, numElements * 4); + buffer.copyToFloatArray(elementOffset, values, 0, numElements * 4); return values; } public double[] toDoubleArray() { double[] values = new double[numElements]; - buffer.copyToUnsafe(elementOffset, values, UnsafeOps.DOUBLE_ARRAY_OFFSET, numElements * 8); + buffer.copyToDoubleArray(elementOffset, values, 0, numElements * 8); return values; } 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); } From 46f632441514cdbe8c0cd900b35776785c515472 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 24 May 2026 23:58:56 +0800 Subject: [PATCH 31/34] feat(java): remove Unsafe for jdk25 --- benchmarks/java/pom.xml | 36 +- .../fory/benchmark/CompressStringSuite.java | 3 +- .../apache/fory/benchmark/Identity2IdMap.java | 15 +- .../fory/benchmark/Jdk25MrJarCheck.java | 12 +- .../apache/fory/benchmark/MemorySuite.java | 12 +- .../fory/benchmark/NewJava11StringSuite.java | 13 +- .../apache/fory/benchmark/NewStringSuite.java | 7 +- .../apache/fory/benchmark/UnsafeAccess.java | 37 + benchmarks/java25/README.md | 8 +- ci/run_ci.sh | 3 +- ci/tasks/java.py | 9 +- docs/guide/java/troubleshooting.md | 45 +- docs/guide/kotlin/configuration.md | 23 + integration_tests/graalvm_tests/pom.xml | 22 +- .../jdk_compatibility_tests/pom.xml | 1 - integration_tests/jpms_tests/pom.xml | 1 - java/fory-core/pom.xml | 18 +- .../fory/builder/BaseObjectCodecBuilder.java | 10 +- .../org/apache/fory/builder/CodecBuilder.java | 83 +- .../fory/builder/ObjectCodecBuilder.java | 5 +- .../org/apache/fory/codegen/Expression.java | 36 +- .../org/apache/fory/memory/LittleEndian.java | 11 +- .../org/apache/fory/memory/MemoryBuffer.java | 330 +++++-- .../org/apache/fory/memory/MemoryOps.java | 86 +- .../org/apache/fory/platform/UnsafeOps.java | 215 ----- .../fory/platform/internal/_JDKAccess.java | 4 + .../apache/fory/reflect/FieldAccessor.java | 87 +- .../apache/fory/reflect/ObjectCreator.java | 2 +- .../apache/fory/reflect/ObjectCreators.java | 23 +- .../apache/fory/reflect/ReflectionUtils.java | 2 +- .../apache/fory/resolver/ClassResolver.java | 16 - .../fory/serializer/CompatibleSerializer.java | 4 +- .../fory/serializer/ExceptionSerializers.java | 2 +- .../serializer/ObjectStreamSerializer.java | 6 +- .../fory/serializer/PlatformStringUtils.java | 30 +- .../apache/fory/serializer/Serializers.java | 101 +- .../org/apache/fory/memory/LittleEndian.java | 65 ++ .../org/apache/fory/memory/MemoryBuffer.java | 902 +++++++----------- .../org/apache/fory/platform/UnsafeOps.java | 497 ---------- .../fory/platform/internal/_JDKAccess.java | 4 + .../src/main/resources/META-INF/LICENSE | 1 - .../fory-core/native-image.properties | 2 +- .../fory/GuavaOptionalDependencyTest.java | 4 +- .../fory/JpmsOptionalClassLoadingTest.java | 14 +- .../test/java/org/apache/fory/StreamTest.java | 9 + .../test/java/org/apache/fory/TestUtils.java | 47 +- .../apache/fory/config/ForyBuilderTest.java | 13 +- .../apache/fory/memory/MemoryBufferTest.java | 69 +- .../resolver/GraalvmRuntimeArrayTest.java | 13 +- .../serializer/AndroidDynamicFeatureTest.java | 10 +- .../org/apache/fory/util/StringUtilsTest.java | 49 +- java/fory-format/pom.xml | 2 +- .../fory/format/row/binary/BinaryArray.java | 118 ++- .../row/binary/writer/BinaryArrayWriter.java | 75 +- .../format/row/binary/BinaryArrayTest.java | 104 ++ .../fory/format/row/binary/BinaryRowTest.java | 13 +- java/fory-simd/pom.xml | 2 +- java/pom.xml | 1 + kotlin/fory-kotlin/pom.xml | 2 + .../serializer/kotlin/KotlinSerializers.java | 18 + .../kotlin/KotlinBuiltinSerializers.kt | 236 +++++ .../kotlin/KotlinDefaultValueSupport.kt | 74 +- .../serializer/kotlin/DefaultValueTest.kt | 7 +- scala/build.sbt | 14 + .../scala/ToFactorySerializers.java | 58 +- 65 files changed, 1986 insertions(+), 1755 deletions(-) create mode 100644 benchmarks/java/src/main/java/org/apache/fory/benchmark/UnsafeAccess.java delete mode 100644 java/fory-core/src/main/java/org/apache/fory/platform/UnsafeOps.java create mode 100644 java/fory-core/src/main/java25/org/apache/fory/memory/LittleEndian.java delete mode 100644 java/fory-core/src/main/java25/org/apache/fory/platform/UnsafeOps.java create mode 100644 kotlin/fory-kotlin/src/main/kotlin/org/apache/fory/serializer/kotlin/KotlinBuiltinSerializers.kt diff --git a/benchmarks/java/pom.xml b/benchmarks/java/pom.xml index 2c8e5f33a1..a2ba15d9cf 100644 --- a/benchmarks/java/pom.xml +++ b/benchmarks/java/pom.xml @@ -264,8 +264,12 @@ src="${project.build.directory}/${uberjar.name}.jar" dest="${jdk25.benchmark.check.dir}"> + + - - + + @@ -297,18 +303,18 @@ - - + if="jdk25.benchmark.rootunsafeops.present" + message="JDK25 benchmark jar must not contain root UnsafeOps class."/> + + @@ -321,12 +327,6 @@ - - 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 index 1bb2b38d91..41aae1fdae 100644 --- a/benchmarks/java/src/main/java/org/apache/fory/benchmark/Jdk25MrJarCheck.java +++ b/benchmarks/java/src/main/java/org/apache/fory/benchmark/Jdk25MrJarCheck.java @@ -20,7 +20,6 @@ package org.apache.fory.benchmark; import org.apache.fory.memory.MemoryBuffer; -import org.apache.fory.platform.UnsafeOps; import org.apache.fory.platform.internal._JDKAccess; import org.apache.fory.reflect.FieldAccessor; @@ -30,7 +29,7 @@ private Jdk25MrJarCheck() {} public static void main(String[] args) { verifyClass(MemoryBuffer.class); - verifyClass(UnsafeOps.class); + verifyMissing("org.apache.fory.platform.UnsafeOps"); verifyClass(_JDKAccess.class); verifyClass(FieldAccessor.class); verifyClass("org.apache.fory.serializer.PlatformStringUtils"); @@ -39,6 +38,15 @@ public static void main(String[] args) { } } + 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)); 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 c4fcde43e1..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,13 +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.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; @@ -36,8 +37,8 @@ public class NewJava11StringSuite { static { if (JdkVersion.MAJOR_VERSION > 8) { - strBytes = (byte[]) UnsafeOps.getObject(str, fieldOffset(String.class, "value")); - coder = UnsafeOps.getByte(str, fieldOffset(String.class, "coder")); + strBytes = (byte[]) UNSAFE.getObject(str, fieldOffset(String.class, "value")); + coder = UNSAFE.getByte(str, fieldOffset(String.class, "coder")); } } @@ -56,7 +57,7 @@ public class NewJava11StringSuite { private static long fieldOffset(Class type, String fieldName) { try { - return UnsafeOps.objectFieldOffset(type.getDeclaredField(fieldName)); + return UNSAFE.objectFieldOffset(type.getDeclaredField(fieldName)); } catch (NoSuchFieldException e) { throw new IllegalStateException(e); } @@ -70,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 dfbad5136a..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,12 +19,13 @@ package org.apache.fory.benchmark; -import org.apache.fory.platform.UnsafeOps; 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(); @@ -45,7 +46,7 @@ public Object createJDK8StringByCopy() { private static long fieldOffset(Class type, String fieldName) { try { - return UnsafeOps.objectFieldOffset(type.getDeclaredField(fieldName)); + return UNSAFE.objectFieldOffset(type.getDeclaredField(fieldName)); } catch (NoSuchFieldException e) { throw new IllegalStateException(e); } @@ -54,7 +55,7 @@ private static long fieldOffset(Class type, String fieldName) { // @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 index a7edad7c00..80e2928f42 100644 --- a/benchmarks/java25/README.md +++ b/benchmarks/java25/README.md @@ -1,6 +1,6 @@ # Java 25 Direct Memory Access Benchmark -This temporary JMH module compares direct-buffer scalar access paths used to reason about +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. @@ -29,12 +29,16 @@ java -jar target/java25-memory-access-benchmarks.jar \ -f 1 -wi 5 -i 5 -t 1 -w 1s -r 1s ``` -The benchmark class adds the required fork JVM options for the Unsafe path: +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/ci/run_ci.sh b/ci/run_ci.sh index dfe1611726..76f9aecbd3 100755 --- a/ci/run_ci.sh +++ b/ci/run_ci.sh @@ -113,7 +113,6 @@ jdk25_deny_options() { 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.nio=${fory_open_targets}" printf " %s" "--add-opens=java.base/java.math=${fory_open_targets}" } @@ -205,7 +204,7 @@ jdk17_plus_tests() { 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" + JDK_JAVA_OPTIONS="--add-opens=java.base/java.nio=org.apache.arrow.memory.core" if [[ "$java_major" -ge 25 ]]; then JDK_JAVA_OPTIONS="$JDK_JAVA_OPTIONS $(jdk25_deny_options) $(jdk25_javac_options)" fi diff --git a/ci/tasks/java.py b/ci/tasks/java.py index bed7db53fc..2aca7ad071 100644 --- a/ci/tasks/java.py +++ b/ci/tasks/java.py @@ -89,7 +89,6 @@ def jdk25_deny_options(): 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.nio={fory_open_targets}", f"--add-opens=java.base/java.math={fory_open_targets}", ] @@ -252,12 +251,14 @@ 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") - jdk_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") diff --git a/docs/guide/java/troubleshooting.md b/docs/guide/java/troubleshooting.md index a28518ffa7..9c35b3eb70 100644 --- a/docs/guide/java/troubleshooting.md +++ b/docs/guide/java/troubleshooting.md @@ -150,7 +150,8 @@ fory.registerSerializer(MyClass.class, new MyClassSerializer(fory.getTypeResolve ### JDK25+ zero-Unsafe mode and module opens -When running on JDK25+ with Unsafe memory access denied, start the JVM with: +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 @@ -163,31 +164,35 @@ When any Fory artifact is on the classpath instead of the module path, also incl --add-opens=/=ALL-UNNAMED,org.apache.fory.core,org.apache.fory.format ``` -Some optimized serializers and direct-buffer helpers also need JDK-private packages. 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` | -| `ByteArrayInputStream`, `ByteArrayOutputStream`, and Java object-stream metadata | `java.base/java.io` | -| Proxy serializers | `java.base/java.lang.reflect` | -| Direct `ByteBuffer` wrapping | `java.base/java.nio` | - -For example, direct `ByteBuffer` wrapping on the module path requires: +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 ---add-opens=java.base/java.nio=ALL-UNNAMED,org.apache.fory.core,org.apache.fory.format +--enable-final-field-mutation=org.apache.fory.core ``` -Normal classes with final instance fields require final-field mutation to be enabled for Fory core -when Unsafe allocation is denied: +Use `ALL-UNNAMED` when running Fory on the classpath: ```bash ---enable-final-field-mutation=org.apache.fory.core +--enable-final-field-mutation=ALL-UNNAMED ``` Fory restores those final fields through method-handle based access. Non-final fields can still be 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/graalvm_tests/pom.xml b/integration_tests/graalvm_tests/pom.xml index a0de283a90..906347fdfb 100644 --- a/integration_tests/graalvm_tests/pom.xml +++ b/integration_tests/graalvm_tests/pom.xml @@ -185,7 +185,6 @@ -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.nio=ALL-UNNAMED -J--add-opens=java.base/java.math=ALL-UNNAMED @@ -198,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} + diff --git a/integration_tests/jdk_compatibility_tests/pom.xml b/integration_tests/jdk_compatibility_tests/pom.xml index 1d0899d92f..53a40d1b56 100644 --- a/integration_tests/jdk_compatibility_tests/pom.xml +++ b/integration_tests/jdk_compatibility_tests/pom.xml @@ -104,7 +104,6 @@ --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.nio=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 e98b2d633c..f74ff38671 100644 --- a/integration_tests/jpms_tests/pom.xml +++ b/integration_tests/jpms_tests/pom.xml @@ -99,7 +99,6 @@ --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.nio=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/java/fory-core/pom.xml b/java/fory-core/pom.xml index 6e74e2c2a4..7aabd48d97 100644 --- a/java/fory-core/pom.xml +++ b/java/fory-core/pom.xml @@ -239,7 +239,9 @@ src="${project.build.directory}/${project.build.finalName}.jar" dest="${jdk25.mrjar.check.dir}"> + + @@ -251,9 +253,15 @@ + + @@ -276,8 +284,14 @@ file="${jdk25.mrjar.check.dir}/META-INF/versions/25/org/apache/fory/serializer/PlatformStringUtils.class" property="jdk25.platformstring.present"/> + if="jdk25.root.unsafeops.present" + message="Root UnsafeOps class must not be packaged in fory-core."/> + + 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 3bcad7a701..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,7 @@ 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; @@ -71,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. @@ -199,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. */ @@ -328,22 +329,16 @@ private Expression unsafeAccessField( 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); } } @@ -361,15 +356,23 @@ private Expression fieldOffsetExpr(Class cls, Descriptor descriptor) { Expression classExpr = beanClassExpr(field.getDeclaringClass()); new Invoke(classExpr, "getDeclaredField", TypeRef.of(Field.class)); Expression reflectFieldRef = getReflectField(field.getDeclaringClass(), field, false); - return new StaticInvoke( - UnsafeOps.class, "objectFieldOffset", PRIMITIVE_LONG_TYPE, reflectFieldRef) + return new Invoke( + getUnsafe(), "objectFieldOffset", PRIMITIVE_LONG_TYPE, reflectFieldRef) .inline(); }); } else { - return Literal.ofLong(UnsafeOps.objectFieldOffset(field)); + 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(); @@ -474,9 +477,9 @@ private Expression unsafeSetField(Expression bean, Descriptor descriptor, Expres 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); } } @@ -550,7 +553,9 @@ protected Expression newBean() { 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; } } @@ -649,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()); } @@ -700,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()); } @@ -709,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()); } @@ -717,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()); } @@ -726,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()); } @@ -734,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/ObjectCodecBuilder.java b/java/fory-core/src/main/java/org/apache/fory/builder/ObjectCodecBuilder.java index f23801f948..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 @@ -65,7 +65,6 @@ import org.apache.fory.logging.LoggerFactory; import org.apache.fory.meta.TypeDef; import org.apache.fory.platform.JdkVersion; -import org.apache.fory.platform.UnsafeOps; import org.apache.fory.reflect.ObjectCreator; import org.apache.fory.reflect.ObjectCreators; import org.apache.fory.reflect.TypeRef; @@ -1187,8 +1186,8 @@ protected void deserializeReadGroup( } protected Expression buildComponentsArray() { - return new StaticInvoke( - UnsafeOps.class, "copyObjectArray", OBJECT_ARRAY_TYPE, recordComponentDefaultValues); + return new Cast( + new Invoke(recordComponentDefaultValues, "clone", OBJECT_TYPE), OBJECT_ARRAY_TYPE); } protected Expression createRecord(SortedMap recordComponents) { 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/memory/LittleEndian.java b/java/fory-core/src/main/java/org/apache/fory/memory/LittleEndian.java index 0e0c2d246a..1b58ed70d6 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,10 @@ */ public class LittleEndian { + private static final Unsafe UNSAFE = AndroidSupport.IS_ANDROID ? null : _JDKAccess.UNSAFE; + private static final int BYTE_ARRAY_OFFSET = + AndroidSupport.IS_ANDROID ? 0 : UNSAFE.arrayBaseOffset(byte[].class); + public static int putVarUint36Small(byte[] arr, int index, long v) { if (v >>> 7 == 0) { arr[index] = (byte) v; @@ -62,7 +67,7 @@ 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); } @@ -74,6 +79,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); + 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 743ccd32bc..6f5f8b5a8d 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 @@ -29,7 +29,8 @@ 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; /** @@ -65,9 +66,29 @@ */ 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 && UnsafeOps.unaligned(); + private static final boolean UNALIGNED = !AndroidSupport.IS_ANDROID && unaligned(); + private static final int BOOLEAN_ARRAY_OFFSET = + AndroidSupport.IS_ANDROID ? 0 : UNSAFE.arrayBaseOffset(boolean[].class); + private static final int BYTE_ARRAY_OFFSET = + AndroidSupport.IS_ANDROID ? 0 : UNSAFE.arrayBaseOffset(byte[].class); + private static final int CHAR_ARRAY_OFFSET = + AndroidSupport.IS_ANDROID ? 0 : UNSAFE.arrayBaseOffset(char[].class); + private static final int SHORT_ARRAY_OFFSET = + AndroidSupport.IS_ANDROID ? 0 : UNSAFE.arrayBaseOffset(short[].class); + private static final int INT_ARRAY_OFFSET = + AndroidSupport.IS_ANDROID ? 0 : UNSAFE.arrayBaseOffset(int[].class); + private static final int LONG_ARRAY_OFFSET = + AndroidSupport.IS_ANDROID ? 0 : UNSAFE.arrayBaseOffset(long[].class); + private static final int FLOAT_ARRAY_OFFSET = + AndroidSupport.IS_ANDROID ? 0 : UNSAFE.arrayBaseOffset(float[].class); + private static final int DOUBLE_ARRAY_OFFSET = + AndroidSupport.IS_ANDROID ? 0 : 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(); @@ -77,7 +98,7 @@ private static final class DirectBufferAccess { static { try { Field addressField = Buffer.class.getDeclaredField("address"); - BUFFER_ADDRESS_FIELD_OFFSET = UnsafeOps.objectFieldOffset(addressField); + BUFFER_ADDRESS_FIELD_OFFSET = UNSAFE.objectFieldOffset(addressField); checkArgument(BUFFER_ADDRESS_FIELD_OFFSET != 0); } catch (NoSuchFieldException e) { throw new IllegalStateException(e); @@ -85,6 +106,41 @@ private static final class DirectBufferAccess { } } + 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 @@ -208,7 +264,7 @@ 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 UnsafeOps.getLong(buffer, DirectBufferAccess.BUFFER_ADDRESS_FIELD_OFFSET); + return UNSAFE.getLong(buffer, DirectBufferAccess.BUFFER_ADDRESS_FIELD_OFFSET); } catch (Throwable t) { throw new Error("Could not access direct byte buffer address field.", t); } @@ -268,7 +324,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; @@ -379,7 +435,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); } } @@ -400,7 +456,7 @@ public void get(int offset, ByteBuffer target, int numBytes) { 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(); } @@ -425,7 +481,7 @@ public void put(int offset, ByteBuffer source, int numBytes) { 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(); } @@ -459,8 +515,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); } } @@ -1688,12 +1744,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; } } @@ -1720,9 +1772,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); @@ -1752,9 +1804,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); @@ -1784,9 +1836,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); @@ -1816,9 +1868,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); @@ -1848,9 +1900,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); @@ -1880,9 +1932,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); @@ -3056,7 +3108,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; @@ -3226,8 +3278,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; @@ -3251,7 +3302,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; } @@ -3271,8 +3322,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; } } @@ -3291,8 +3341,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; } } @@ -3311,8 +3360,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; } } @@ -3331,8 +3379,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; } } @@ -3351,8 +3398,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; } } @@ -3371,8 +3417,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; } } @@ -3391,8 +3436,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; } } @@ -3410,12 +3454,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; } } @@ -3438,11 +3478,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; } @@ -3471,11 +3511,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; } @@ -3495,11 +3535,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; } @@ -3519,11 +3559,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; } @@ -3543,11 +3583,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; } @@ -3567,11 +3607,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; } @@ -3611,7 +3651,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( @@ -3630,12 +3670,7 @@ public void copyToByteArray(int offset, byte[] target, int targetOffset, int num MemoryOps.copyToByteArray(this, offset, target, targetOffset, numBytes); } else { checkArrayCopy(offset, targetOffset, target.length, numBytes, 0); - UNSAFE.copyMemory( - heapMemory, - address + offset, - target, - UnsafeOps.BYTE_ARRAY_OFFSET + targetOffset, - numBytes); + copyMemory(heapMemory, address + offset, target, BYTE_ARRAY_OFFSET + targetOffset, numBytes); } } @@ -3644,12 +3679,8 @@ public void copyToBooleanArray(int offset, boolean[] target, int targetOffset, i MemoryOps.copyToBooleanArray(this, offset, target, targetOffset, numBytes); } else { checkArrayCopy(offset, targetOffset, target.length, numBytes, 0); - UNSAFE.copyMemory( - heapMemory, - address + offset, - target, - UnsafeOps.BOOLEAN_ARRAY_OFFSET + targetOffset, - numBytes); + copyMemory( + heapMemory, address + offset, target, BOOLEAN_ARRAY_OFFSET + targetOffset, numBytes); } } @@ -3658,11 +3689,11 @@ public void copyToCharArray(int offset, char[] target, int targetOffset, int num MemoryOps.copyToCharArray(this, offset, target, targetOffset, numBytes); } else { checkArrayCopy(offset, targetOffset, target.length, numBytes, 1); - UNSAFE.copyMemory( + copyMemory( heapMemory, address + offset, target, - UnsafeOps.CHAR_ARRAY_OFFSET + arrayCopyOffset(targetOffset, 1), + CHAR_ARRAY_OFFSET + arrayCopyOffset(targetOffset, 1), numBytes); } } @@ -3672,11 +3703,11 @@ public void copyToShortArray(int offset, short[] target, int targetOffset, int n MemoryOps.copyToShortArray(this, offset, target, targetOffset, numBytes); } else { checkArrayCopy(offset, targetOffset, target.length, numBytes, 1); - UNSAFE.copyMemory( + copyMemory( heapMemory, address + offset, target, - UnsafeOps.SHORT_ARRAY_OFFSET + arrayCopyOffset(targetOffset, 1), + SHORT_ARRAY_OFFSET + arrayCopyOffset(targetOffset, 1), numBytes); } } @@ -3686,11 +3717,11 @@ public void copyToIntArray(int offset, int[] target, int targetOffset, int numBy MemoryOps.copyToIntArray(this, offset, target, targetOffset, numBytes); } else { checkArrayCopy(offset, targetOffset, target.length, numBytes, 2); - UNSAFE.copyMemory( + copyMemory( heapMemory, address + offset, target, - UnsafeOps.INT_ARRAY_OFFSET + arrayCopyOffset(targetOffset, 2), + INT_ARRAY_OFFSET + arrayCopyOffset(targetOffset, 2), numBytes); } } @@ -3700,11 +3731,11 @@ public void copyToLongArray(int offset, long[] target, int targetOffset, int num MemoryOps.copyToLongArray(this, offset, target, targetOffset, numBytes); } else { checkArrayCopy(offset, targetOffset, target.length, numBytes, 3); - UNSAFE.copyMemory( + copyMemory( heapMemory, address + offset, target, - UnsafeOps.LONG_ARRAY_OFFSET + arrayCopyOffset(targetOffset, 3), + LONG_ARRAY_OFFSET + arrayCopyOffset(targetOffset, 3), numBytes); } } @@ -3714,11 +3745,11 @@ public void copyToFloatArray(int offset, float[] target, int targetOffset, int n MemoryOps.copyToFloatArray(this, offset, target, targetOffset, numBytes); } else { checkArrayCopy(offset, targetOffset, target.length, numBytes, 2); - UNSAFE.copyMemory( + copyMemory( heapMemory, address + offset, target, - UnsafeOps.FLOAT_ARRAY_OFFSET + arrayCopyOffset(targetOffset, 2), + FLOAT_ARRAY_OFFSET + arrayCopyOffset(targetOffset, 2), numBytes); } } @@ -3728,11 +3759,114 @@ public void copyToDoubleArray(int offset, double[] target, int targetOffset, int MemoryOps.copyToDoubleArray(this, offset, target, targetOffset, numBytes); } else { checkArrayCopy(offset, targetOffset, target.length, numBytes, 3); - UNSAFE.copyMemory( + copyMemory( heapMemory, address + offset, target, - UnsafeOps.DOUBLE_ARRAY_OFFSET + arrayCopyOffset(targetOffset, 3), + 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); } } @@ -3755,20 +3889,6 @@ private static long arrayCopyOffset(int elementOffset, int elementShift) { return (long) elementOffset << elementShift; } - /** - * 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) { - if (AndroidSupport.IS_ANDROID) { - MemoryOps.throwRawUnsafeMemoryCopyUnsupported(); - } else { - final long thisPointer = this.address + offset; - checkArgument(thisPointer + numBytes <= addressLimit); - UNSAFE.copyMemory(source, sourcePointer, heapMemory, thisPointer, numBytes); - } - } - public byte[] getBytes(int index, int length) { if (index == 0 && heapMemory != null && heapOffset == 0) { // Arrays.copyOf is an intrinsics, which is faster @@ -3884,7 +4004,7 @@ 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 unsafeEqualTo(heapMemory, pos, bytes, UnsafeOps.BYTE_ARRAY_OFFSET + bytesOffset, len); + return unsafeEqualTo(heapMemory, pos, bytes, BYTE_ARRAY_OFFSET + bytesOffset, len); } private static boolean unsafeEqualTo( 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 8b1879f250..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()]; @@ -1341,6 +1337,88 @@ static void copyToDoubleArray( } } + 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, 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 2dc5456a63..0000000000 --- a/java/fory-core/src/main/java/org/apache/fory/platform/UnsafeOps.java +++ /dev/null @@ -1,215 +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.platform.internal._JDKAccess; -import org.apache.fory.util.ExceptionUtils; -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 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; - } - - /** Create an instance of type. This method don't call constructor. */ - public static T newInstance(Class type) { - try { - return type.cast(UNSAFE.allocateInstance(type)); - } catch (InstantiationException e) { - ExceptionUtils.throwException(e); - } - throw new IllegalStateException("unreachable"); - } -} diff --git a/java/fory-core/src/main/java/org/apache/fory/platform/internal/_JDKAccess.java b/java/fory-core/src/main/java/org/apache/fory/platform/internal/_JDKAccess.java index 8584f722f4..9efba19593 100644 --- 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 @@ -124,6 +124,10 @@ public class _JDKAccess { _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; 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 2eb540cb79..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 @@ -37,7 +37,6 @@ 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; @@ -47,10 +46,12 @@ import org.apache.fory.util.function.ToFloatFunction; import org.apache.fory.util.function.ToShortFunction; import org.apache.fory.util.record.RecordUtils; +import sun.misc.Unsafe; /** 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; @@ -81,7 +82,7 @@ private static long fieldOffset(Field field) { // Field offsets are rewritten by GraalVM and are not stable during native-image build time. return -1; } - return UnsafeOps.objectFieldOffset(field); + return UNSAFE.objectFieldOffset(field); } protected FieldAccessor(Field field, long fieldOffset) { @@ -124,36 +125,31 @@ public void set(Object obj, Object value) { public final void copy(Object sourceObject, Object targetObject) { switch (accessKind) { case BOOLEAN_ACCESS: - UnsafeOps.putBoolean( - targetObject, fieldOffset, UnsafeOps.getBoolean(sourceObject, fieldOffset)); + UNSAFE.putBoolean(targetObject, fieldOffset, UNSAFE.getBoolean(sourceObject, fieldOffset)); return; case BYTE_ACCESS: - UnsafeOps.putByte(targetObject, fieldOffset, UnsafeOps.getByte(sourceObject, fieldOffset)); + UNSAFE.putByte(targetObject, fieldOffset, UNSAFE.getByte(sourceObject, fieldOffset)); return; case CHAR_ACCESS: - UnsafeOps.putChar(targetObject, fieldOffset, UnsafeOps.getChar(sourceObject, fieldOffset)); + UNSAFE.putChar(targetObject, fieldOffset, UNSAFE.getChar(sourceObject, fieldOffset)); return; case SHORT_ACCESS: - UnsafeOps.putShort( - targetObject, fieldOffset, UnsafeOps.getShort(sourceObject, fieldOffset)); + UNSAFE.putShort(targetObject, fieldOffset, UNSAFE.getShort(sourceObject, fieldOffset)); return; case INT_ACCESS: - UnsafeOps.putInt(targetObject, fieldOffset, UnsafeOps.getInt(sourceObject, fieldOffset)); + UNSAFE.putInt(targetObject, fieldOffset, UNSAFE.getInt(sourceObject, fieldOffset)); return; case LONG_ACCESS: - UnsafeOps.putLong(targetObject, fieldOffset, UnsafeOps.getLong(sourceObject, fieldOffset)); + UNSAFE.putLong(targetObject, fieldOffset, UNSAFE.getLong(sourceObject, fieldOffset)); return; case FLOAT_ACCESS: - UnsafeOps.putFloat( - targetObject, fieldOffset, UnsafeOps.getFloat(sourceObject, fieldOffset)); + UNSAFE.putFloat(targetObject, fieldOffset, UNSAFE.getFloat(sourceObject, fieldOffset)); return; case DOUBLE_ACCESS: - UnsafeOps.putDouble( - targetObject, fieldOffset, UnsafeOps.getDouble(sourceObject, fieldOffset)); + UNSAFE.putDouble(targetObject, fieldOffset, UNSAFE.getDouble(sourceObject, fieldOffset)); return; case OBJECT_ACCESS: - UnsafeOps.putObject( - targetObject, fieldOffset, UnsafeOps.getObject(sourceObject, fieldOffset)); + UNSAFE.putObject(targetObject, fieldOffset, UNSAFE.getObject(sourceObject, fieldOffset)); return; default: putObject(targetObject, getObject(sourceObject)); @@ -162,8 +158,7 @@ public final void copy(Object sourceObject, Object targetObject) { public final void copyObject(Object sourceObject, Object targetObject) { if (accessKind == OBJECT_ACCESS) { - UnsafeOps.putObject( - targetObject, fieldOffset, UnsafeOps.getObject(sourceObject, fieldOffset)); + UNSAFE.putObject(targetObject, fieldOffset, UNSAFE.getObject(sourceObject, fieldOffset)); } else { putObject(targetObject, getObject(sourceObject)); } @@ -238,21 +233,21 @@ public void putDouble(Object targetObject, double 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); } @@ -401,7 +396,7 @@ public Object get(Object obj) { @Override public boolean getBoolean(Object obj) { checkObj(obj); - return UnsafeOps.getBoolean(obj, fieldOffset); + return UNSAFE.getBoolean(obj, fieldOffset); } @Override @@ -412,7 +407,7 @@ public void set(Object obj, Object value) { @Override public void putBoolean(Object obj, boolean value) { checkObj(obj); - UnsafeOps.putBoolean(obj, fieldOffset, value); + UNSAFE.putBoolean(obj, fieldOffset, value); } } @@ -452,7 +447,7 @@ public Byte get(Object obj) { @Override public byte getByte(Object obj) { checkObj(obj); - return UnsafeOps.getByte(obj, fieldOffset); + return UNSAFE.getByte(obj, fieldOffset); } @Override @@ -463,7 +458,7 @@ public void set(Object obj, Object value) { @Override public void putByte(Object obj, byte value) { checkObj(obj); - UnsafeOps.putByte(obj, fieldOffset, value); + UNSAFE.putByte(obj, fieldOffset, value); } } @@ -503,7 +498,7 @@ public Character get(Object obj) { @Override public char getChar(Object obj) { checkObj(obj); - return UnsafeOps.getChar(obj, fieldOffset); + return UNSAFE.getChar(obj, fieldOffset); } @Override @@ -514,7 +509,7 @@ public void set(Object obj, Object value) { @Override public void putChar(Object obj, char value) { checkObj(obj); - UnsafeOps.putChar(obj, fieldOffset, value); + UNSAFE.putChar(obj, fieldOffset, value); } } @@ -553,7 +548,7 @@ public Short get(Object obj) { @Override public short getShort(Object obj) { checkObj(obj); - return UnsafeOps.getShort(obj, fieldOffset); + return UNSAFE.getShort(obj, fieldOffset); } @Override @@ -564,7 +559,7 @@ public void set(Object obj, Object value) { @Override public void putShort(Object obj, short value) { checkObj(obj); - UnsafeOps.putShort(obj, fieldOffset, value); + UNSAFE.putShort(obj, fieldOffset, value); } } @@ -603,7 +598,7 @@ public Integer get(Object obj) { @Override public int getInt(Object obj) { checkObj(obj); - return UnsafeOps.getInt(obj, fieldOffset); + return UNSAFE.getInt(obj, fieldOffset); } @Override @@ -614,7 +609,7 @@ public void set(Object obj, Object value) { @Override public void putInt(Object obj, int value) { checkObj(obj); - UnsafeOps.putInt(obj, fieldOffset, value); + UNSAFE.putInt(obj, fieldOffset, value); } } @@ -653,7 +648,7 @@ public Long get(Object obj) { @Override public long getLong(Object obj) { checkObj(obj); - return UnsafeOps.getLong(obj, fieldOffset); + return UNSAFE.getLong(obj, fieldOffset); } @Override @@ -664,7 +659,7 @@ public void set(Object obj, Object value) { @Override public void putLong(Object obj, long value) { checkObj(obj); - UnsafeOps.putLong(obj, fieldOffset, value); + UNSAFE.putLong(obj, fieldOffset, value); } } @@ -703,7 +698,7 @@ public Object get(Object obj) { @Override public float getFloat(Object obj) { checkObj(obj); - return UnsafeOps.getFloat(obj, fieldOffset); + return UNSAFE.getFloat(obj, fieldOffset); } @Override @@ -714,7 +709,7 @@ public void set(Object obj, Object value) { @Override public void putFloat(Object obj, float value) { checkObj(obj); - UnsafeOps.putFloat(obj, fieldOffset, value); + UNSAFE.putFloat(obj, fieldOffset, value); } } @@ -753,7 +748,7 @@ public Object get(Object obj) { @Override public double getDouble(Object obj) { checkObj(obj); - return UnsafeOps.getDouble(obj, fieldOffset); + return UNSAFE.getDouble(obj, fieldOffset); } @Override @@ -764,7 +759,7 @@ public void set(Object obj, Object value) { @Override public void putDouble(Object obj, double value) { checkObj(obj); - UnsafeOps.putDouble(obj, fieldOffset, value); + UNSAFE.putDouble(obj, fieldOffset, value); } } @@ -798,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); } } @@ -854,18 +849,18 @@ static final class StaticObjectAccessor extends FieldAccessor { StaticObjectAccessor(Field field) { super(field, -1); Preconditions.checkArgument(!TypeUtils.isPrimitive(field.getType())); - base = UnsafeOps.UNSAFE.staticFieldBase(field); - offset = UnsafeOps.UNSAFE.staticFieldOffset(field); + base = UNSAFE.staticFieldBase(field); + offset = UNSAFE.staticFieldOffset(field); } @Override public Object get(Object obj) { - return UnsafeOps.getObject(base, offset); + return UNSAFE.getObject(base, offset); } @Override public void set(Object obj, Object value) { - UnsafeOps.putObject(base, offset, value); + UNSAFE.putObject(base, offset, value); } } 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 75f8c64558..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,7 +31,7 @@ * *

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 */ 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 682a9546be..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 @@ -40,11 +40,11 @@ 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 sun.misc.Unsafe; /** * Factory class for creating and caching {@link ObjectCreator} instances. @@ -58,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 @@ -74,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); @@ -94,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); @@ -529,7 +542,7 @@ public T newInstanceWithArguments(Object... arguments) { } } - public static final class UnsafeObjectCreator extends ObjectCreator { + private static final class UnsafeObjectCreator extends ObjectCreator { public UnsafeObjectCreator(Class type) { super(type); @@ -537,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 05f0e23712..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 @@ -454,7 +454,7 @@ 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); } 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 24f3d7f472..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 @@ -1004,7 +1004,6 @@ private boolean usesNonStructTypeDef(Class cls) { || isMap(cls) || Externalizable.class.isAssignableFrom(cls) || requireJavaSerialization(cls) - || requiresJavaSerializer(cls) || useReplaceResolveSerializer(cls) || Functions.isLambda(cls) || (ScalaTypes.SCALA_AVAILABLE && ReflectionUtils.isScalaSingletonObject(cls)) @@ -1546,9 +1545,6 @@ public Class getSerializerClass(Class cls, boolean code if (requiresJdkStream(cls)) { return getDefaultJDKStreamSerializerType(); } - if (requiresJavaSerializer(cls)) { - return JavaSerializer.class; - } if (isCrossLanguage()) { LOG.warn("Class {} isn't supported for cross-language serialization.", cls); } @@ -1646,18 +1642,6 @@ private static boolean requiresJdkStream(Class cls) { && !hasNoArgConstructor(cls); } - private static boolean requiresJavaSerializer(Class cls) { - if (JdkVersion.MAJOR_VERSION < 25 || !Serializable.class.isAssignableFrom(cls)) { - return false; - } - // Scala products can have final derived fields initialized by the primary constructor but not - // represented as constructor parameters. Keep that compatibility in the isolated JDK stream - // path instead of teaching the generic JDK25 field serializer to ignore final fields. - return ScalaTypes.SCALA_AVAILABLE - && ScalaTypes.isScalaProductType(cls) - && !ReflectionUtils.isScalaSingletonObject(cls); - } - private static boolean hasNoArgConstructor(Class cls) { try { cls.getDeclaredConstructor(); 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 d5e548fb45..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 @@ -34,8 +34,8 @@ 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.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; @@ -267,7 +267,7 @@ private T newInstance() { || 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; 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 6cdeab3b7a..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 @@ -385,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); 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 09b34f4318..e03dd8cdf2 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 @@ -252,12 +252,8 @@ private static ObjectCreator createObjectStreamCreator(Class 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); - } else if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { - return new ObjectCreators.UnsafeObjectCreator<>(type); - } else { - // In regular JVM, use the standard object creator - return ObjectCreators.getObjectCreator(type); } + return ObjectCreators.getObjectCreator(type); } private static boolean hasJdk25Fallback(Class type) { 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 index 6ac082acdc..d9770e1892 100644 --- 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 @@ -23,11 +23,17 @@ import org.apache.fory.memory.MemoryBuffer; import org.apache.fory.platform.AndroidSupport; import org.apache.fory.platform.GraalvmSupport; -import org.apache.fory.platform.UnsafeOps; import org.apache.fory.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 = + AndroidSupport.IS_ANDROID ? 0 : UNSAFE.arrayBaseOffset(byte[].class); + private static final int CHAR_ARRAY_OFFSET = + AndroidSupport.IS_ANDROID ? 0 : UNSAFE.arrayBaseOffset(char[].class); + static final boolean JDK_STRING_FIELD_ACCESS = !AndroidSupport.IS_ANDROID && !GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE @@ -54,19 +60,19 @@ final class PlatformStringUtils { private PlatformStringUtils() {} static Object getStringValue(String value) { - return UnsafeOps.getObject(value, STRING_VALUE_FIELD_OFFSET); + return UNSAFE.getObject(value, STRING_VALUE_FIELD_OFFSET); } static byte getStringCoder(String value) { - return UnsafeOps.getByte(value, STRING_CODER_FIELD_OFFSET); + return UNSAFE.getByte(value, STRING_CODER_FIELD_OFFSET); } static int getStringOffset(String value) { - return UnsafeOps.getInt(value, STRING_OFFSET_FIELD_OFFSET); + return UNSAFE.getInt(value, STRING_OFFSET_FIELD_OFFSET); } static int getStringCount(String value) { - return UnsafeOps.getInt(value, STRING_COUNT_FIELD_OFFSET); + return UNSAFE.getInt(value, STRING_COUNT_FIELD_OFFSET); } static String newCharsStringZeroCopy(char[] data) { @@ -98,29 +104,29 @@ private static String newBytesStringSlow(byte coder, byte[] data) { } static long getCharsLong(char[] chars, int charIndex) { - return UnsafeOps.getLong(chars, UnsafeOps.CHAR_ARRAY_OFFSET + ((long) charIndex << 1)); + return UNSAFE.getLong(chars, CHAR_ARRAY_OFFSET + ((long) charIndex << 1)); } static long getBytesLong(byte[] bytes, int byteIndex) { - return UnsafeOps.getLong(bytes, UnsafeOps.BYTE_ARRAY_OFFSET + byteIndex); + return UNSAFE.getLong(bytes, BYTE_ARRAY_OFFSET + byteIndex); } static char getBytesChar(byte[] bytes, int byteIndex) { - return UnsafeOps.getChar(bytes, UnsafeOps.BYTE_ARRAY_OFFSET + byteIndex); + return UNSAFE.getChar(bytes, BYTE_ARRAY_OFFSET + byteIndex); } static void copyCharsToBytes( char[] chars, int charOffset, byte[] target, int byteOffset, int numBytes) { - UnsafeOps.UNSAFE.copyMemory( + UNSAFE.copyMemory( chars, - UnsafeOps.CHAR_ARRAY_OFFSET + ((long) charOffset << 1), + CHAR_ARRAY_OFFSET + ((long) charOffset << 1), target, - UnsafeOps.BYTE_ARRAY_OFFSET + byteOffset, + BYTE_ARRAY_OFFSET + byteOffset, numBytes); } static void putBytes(MemoryBuffer buffer, int writerIndex, byte[] bytes, int numBytes) { long address = buffer._unsafeWriterAddress() + writerIndex - buffer.writerIndex(); - UnsafeOps.copyMemory(bytes, UnsafeOps.BYTE_ARRAY_OFFSET, null, address, numBytes); + UNSAFE.copyMemory(bytes, BYTE_ARRAY_OFFSET, null, address, numBytes); } } 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 7d808985d2..d8647f19d8 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 @@ -275,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); @@ -283,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); @@ -290,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); @@ -297,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); @@ -304,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); @@ -311,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; } } 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 index 5232671e14..86827aaeee 100644 --- 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 @@ -22,8 +22,6 @@ import static org.apache.fory.util.Preconditions.checkArgument; import static org.apache.fory.util.Preconditions.checkNotNull; -import java.lang.foreign.MemorySegment; -import java.lang.foreign.ValueLayout; import java.lang.invoke.MethodHandles; import java.lang.invoke.VarHandle; import java.nio.ByteBuffer; @@ -33,7 +31,6 @@ import org.apache.fory.io.AbstractStreamReader; import org.apache.fory.io.ForyStreamReader; import org.apache.fory.platform.AndroidSupport; -import org.apache.fory.platform.UnsafeOps; /** * A class for operations on memory managed by Fory. The buffer may be backed by heap memory (byte @@ -69,8 +66,16 @@ 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 = !AndroidSupport.IS_ANDROID && UnsafeOps.unaligned(); + 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 = @@ -87,14 +92,6 @@ public final class MemoryBuffer { MethodHandles.byteBufferViewVarHandle(int[].class, NATIVE_ORDER); private static final VarHandle BYTE_BUFFER_LONG = MethodHandles.byteBufferViewVarHandle(long[].class, NATIVE_ORDER); - private static final ValueLayout.OfChar NATIVE_CHAR = - ValueLayout.JAVA_CHAR_UNALIGNED.withOrder(NATIVE_ORDER); - private static final ValueLayout.OfShort NATIVE_SHORT = - ValueLayout.JAVA_SHORT_UNALIGNED.withOrder(NATIVE_ORDER); - private static final ValueLayout.OfInt NATIVE_INT = - ValueLayout.JAVA_INT_UNALIGNED.withOrder(NATIVE_ORDER); - private static final ValueLayout.OfLong NATIVE_LONG = - ValueLayout.JAVA_LONG_UNALIGNED.withOrder(NATIVE_ORDER); // Global allocator instance that can be customized private static volatile MemoryAllocator globalAllocator = new DefaultMemoryAllocator(); @@ -111,7 +108,6 @@ public final class MemoryBuffer { // the memory will not be released. ByteBuffer offHeapBuffer; ByteBuffer nativeOffHeapBuffer; - MemorySegment offHeapSegment; // 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`. @@ -124,7 +120,6 @@ public final class MemoryBuffer { int readerIndex; int writerIndex; final ForyStreamReader streamReader; - private final MemoryAccess memoryAccess = new MemoryAccess(this); // 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 @@ -205,10 +200,12 @@ private void initOffHeapBuffer(long offHeapAddress, int size, ByteBuffer offHeap 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; - this.nativeOffHeapBuffer = offHeapBuffer.duplicate().order(NATIVE_ORDER); - ByteBuffer segmentBuffer = offHeapBuffer.duplicate(); - ByteBufferUtil.position(segmentBuffer, 0); - this.offHeapSegment = MemorySegment.ofBuffer(segmentBuffer); + 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; @@ -277,9 +274,7 @@ public void initHeapBuffer(byte[] buffer, int offset, int length) { this.heapOffset = offset; this.offHeapBuffer = null; this.nativeOffHeapBuffer = null; - this.offHeapSegment = null; - // Versioned UnsafeOps array base offsets are zero on JDK25; address is a logical byte index. - 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; @@ -371,6 +366,240 @@ private void storeLong(long pos, long 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; @@ -500,7 +729,7 @@ public void get(int index, byte[] dst, int offset, int length) { < 0) { throwOOBException(); } - memoryAccess.copyMemory(null, pos, dst, UnsafeOps.BYTE_ARRAY_OFFSET + offset, length); + readBytesToArray(pos, dst, BYTE_ARRAY_OFFSET + offset, length); } } @@ -587,8 +816,7 @@ public void put(int index, byte[] src, int offset, int length) { < 0) { throwOOBException(); } - final long arrayAddress = UnsafeOps.BYTE_ARRAY_OFFSET + offset; - memoryAccess.copyMemory(src, arrayAddress, null, pos, length); + writeBytesFromArray(pos, src, BYTE_ARRAY_OFFSET + offset, length); } } @@ -1873,12 +2101,7 @@ public void writeBooleans(boolean[] values, int offset, int numElements) { final int writerIdx = writerIndex; final int newIdx = writerIdx + numElements; ensure(newIdx); - memoryAccess.copyMemory( - values, - UnsafeOps.BOOLEAN_ARRAY_OFFSET + offset, - heapMemory, - address + writerIdx, - numElements); + writeBooleansFromArray(address + writerIdx, values, BOOLEAN_ARRAY_OFFSET + offset, numElements); writerIndex = newIdx; } } @@ -1905,12 +2128,7 @@ public void writeChars(char[] values, int offset, int numElements) { final int writerIdx = writerIndex; final int newIdx = writerIdx + numBytes; ensure(newIdx); - memoryAccess.copyMemory( - values, - UnsafeOps.CHAR_ARRAY_OFFSET + ((long) offset << 1), - heapMemory, - address + writerIdx, - numBytes); + writeCharsFromArray(address + writerIdx, values, CHAR_ARRAY_OFFSET + offset, numBytes); writerIndex = newIdx; } } @@ -1937,12 +2155,7 @@ public void writeShorts(short[] values, int offset, int numElements) { final int writerIdx = writerIndex; final int newIdx = writerIdx + numBytes; ensure(newIdx); - memoryAccess.copyMemory( - values, - UnsafeOps.SHORT_ARRAY_OFFSET + ((long) offset << 1), - heapMemory, - address + writerIdx, - numBytes); + writeShortsFromArray(address + writerIdx, values, SHORT_ARRAY_OFFSET + offset, numBytes); writerIndex = newIdx; } } @@ -1969,12 +2182,7 @@ public void writeInts(int[] values, int offset, int numElements) { final int writerIdx = writerIndex; final int newIdx = writerIdx + numBytes; ensure(newIdx); - memoryAccess.copyMemory( - values, - UnsafeOps.INT_ARRAY_OFFSET + ((long) offset << 2), - heapMemory, - address + writerIdx, - numBytes); + writeIntsFromArray(address + writerIdx, values, INT_ARRAY_OFFSET + offset, numBytes); writerIndex = newIdx; } } @@ -2001,12 +2209,7 @@ public void writeLongs(long[] values, int offset, int numElements) { final int writerIdx = writerIndex; final int newIdx = writerIdx + numBytes; ensure(newIdx); - memoryAccess.copyMemory( - values, - UnsafeOps.LONG_ARRAY_OFFSET + ((long) offset << 3), - heapMemory, - address + writerIdx, - numBytes); + writeLongsFromArray(address + writerIdx, values, LONG_ARRAY_OFFSET + offset, numBytes); writerIndex = newIdx; } } @@ -2033,12 +2236,7 @@ public void writeFloats(float[] values, int offset, int numElements) { final int writerIdx = writerIndex; final int newIdx = writerIdx + numBytes; ensure(newIdx); - memoryAccess.copyMemory( - values, - UnsafeOps.FLOAT_ARRAY_OFFSET + ((long) offset << 2), - heapMemory, - address + writerIdx, - numBytes); + writeFloatsFromArray(address + writerIdx, values, FLOAT_ARRAY_OFFSET + offset, numBytes); writerIndex = newIdx; } } @@ -2065,12 +2263,7 @@ public void writeDoubles(double[] values, int offset, int numElements) { final int writerIdx = writerIndex; final int newIdx = writerIdx + numBytes; ensure(newIdx); - memoryAccess.copyMemory( - values, - UnsafeOps.DOUBLE_ARRAY_OFFSET + ((long) offset << 3), - heapMemory, - address + writerIdx, - numBytes); + writeDoublesFromArray(address + writerIdx, values, DOUBLE_ARRAY_OFFSET + offset, numBytes); writerIndex = newIdx; } } @@ -3244,13 +3437,7 @@ public byte[] readBytes(int length) { streamReader.readTo(bytes, 0, length); return bytes; } - byte[] heapMemory = this.heapMemory; - if (heapMemory != null) { - // System.arraycopy faster for some jdk than Unsafe. - System.arraycopy(heapMemory, heapOffset + readerIdx, bytes, 0, length); - } else { - memoryAccess.copyMemory(null, address + readerIdx, bytes, UnsafeOps.BYTE_ARRAY_OFFSET, length); - } + readBytesToArray(address + readerIdx, bytes, BYTE_ARRAY_OFFSET, length); readerIndex = readerIdx + length; return bytes; } @@ -3305,7 +3492,7 @@ private long slowReadBytesAsInt64(int remaining, int len) { } else { long start = address + readerIdx; for (int i = 0; i < len; i++) { - result |= ((long) memoryAccess.getByte(null, start + i) & 0xff) << (i * 8); + result |= ((long) loadByte(start + i) & 0xff) << (i * 8); } } return result; @@ -3415,13 +3602,7 @@ public byte[] readBytesAndSize() { streamReader.readTo(arr, 0, numBytes); return arr; } - byte[] heapMemory = this.heapMemory; - if (heapMemory != null) { - System.arraycopy(heapMemory, heapOffset + readerIdx, arr, 0, numBytes); - } else { - memoryAccess.copyMemory( - null, address + readerIdx, arr, UnsafeOps.BYTE_ARRAY_OFFSET, numBytes); - } + readBytesToArray(address + readerIdx, arr, BYTE_ARRAY_OFFSET, numBytes); readerIndex = readerIdx + numBytes; return arr; } @@ -3440,12 +3621,7 @@ public void readByteArrayPayload(byte[] values, int numBytes) { streamReader.readTo(values, 0, numBytes); return; } - byte[] heapMemory = this.heapMemory; - if (heapMemory != null) { - System.arraycopy(heapMemory, heapOffset + readerIdx, values, 0, numBytes); - } else { - memoryAccess.copyMemory(null, address + readerIdx, values, UnsafeOps.BYTE_ARRAY_OFFSET, numBytes); - } + readBytesToArray(address + readerIdx, values, BYTE_ARRAY_OFFSET, numBytes); readerIndex = readerIdx + numBytes; } } @@ -3464,8 +3640,7 @@ public void readBooleanArrayPayload(boolean[] values, int numBytes) { streamReader.readBooleans(values, 0, numBytes); return; } - memoryAccess.copyMemory( - heapMemory, address + readerIdx, values, UnsafeOps.BOOLEAN_ARRAY_OFFSET, numBytes); + readBooleansToArray(address + readerIdx, values, BOOLEAN_ARRAY_OFFSET, numBytes); readerIndex = readerIdx + numBytes; } } @@ -3484,8 +3659,7 @@ public void readCharArrayPayload(char[] values, int numBytes) { streamReader.readChars(values, 0, numBytes >>> 1); return; } - memoryAccess.copyMemory( - heapMemory, address + readerIdx, values, UnsafeOps.CHAR_ARRAY_OFFSET, numBytes); + readCharsToArray(address + readerIdx, values, CHAR_ARRAY_OFFSET, numBytes); readerIndex = readerIdx + numBytes; } } @@ -3504,8 +3678,7 @@ public void readInt16ArrayPayload(short[] values, int numBytes) { streamReader.readShorts(values, 0, numBytes >>> 1); return; } - memoryAccess.copyMemory( - heapMemory, address + readerIdx, values, UnsafeOps.SHORT_ARRAY_OFFSET, numBytes); + readShortsToArray(address + readerIdx, values, SHORT_ARRAY_OFFSET, numBytes); readerIndex = readerIdx + numBytes; } } @@ -3524,8 +3697,7 @@ public void readInt32ArrayPayload(int[] values, int numBytes) { streamReader.readInts(values, 0, numBytes >>> 2); return; } - memoryAccess.copyMemory( - heapMemory, address + readerIdx, values, UnsafeOps.INT_ARRAY_OFFSET, numBytes); + readIntsToArray(address + readerIdx, values, INT_ARRAY_OFFSET, numBytes); readerIndex = readerIdx + numBytes; } } @@ -3544,8 +3716,7 @@ public void readInt64ArrayPayload(long[] values, int numBytes) { streamReader.readLongs(values, 0, numBytes >>> 3); return; } - memoryAccess.copyMemory( - heapMemory, address + readerIdx, values, UnsafeOps.LONG_ARRAY_OFFSET, numBytes); + readLongsToArray(address + readerIdx, values, LONG_ARRAY_OFFSET, numBytes); readerIndex = readerIdx + numBytes; } } @@ -3564,8 +3735,7 @@ public void readFloat32ArrayPayload(float[] values, int numBytes) { streamReader.readFloats(values, 0, numBytes >>> 2); return; } - memoryAccess.copyMemory( - heapMemory, address + readerIdx, values, UnsafeOps.FLOAT_ARRAY_OFFSET, numBytes); + readFloatsToArray(address + readerIdx, values, FLOAT_ARRAY_OFFSET, numBytes); readerIndex = readerIdx + numBytes; } } @@ -3584,8 +3754,7 @@ public void readFloat64ArrayPayload(double[] values, int numBytes) { streamReader.readDoubles(values, 0, numBytes >>> 3); return; } - memoryAccess.copyMemory( - heapMemory, address + readerIdx, values, UnsafeOps.DOUBLE_ARRAY_OFFSET, numBytes); + readDoublesToArray(address + readerIdx, values, DOUBLE_ARRAY_OFFSET, numBytes); readerIndex = readerIdx + numBytes; } } @@ -3603,12 +3772,7 @@ public void readBooleans(boolean[] values, int offset, int numElements) { return; } int readerIdx = readerIndex; - memoryAccess.copyMemory( - heapMemory, - address + readerIdx, - values, - UnsafeOps.BOOLEAN_ARRAY_OFFSET + offset, - numElements); + readBooleansToArray(address + readerIdx, values, BOOLEAN_ARRAY_OFFSET + offset, numElements); readerIndex = readerIdx + numElements; } } @@ -3631,12 +3795,7 @@ public void readChars(char[] chars, int offset, int numElements) { return; } int readerIdx = readerIndex; - memoryAccess.copyMemory( - heapMemory, - address + readerIdx, - chars, - UnsafeOps.CHAR_ARRAY_OFFSET + ((long) offset << 1), - numBytes); + readCharsToArray(address + readerIdx, chars, CHAR_ARRAY_OFFSET + offset, numBytes); readerIndex = readerIdx + numBytes; } } @@ -3664,12 +3823,7 @@ public void readShorts(short[] values, int offset, int numElements) { return; } int readerIdx = readerIndex; - memoryAccess.copyMemory( - heapMemory, - address + readerIdx, - values, - UnsafeOps.SHORT_ARRAY_OFFSET + ((long) offset << 1), - numBytes); + readShortsToArray(address + readerIdx, values, SHORT_ARRAY_OFFSET + offset, numBytes); readerIndex = readerIdx + numBytes; } } @@ -3688,12 +3842,7 @@ public void readInts(int[] values, int offset, int numElements) { return; } int readerIdx = readerIndex; - memoryAccess.copyMemory( - heapMemory, - address + readerIdx, - values, - UnsafeOps.INT_ARRAY_OFFSET + ((long) offset << 2), - numBytes); + readIntsToArray(address + readerIdx, values, INT_ARRAY_OFFSET + offset, numBytes); readerIndex = readerIdx + numBytes; } } @@ -3712,12 +3861,7 @@ public void readLongs(long[] values, int offset, int numElements) { return; } int readerIdx = readerIndex; - memoryAccess.copyMemory( - heapMemory, - address + readerIdx, - values, - UnsafeOps.LONG_ARRAY_OFFSET + ((long) offset << 3), - numBytes); + readLongsToArray(address + readerIdx, values, LONG_ARRAY_OFFSET + offset, numBytes); readerIndex = readerIdx + numBytes; } } @@ -3736,12 +3880,7 @@ public void readFloats(float[] values, int offset, int numElements) { return; } int readerIdx = readerIndex; - memoryAccess.copyMemory( - heapMemory, - address + readerIdx, - values, - UnsafeOps.FLOAT_ARRAY_OFFSET + ((long) offset << 2), - numBytes); + readFloatsToArray(address + readerIdx, values, FLOAT_ARRAY_OFFSET + offset, numBytes); readerIndex = readerIdx + numBytes; } } @@ -3760,12 +3899,7 @@ public void readDoubles(double[] values, int offset, int numElements) { return; } int readerIdx = readerIndex; - memoryAccess.copyMemory( - heapMemory, - address + readerIdx, - values, - UnsafeOps.DOUBLE_ARRAY_OFFSET + ((long) offset << 3), - numBytes); + readDoublesToArray(address + readerIdx, values, DOUBLE_ARRAY_OFFSET + offset, numBytes); readerIndex = readerIdx + numBytes; } } @@ -3805,7 +3939,7 @@ public void copyTo(int offset, MemoryBuffer target, int targetOffset, int numByt && thisPointer <= this.addressLimit - numBytes && otherPointer <= target.addressLimit - numBytes) { if (thisHeapRef != null && otherHeapRef != null) { - memoryAccess.copyMemory(thisHeapRef, thisPointer, otherHeapRef, otherPointer, numBytes); + 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); @@ -3849,7 +3983,7 @@ public void copyToByteArray(int offset, byte[] target, int targetOffset, int num MemoryOps.copyToByteArray(this, offset, target, targetOffset, numBytes); } else { checkArrayCopy(offset, targetOffset, target.length, numBytes, 0); - memoryAccess.copyMemory(heapMemory, address + offset, target, targetOffset, numBytes); + readBytesToArray(address + offset, target, BYTE_ARRAY_OFFSET + targetOffset, numBytes); } } @@ -3858,7 +3992,7 @@ public void copyToBooleanArray(int offset, boolean[] target, int targetOffset, i MemoryOps.copyToBooleanArray(this, offset, target, targetOffset, numBytes); } else { checkArrayCopy(offset, targetOffset, target.length, numBytes, 0); - memoryAccess.copyMemory(heapMemory, address + offset, target, targetOffset, numBytes); + readBooleansToArray(address + offset, target, BOOLEAN_ARRAY_OFFSET + targetOffset, numBytes); } } @@ -3867,8 +4001,7 @@ public void copyToCharArray(int offset, char[] target, int targetOffset, int num MemoryOps.copyToCharArray(this, offset, target, targetOffset, numBytes); } else { checkArrayCopy(offset, targetOffset, target.length, numBytes, 1); - memoryAccess.copyMemory( - heapMemory, address + offset, target, arrayCopyOffset(targetOffset, 1), numBytes); + readCharsToArray(address + offset, target, CHAR_ARRAY_OFFSET + targetOffset, numBytes); } } @@ -3877,8 +4010,7 @@ public void copyToShortArray(int offset, short[] target, int targetOffset, int n MemoryOps.copyToShortArray(this, offset, target, targetOffset, numBytes); } else { checkArrayCopy(offset, targetOffset, target.length, numBytes, 1); - memoryAccess.copyMemory( - heapMemory, address + offset, target, arrayCopyOffset(targetOffset, 1), numBytes); + readShortsToArray(address + offset, target, SHORT_ARRAY_OFFSET + targetOffset, numBytes); } } @@ -3887,8 +4019,7 @@ public void copyToIntArray(int offset, int[] target, int targetOffset, int numBy MemoryOps.copyToIntArray(this, offset, target, targetOffset, numBytes); } else { checkArrayCopy(offset, targetOffset, target.length, numBytes, 2); - memoryAccess.copyMemory( - heapMemory, address + offset, target, arrayCopyOffset(targetOffset, 2), numBytes); + readIntsToArray(address + offset, target, INT_ARRAY_OFFSET + targetOffset, numBytes); } } @@ -3897,8 +4028,7 @@ public void copyToLongArray(int offset, long[] target, int targetOffset, int num MemoryOps.copyToLongArray(this, offset, target, targetOffset, numBytes); } else { checkArrayCopy(offset, targetOffset, target.length, numBytes, 3); - memoryAccess.copyMemory( - heapMemory, address + offset, target, arrayCopyOffset(targetOffset, 3), numBytes); + readLongsToArray(address + offset, target, LONG_ARRAY_OFFSET + targetOffset, numBytes); } } @@ -3907,8 +4037,7 @@ public void copyToFloatArray(int offset, float[] target, int targetOffset, int n MemoryOps.copyToFloatArray(this, offset, target, targetOffset, numBytes); } else { checkArrayCopy(offset, targetOffset, target.length, numBytes, 2); - memoryAccess.copyMemory( - heapMemory, address + offset, target, arrayCopyOffset(targetOffset, 2), numBytes); + readFloatsToArray(address + offset, target, FLOAT_ARRAY_OFFSET + targetOffset, numBytes); } } @@ -3917,8 +4046,7 @@ public void copyToDoubleArray(int offset, double[] target, int targetOffset, int MemoryOps.copyToDoubleArray(this, offset, target, targetOffset, numBytes); } else { checkArrayCopy(offset, targetOffset, target.length, numBytes, 3); - memoryAccess.copyMemory( - heapMemory, address + offset, target, arrayCopyOffset(targetOffset, 3), numBytes); + readDoublesToArray(address + offset, target, DOUBLE_ARRAY_OFFSET + targetOffset, numBytes); } } @@ -3936,22 +4064,76 @@ private void checkArrayCopy( } } - private static long arrayCopyOffset(int elementOffset, int elementShift) { - return (long) elementOffset << elementShift; + 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); + } } - /** - * 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 copyFromCharArray(int offset, char[] source, int sourceOffset, int numBytes) { if (AndroidSupport.IS_ANDROID) { - MemoryOps.throwRawUnsafeMemoryCopyUnsupported(); + MemoryOps.copyFromCharArray(this, offset, source, sourceOffset, numBytes); } else { - checkArgument(source != null, "Raw native-address source copy is unsupported on JDK25"); - final long thisPointer = this.address + offset; - checkArgument(thisPointer + numBytes <= addressLimit); - memoryAccess.copyMemory(source, sourcePointer, heapMemory, thisPointer, numBytes); + checkArrayCopy(offset, sourceOffset, source.length, numBytes, 1); + writeCharsFromArray(address + offset, source, CHAR_ARRAY_OFFSET + sourceOffset, numBytes); + } + } + + public void copyFromShortArray(int offset, short[] source, int sourceOffset, int numBytes) { + 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); } } @@ -4008,6 +4190,7 @@ public ByteBuffer sliceAsByteBuffer(int offset, int length) { 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(); @@ -4046,7 +4229,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 unsafeEqualTo(memoryAccess, heapMemory, pos1, buf2.memoryAccess, buf2.heapMemory, pos2, len); + return unsafeEqualTo(this, heapMemory, pos1, buf2, buf2.heapMemory, pos2, len); } /** @@ -4070,23 +4253,21 @@ 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 unsafeEqualTo( - memoryAccess, heapMemory, pos, memoryAccess, bytes, UnsafeOps.BYTE_ARRAY_OFFSET + bytesOffset, len); + return unsafeEqualTo(this, heapMemory, pos, this, bytes, BYTE_ARRAY_OFFSET + bytesOffset, len); } private static boolean unsafeEqualTo( - MemoryAccess leftAccess, + MemoryBuffer left, Object leftBase, long leftOffset, - MemoryAccess rightAccess, + MemoryBuffer right, Object rightBase, long rightOffset, int length) { int i = 0; if ((leftOffset % 8) == (rightOffset % 8)) { while ((leftOffset + i) % 8 != 0 && i < length) { - if (leftAccess.getByte(leftBase, leftOffset + i) - != rightAccess.getByte(rightBase, rightOffset + i)) { + if (left.rawByte(leftBase, leftOffset + i) != right.rawByte(rightBase, rightOffset + i)) { return false; } i += 1; @@ -4094,16 +4275,14 @@ private static boolean unsafeEqualTo( } if (UNALIGNED || (((leftOffset + i) % 8 == 0) && ((rightOffset + i) % 8 == 0))) { while (i <= length - 8) { - if (leftAccess.getLong(leftBase, leftOffset + i) - != rightAccess.getLong(rightBase, rightOffset + i)) { + if (left.rawLong(leftBase, leftOffset + i) != right.rawLong(rightBase, rightOffset + i)) { return false; } i += 8; } } while (i < length) { - if (leftAccess.getByte(leftBase, leftOffset + i) - != rightAccess.getByte(rightBase, rightOffset + i)) { + if (left.rawByte(leftBase, leftOffset + i) != right.rawByte(rightBase, rightOffset + i)) { return false; } i += 1; @@ -4111,361 +4290,18 @@ private static boolean unsafeEqualTo( return true; } - private static final class MemoryAccess { - private final MemoryBuffer buffer; - - private MemoryAccess(MemoryBuffer buffer) { - this.buffer = buffer; - } - - private byte getByte(Object base, long offset) { - if (base != null) { - return UnsafeOps.getByte(base, offset); - } - return directSegment().get(ValueLayout.JAVA_BYTE, offset); - } - - private void putByte(Object base, long offset, byte value) { - if (base != null) { - UnsafeOps.putByte(base, offset, value); - return; - } - directSegment().set(ValueLayout.JAVA_BYTE, offset, value); - } - - private char getChar(Object base, long offset) { - if (base != null) { - return UnsafeOps.getChar(base, offset); - } - return directSegment().get(NATIVE_CHAR, offset); - } - - private void putChar(Object base, long offset, char value) { - if (base != null) { - UnsafeOps.putChar(base, offset, value); - return; - } - directSegment().set(NATIVE_CHAR, offset, value); - } - - private short getShort(Object base, long offset) { - if (base != null) { - return UnsafeOps.getShort(base, offset); - } - return directSegment().get(NATIVE_SHORT, offset); - } - - private void putShort(Object base, long offset, short value) { - if (base != null) { - UnsafeOps.putShort(base, offset, value); - return; - } - directSegment().set(NATIVE_SHORT, offset, value); - } - - private int getInt(Object base, long offset) { - if (base != null) { - return UnsafeOps.getInt(base, offset); - } - return directSegment().get(NATIVE_INT, offset); - } - - private void putInt(Object base, long offset, int value) { - if (base != null) { - UnsafeOps.putInt(base, offset, value); - return; - } - directSegment().set(NATIVE_INT, offset, value); - } - - private long getLong(Object base, long offset) { - if (base != null) { - return UnsafeOps.getLong(base, offset); - } - return directSegment().get(NATIVE_LONG, offset); - } - - private void putLong(Object base, long offset, long value) { - if (base != null) { - UnsafeOps.putLong(base, offset, value); - return; - } - directSegment().set(NATIVE_LONG, offset, value); - } - - private void copyMemory( - Object src, long srcOffset, Object dst, long dstOffset, long length) { - int len = toIntLength(length); - if (len == 0) { - return; - } - if (src != null && dst != null) { - if (dst instanceof byte[] && copyArrayToBytes(src, srcOffset, (byte[]) dst, toIntIndex(dstOffset), len)) { - return; - } - if (src instanceof byte[] && copyBytesToArray((byte[]) src, toIntIndex(srcOffset), dst, dstOffset, len)) { - return; - } - UnsafeOps.copyMemory(src, srcOffset, dst, dstOffset, len); - } else if (src == null && dst == null) { - copyDirect(srcOffset, dstOffset, len); - } else if (src == null) { - if (dst instanceof byte[]) { - readDirect(srcOffset, (byte[]) dst, toIntIndex(dstOffset), len); - } else if (readArray(srcOffset, dst, dstOffset, len)) { - return; - } else { - for (int i = 0; i < len; i++) { - UnsafeOps.putByte(dst, dstOffset + i, getByte(null, srcOffset + i)); - } - } - } else if (src instanceof byte[]) { - writeDirect(dstOffset, (byte[]) src, toIntIndex(srcOffset), len); - } else if (writeArray(src, srcOffset, dstOffset, len)) { - return; - } else { - for (int i = 0; i < len; i++) { - putByte(null, dstOffset + i, UnsafeOps.getByte(src, srcOffset + i)); - } - } - } - - private boolean copyArrayToBytes( - Object src, long srcOffset, byte[] dst, int dstOffset, int len) { - if (src instanceof boolean[]) { - boolean[] array = (boolean[]) src; - int srcIndex = toIntIndex(srcOffset); - for (int i = 0; i < len; i++) { - dst[dstOffset + i] = array[srcIndex + i] ? (byte) 1 : (byte) 0; - } - return true; - } else if (src instanceof char[] && aligned(srcOffset, len, Character.BYTES)) { - heapBytes(dst, dstOffset, len) - .asCharBuffer() - .put((char[]) src, toIntIndex(srcOffset / Character.BYTES), len / Character.BYTES); - return true; - } else if (src instanceof short[] && aligned(srcOffset, len, Short.BYTES)) { - heapBytes(dst, dstOffset, len) - .asShortBuffer() - .put((short[]) src, toIntIndex(srcOffset / Short.BYTES), len / Short.BYTES); - return true; - } else if (src instanceof int[] && aligned(srcOffset, len, Integer.BYTES)) { - heapBytes(dst, dstOffset, len) - .asIntBuffer() - .put((int[]) src, toIntIndex(srcOffset / Integer.BYTES), len / Integer.BYTES); - return true; - } else if (src instanceof long[] && aligned(srcOffset, len, Long.BYTES)) { - heapBytes(dst, dstOffset, len) - .asLongBuffer() - .put((long[]) src, toIntIndex(srcOffset / Long.BYTES), len / Long.BYTES); - return true; - } else if (src instanceof float[] && aligned(srcOffset, len, Float.BYTES)) { - heapBytes(dst, dstOffset, len) - .asFloatBuffer() - .put((float[]) src, toIntIndex(srcOffset / Float.BYTES), len / Float.BYTES); - return true; - } else if (src instanceof double[] && aligned(srcOffset, len, Double.BYTES)) { - heapBytes(dst, dstOffset, len) - .asDoubleBuffer() - .put((double[]) src, toIntIndex(srcOffset / Double.BYTES), len / Double.BYTES); - return true; - } - return false; - } - - private boolean copyBytesToArray( - byte[] src, int srcOffset, Object dst, long dstOffset, int len) { - if (dst instanceof boolean[]) { - boolean[] array = (boolean[]) dst; - int dstIndex = toIntIndex(dstOffset); - for (int i = 0; i < len; i++) { - array[dstIndex + i] = src[srcOffset + i] != 0; - } - return true; - } else if (dst instanceof char[] && aligned(dstOffset, len, Character.BYTES)) { - heapBytes(src, srcOffset, len) - .asCharBuffer() - .get((char[]) dst, toIntIndex(dstOffset / Character.BYTES), len / Character.BYTES); - return true; - } else if (dst instanceof short[] && aligned(dstOffset, len, Short.BYTES)) { - heapBytes(src, srcOffset, len) - .asShortBuffer() - .get((short[]) dst, toIntIndex(dstOffset / Short.BYTES), len / Short.BYTES); - return true; - } else if (dst instanceof int[] && aligned(dstOffset, len, Integer.BYTES)) { - heapBytes(src, srcOffset, len) - .asIntBuffer() - .get((int[]) dst, toIntIndex(dstOffset / Integer.BYTES), len / Integer.BYTES); - return true; - } else if (dst instanceof long[] && aligned(dstOffset, len, Long.BYTES)) { - heapBytes(src, srcOffset, len) - .asLongBuffer() - .get((long[]) dst, toIntIndex(dstOffset / Long.BYTES), len / Long.BYTES); - return true; - } else if (dst instanceof float[] && aligned(dstOffset, len, Float.BYTES)) { - heapBytes(src, srcOffset, len) - .asFloatBuffer() - .get((float[]) dst, toIntIndex(dstOffset / Float.BYTES), len / Float.BYTES); - return true; - } else if (dst instanceof double[] && aligned(dstOffset, len, Double.BYTES)) { - heapBytes(src, srcOffset, len) - .asDoubleBuffer() - .get((double[]) dst, toIntIndex(dstOffset / Double.BYTES), len / Double.BYTES); - return true; - } - return false; - } - - private boolean writeArray(Object src, long srcOffset, long dstOffset, int len) { - if (src instanceof boolean[]) { - boolean[] array = (boolean[]) src; - int srcIndex = toIntIndex(srcOffset); - ByteBuffer dst = directBytes(dstOffset, len); - for (int i = 0; i < len; i++) { - dst.put(i, array[srcIndex + i] ? (byte) 1 : (byte) 0); - } - return true; - } else if (src instanceof char[] && aligned(srcOffset, len, Character.BYTES)) { - directBytes(dstOffset, len) - .asCharBuffer() - .put((char[]) src, toIntIndex(srcOffset / Character.BYTES), len / Character.BYTES); - return true; - } else if (src instanceof short[] && aligned(srcOffset, len, Short.BYTES)) { - directBytes(dstOffset, len) - .asShortBuffer() - .put((short[]) src, toIntIndex(srcOffset / Short.BYTES), len / Short.BYTES); - return true; - } else if (src instanceof int[] && aligned(srcOffset, len, Integer.BYTES)) { - directBytes(dstOffset, len) - .asIntBuffer() - .put((int[]) src, toIntIndex(srcOffset / Integer.BYTES), len / Integer.BYTES); - return true; - } else if (src instanceof long[] && aligned(srcOffset, len, Long.BYTES)) { - directBytes(dstOffset, len) - .asLongBuffer() - .put((long[]) src, toIntIndex(srcOffset / Long.BYTES), len / Long.BYTES); - return true; - } else if (src instanceof float[] && aligned(srcOffset, len, Float.BYTES)) { - directBytes(dstOffset, len) - .asFloatBuffer() - .put((float[]) src, toIntIndex(srcOffset / Float.BYTES), len / Float.BYTES); - return true; - } else if (src instanceof double[] && aligned(srcOffset, len, Double.BYTES)) { - directBytes(dstOffset, len) - .asDoubleBuffer() - .put((double[]) src, toIntIndex(srcOffset / Double.BYTES), len / Double.BYTES); - return true; - } - return false; - } - - private boolean readArray(long srcOffset, Object dst, long dstOffset, int len) { - if (dst instanceof boolean[]) { - boolean[] array = (boolean[]) dst; - int dstIndex = toIntIndex(dstOffset); - ByteBuffer src = directBytes(srcOffset, len); - for (int i = 0; i < len; i++) { - array[dstIndex + i] = src.get(i) != 0; - } - return true; - } else if (dst instanceof char[] && aligned(dstOffset, len, Character.BYTES)) { - directBytes(srcOffset, len) - .asCharBuffer() - .get((char[]) dst, toIntIndex(dstOffset / Character.BYTES), len / Character.BYTES); - return true; - } else if (dst instanceof short[] && aligned(dstOffset, len, Short.BYTES)) { - directBytes(srcOffset, len) - .asShortBuffer() - .get((short[]) dst, toIntIndex(dstOffset / Short.BYTES), len / Short.BYTES); - return true; - } else if (dst instanceof int[] && aligned(dstOffset, len, Integer.BYTES)) { - directBytes(srcOffset, len) - .asIntBuffer() - .get((int[]) dst, toIntIndex(dstOffset / Integer.BYTES), len / Integer.BYTES); - return true; - } else if (dst instanceof long[] && aligned(dstOffset, len, Long.BYTES)) { - directBytes(srcOffset, len) - .asLongBuffer() - .get((long[]) dst, toIntIndex(dstOffset / Long.BYTES), len / Long.BYTES); - return true; - } else if (dst instanceof float[] && aligned(dstOffset, len, Float.BYTES)) { - directBytes(srcOffset, len) - .asFloatBuffer() - .get((float[]) dst, toIntIndex(dstOffset / Float.BYTES), len / Float.BYTES); - return true; - } else if (dst instanceof double[] && aligned(dstOffset, len, Double.BYTES)) { - directBytes(srcOffset, len) - .asDoubleBuffer() - .get((double[]) dst, toIntIndex(dstOffset / Double.BYTES), len / Double.BYTES); - return true; - } - return false; - } - - private void copyDirect(long srcOffset, long dstOffset, int len) { - if (srcOffset < dstOffset && dstOffset < srcOffset + len) { - byte[] tmp = new byte[len]; - readDirect(srcOffset, tmp, 0, len); - writeDirect(dstOffset, tmp, 0, len); - } else { - MemorySegment segment = directSegment(); - MemorySegment.copy(segment, srcOffset, segment, dstOffset, len); - } - } - - private ByteBuffer directBuffer() { - ByteBuffer directBuffer = buffer.nativeOffHeapBuffer; - if (directBuffer == null) { - throw new IllegalStateException("Memory buffer does not own a ByteBuffer"); - } - return directBuffer; - } - - private MemorySegment directSegment() { - MemorySegment segment = buffer.offHeapSegment; - if (segment == null) { - throw new IllegalStateException("Memory buffer does not own an off-heap segment"); - } - return segment; - } - - private void readDirect(long offset, byte[] dst, int dstOffset, int length) { - directBuffer().get(toIntIndex(offset), dst, dstOffset, length); - } - - private void writeDirect(long offset, byte[] src, int srcOffset, int length) { - directBuffer().put(toIntIndex(offset), src, srcOffset, length); - } - - private ByteBuffer directBytes(long offset, int length) { - ByteBuffer duplicate = directBuffer().duplicate().order(NATIVE_ORDER); - int start = toIntIndex(offset); - ByteBufferUtil.position(duplicate, start); - duplicate.limit(start + length); - return duplicate.slice().order(NATIVE_ORDER); - } - - private static ByteBuffer heapBytes(byte[] bytes, int offset, int length) { - return ByteBuffer.wrap(bytes, offset, length).order(NATIVE_ORDER); - } - - private static boolean aligned(long offset, int length, int width) { - return offset % width == 0 && length % width == 0; - } - - 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 byte rawByte(Object base, long offset) { + if (base == null) { + return loadByte(offset); } + return ((byte[]) base)[toIntIndex(offset)]; + } - private static int toIntLength(long length) { - if (length < 0 || length > Integer.MAX_VALUE) { - throw new IndexOutOfBoundsException("length out of int range: " + length); - } - return (int) length; + private long rawLong(Object base, long offset) { + if (base == null) { + return loadLong(offset); } + return (long) BYTE_ARRAY_LONG.get((byte[]) base, toIntIndex(offset)); } @Override diff --git a/java/fory-core/src/main/java25/org/apache/fory/platform/UnsafeOps.java b/java/fory-core/src/main/java25/org/apache/fory/platform/UnsafeOps.java deleted file mode 100644 index cd478704d3..0000000000 --- a/java/fory-core/src/main/java25/org/apache/fory/platform/UnsafeOps.java +++ /dev/null @@ -1,497 +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.invoke.MethodHandles; -import java.lang.invoke.VarHandle; -import java.lang.reflect.Field; -import java.nio.ByteOrder; -import org.apache.fory.annotation.Internal; -import org.apache.fory.platform.internal._JDKAccess; -import sun.misc.Unsafe; - -/** A utility class for array memory operations on JDK25+. */ -@Internal -@SuppressWarnings("restriction") -public final class UnsafeOps { - @SuppressWarnings("restriction") - public static final Unsafe UNSAFE = _JDKAccess.UNSAFE; - - // JDK25 array operations use Java/VarHandle indexes instead of raw Unsafe byte offsets. - // Keep these constants zero so versioned MemoryBuffer code can preserve the root API shape - // without mixing Unsafe base-offset domains into the zero-Unsafe runtime. - public static final int BOOLEAN_ARRAY_OFFSET = 0; - public static final int BYTE_ARRAY_OFFSET = 0; - public static final int CHAR_ARRAY_OFFSET = 0; - public static final int SHORT_ARRAY_OFFSET = 0; - public static final int INT_ARRAY_OFFSET = 0; - public static final int LONG_ARRAY_OFFSET = 0; - public static final int FLOAT_ARRAY_OFFSET = 0; - public static final int DOUBLE_ARRAY_OFFSET = 0; - private static final boolean BIG_ENDIAN = ByteOrder.nativeOrder() == ByteOrder.BIG_ENDIAN; - private static final VarHandle BYTE_ARRAY_CHAR = - MethodHandles.byteArrayViewVarHandle(char[].class, ByteOrder.nativeOrder()); - private static final VarHandle BYTE_ARRAY_SHORT = - MethodHandles.byteArrayViewVarHandle(short[].class, ByteOrder.nativeOrder()); - private static final VarHandle BYTE_ARRAY_INT = - MethodHandles.byteArrayViewVarHandle(int[].class, ByteOrder.nativeOrder()); - private static final VarHandle BYTE_ARRAY_LONG = - MethodHandles.byteArrayViewVarHandle(long[].class, ByteOrder.nativeOrder()); - private static final VarHandle BYTE_ARRAY_FLOAT = - MethodHandles.byteArrayViewVarHandle(float[].class, ByteOrder.nativeOrder()); - private static final VarHandle BYTE_ARRAY_DOUBLE = - MethodHandles.byteArrayViewVarHandle(double[].class, ByteOrder.nativeOrder()); - private static final boolean unaligned; - - private UnsafeOps() {} - - static { - String arch = System.getProperty("os.arch", ""); - if ("ppc64le".equals(arch) || "ppc64".equals(arch) || "s390x".equals(arch)) { - unaligned = true; - } else { - unaligned = arch.matches("^(i[3-6]86|x86(_64)?|x64|amd64|aarch64)$"); - } - } - - /** - * Returns true when the underlying system is known to support unaligned access. JDK25 array - * accessors do not use Unsafe, but callers keep this gate for vectorized array scans. - */ - public static boolean unaligned() { - return unaligned; - } - - public static long objectFieldOffset(Field f) { - throw unsupportedObjectMemory(); - } - - public static int getInt(Object object, long offset) { - if (object instanceof byte[]) { - return (int) BYTE_ARRAY_INT.get((byte[]) object, toIntIndex(offset)); - } - return getIntFromArray(object, offset); - } - - public static void putInt(Object object, long offset, int value) { - if (object instanceof byte[]) { - BYTE_ARRAY_INT.set((byte[]) object, toIntIndex(offset), value); - return; - } - putIntToArray(object, offset, value); - } - - public static boolean getBoolean(Object object, long offset) { - if (object instanceof boolean[]) { - return ((boolean[]) object)[toIntIndex(offset)]; - } - return getByte(object, offset) != 0; - } - - public static void putBoolean(Object object, long offset, boolean value) { - if (object instanceof boolean[]) { - ((boolean[]) object)[toIntIndex(offset)] = value; - return; - } - putByte(object, offset, value ? (byte) 1 : (byte) 0); - } - - public static byte getByte(Object object, long offset) { - return getArrayByte(object, offset); - } - - public static void putByte(Object object, long offset, byte value) { - putArrayByte(object, offset, value); - } - - public static short getShort(Object object, long offset) { - if (object instanceof byte[]) { - return (short) BYTE_ARRAY_SHORT.get((byte[]) object, toIntIndex(offset)); - } - return (short) getIntN(object, offset, Short.BYTES); - } - - public static void putShort(Object object, long offset, short value) { - if (object instanceof byte[]) { - BYTE_ARRAY_SHORT.set((byte[]) object, toIntIndex(offset), value); - return; - } - putIntN(object, offset, value, Short.BYTES); - } - - public static char getChar(Object obj, long offset) { - if (obj instanceof byte[]) { - return (char) BYTE_ARRAY_CHAR.get((byte[]) obj, toIntIndex(offset)); - } - return (char) getIntN(obj, offset, Character.BYTES); - } - - public static void putChar(Object obj, long offset, char value) { - if (obj instanceof byte[]) { - BYTE_ARRAY_CHAR.set((byte[]) obj, toIntIndex(offset), value); - return; - } - putIntN(obj, offset, value, Character.BYTES); - } - - public static long getLong(Object object, long offset) { - if (object instanceof byte[]) { - return (long) BYTE_ARRAY_LONG.get((byte[]) object, toIntIndex(offset)); - } - long value = 0; - if (BIG_ENDIAN) { - for (int i = 0; i < Long.BYTES; i++) { - value = (value << Byte.SIZE) | (getArrayByte(object, offset + i) & 0xffL); - } - } else { - for (int i = Long.BYTES - 1; i >= 0; i--) { - value = (value << Byte.SIZE) | (getArrayByte(object, offset + i) & 0xffL); - } - } - return value; - } - - public static void putLong(Object object, long offset, long value) { - if (object instanceof byte[]) { - BYTE_ARRAY_LONG.set((byte[]) object, toIntIndex(offset), value); - return; - } - if (BIG_ENDIAN) { - for (int i = Long.BYTES - 1; i >= 0; i--) { - putArrayByte(object, offset + i, (byte) value); - value >>>= Byte.SIZE; - } - } else { - for (int i = 0; i < Long.BYTES; i++) { - putArrayByte(object, offset + i, (byte) value); - value >>>= Byte.SIZE; - } - } - } - - public static float getFloat(Object object, long offset) { - if (object instanceof byte[]) { - return (float) BYTE_ARRAY_FLOAT.get((byte[]) object, toIntIndex(offset)); - } - return Float.intBitsToFloat(getInt(object, offset)); - } - - public static void putFloat(Object object, long offset, float value) { - if (object instanceof byte[]) { - BYTE_ARRAY_FLOAT.set((byte[]) object, toIntIndex(offset), value); - return; - } - putInt(object, offset, Float.floatToRawIntBits(value)); - } - - public static double getDouble(Object object, long offset) { - if (object instanceof byte[]) { - return (double) BYTE_ARRAY_DOUBLE.get((byte[]) object, toIntIndex(offset)); - } - return Double.longBitsToDouble(getLong(object, offset)); - } - - public static void putDouble(Object object, long offset, double value) { - if (object instanceof byte[]) { - BYTE_ARRAY_DOUBLE.set((byte[]) object, toIntIndex(offset), value); - return; - } - putLong(object, offset, Double.doubleToRawLongBits(value)); - } - - public static Object getObject(Object o, long offset) { - throw unsupportedObjectMemory(); - } - - public static void putObject(Object object, long offset, Object value) { - throw unsupportedObjectMemory(); - } - - public static void copyMemory( - Object src, long srcOffset, Object dst, long dstOffset, long length) { - if (src == null || dst == null) { - throw unsupportedNativeMemory(); - } - if (length < 0) { - throw new IllegalArgumentException("length must be non-negative: " + length); - } - int len = toIntLength(length); - if (src instanceof byte[] && dst instanceof byte[]) { - System.arraycopy((byte[]) src, toIntIndex(srcOffset), (byte[]) dst, toIntIndex(dstOffset), len); - return; - } - if (copySamePrimitiveArray(src, srcOffset, dst, dstOffset, len)) { - return; - } - if (!isPrimitiveArray(src) || !isPrimitiveArray(dst)) { - throw unsupportedObjectMemory(); - } - if (src == dst && srcOffset < dstOffset && dstOffset < srcOffset + length) { - for (long i = length - 1; i >= 0; i--) { - putArrayByte(dst, dstOffset + i, getArrayByte(src, srcOffset + i)); - } - } else { - for (long i = 0; i < length; i++) { - putArrayByte(dst, dstOffset + i, getArrayByte(src, srcOffset + i)); - } - } - } - - private static boolean copySamePrimitiveArray( - Object src, long srcOffset, Object dst, long dstOffset, int len) { - if (src.getClass() != dst.getClass()) { - return false; - } - if (src instanceof boolean[]) { - System.arraycopy((boolean[]) src, toIntIndex(srcOffset), (boolean[]) dst, toIntIndex(dstOffset), len); - return true; - } else if (src instanceof char[] && aligned(srcOffset, dstOffset, len, Character.BYTES)) { - System.arraycopy( - (char[]) src, - toIntIndex(srcOffset / Character.BYTES), - (char[]) dst, - toIntIndex(dstOffset / Character.BYTES), - len / Character.BYTES); - return true; - } else if (src instanceof short[] && aligned(srcOffset, dstOffset, len, Short.BYTES)) { - System.arraycopy( - (short[]) src, - toIntIndex(srcOffset / Short.BYTES), - (short[]) dst, - toIntIndex(dstOffset / Short.BYTES), - len / Short.BYTES); - return true; - } else if (src instanceof int[] && aligned(srcOffset, dstOffset, len, Integer.BYTES)) { - System.arraycopy( - (int[]) src, - toIntIndex(srcOffset / Integer.BYTES), - (int[]) dst, - toIntIndex(dstOffset / Integer.BYTES), - len / Integer.BYTES); - return true; - } else if (src instanceof long[] && aligned(srcOffset, dstOffset, len, Long.BYTES)) { - System.arraycopy( - (long[]) src, - toIntIndex(srcOffset / Long.BYTES), - (long[]) dst, - toIntIndex(dstOffset / Long.BYTES), - len / Long.BYTES); - return true; - } else if (src instanceof float[] && aligned(srcOffset, dstOffset, len, Float.BYTES)) { - System.arraycopy( - (float[]) src, - toIntIndex(srcOffset / Float.BYTES), - (float[]) dst, - toIntIndex(dstOffset / Float.BYTES), - len / Float.BYTES); - return true; - } else if (src instanceof double[] && aligned(srcOffset, dstOffset, len, Double.BYTES)) { - System.arraycopy( - (double[]) src, - toIntIndex(srcOffset / Double.BYTES), - (double[]) dst, - toIntIndex(dstOffset / Double.BYTES), - len / Double.BYTES); - return true; - } - return false; - } - - public static Object[] copyObjectArray(Object[] arr) { - Object[] objects = new Object[arr.length]; - System.arraycopy(arr, 0, objects, 0, arr.length); - return objects; - } - - /** Create an instance of type. This method does not call constructor. */ - public static T newInstance(Class type) { - throw new UnsupportedOperationException( - "Constructor-bypassing allocation is unsupported on JDK25 without sun.misc.Unsafe; " - + "use a constructor-based serializer path for " - + type); - } - - private static int getIntFromArray(Object object, long offset) { - return getIntN(object, offset, Integer.BYTES); - } - - private static void putIntToArray(Object object, long offset, int value) { - putIntN(object, offset, value, Integer.BYTES); - } - - private static int getIntN(Object object, long offset, int bytes) { - int value = 0; - if (BIG_ENDIAN) { - for (int i = 0; i < bytes; i++) { - value = (value << Byte.SIZE) | (getArrayByte(object, offset + i) & 0xff); - } - } else { - for (int i = bytes - 1; i >= 0; i--) { - value = (value << Byte.SIZE) | (getArrayByte(object, offset + i) & 0xff); - } - } - return value; - } - - private static void putIntN(Object object, long offset, int value, int bytes) { - if (BIG_ENDIAN) { - for (int i = bytes - 1; i >= 0; i--) { - putArrayByte(object, offset + i, (byte) value); - value >>>= Byte.SIZE; - } - } else { - for (int i = 0; i < bytes; i++) { - putArrayByte(object, offset + i, (byte) value); - value >>>= Byte.SIZE; - } - } - } - - private static byte getArrayByte(Object object, long offset) { - checkOffset(offset); - if (object instanceof byte[]) { - return ((byte[]) object)[toIntIndex(offset)]; - } else if (object instanceof boolean[]) { - return ((boolean[]) object)[toIntIndex(offset)] ? (byte) 1 : (byte) 0; - } else if (object instanceof char[]) { - return getIntByte( - ((char[]) object)[toIntIndex(offset / Character.BYTES)], offset, Character.BYTES); - } else if (object instanceof short[]) { - return getIntByte(((short[]) object)[toIntIndex(offset / Short.BYTES)], offset, Short.BYTES); - } else if (object instanceof int[]) { - return getIntByte( - ((int[]) object)[toIntIndex(offset / Integer.BYTES)], offset, Integer.BYTES); - } else if (object instanceof long[]) { - return getLongByte(((long[]) object)[toIntIndex(offset / Long.BYTES)], offset); - } else if (object instanceof float[]) { - int value = Float.floatToRawIntBits(((float[]) object)[toIntIndex(offset / Float.BYTES)]); - return getIntByte(value, offset, Float.BYTES); - } else if (object instanceof double[]) { - long value = - Double.doubleToRawLongBits(((double[]) object)[toIntIndex(offset / Double.BYTES)]); - return getLongByte(value, offset); - } - throw unsupportedObjectMemory(); - } - - private static void putArrayByte(Object object, long offset, byte value) { - checkOffset(offset); - if (object instanceof byte[]) { - ((byte[]) object)[toIntIndex(offset)] = value; - } else if (object instanceof boolean[]) { - ((boolean[]) object)[toIntIndex(offset)] = value != 0; - } else if (object instanceof char[]) { - char[] array = (char[]) object; - int index = toIntIndex(offset / Character.BYTES); - array[index] = (char) setIntByte(array[index], offset, value, Character.BYTES); - } else if (object instanceof short[]) { - short[] array = (short[]) object; - int index = toIntIndex(offset / Short.BYTES); - array[index] = (short) setIntByte(array[index], offset, value, Short.BYTES); - } else if (object instanceof int[]) { - int[] array = (int[]) object; - int index = toIntIndex(offset / Integer.BYTES); - array[index] = setIntByte(array[index], offset, value, Integer.BYTES); - } else if (object instanceof long[]) { - long[] array = (long[]) object; - int index = toIntIndex(offset / Long.BYTES); - array[index] = setLongByte(array[index], offset, value); - } else if (object instanceof float[]) { - float[] array = (float[]) object; - int index = toIntIndex(offset / Float.BYTES); - int bits = Float.floatToRawIntBits(array[index]); - array[index] = Float.intBitsToFloat(setIntByte(bits, offset, value, Float.BYTES)); - } else if (object instanceof double[]) { - double[] array = (double[]) object; - int index = toIntIndex(offset / Double.BYTES); - long bits = Double.doubleToRawLongBits(array[index]); - array[index] = Double.longBitsToDouble(setLongByte(bits, offset, value)); - } else { - throw unsupportedObjectMemory(); - } - } - - private static byte getIntByte(int value, long offset, int width) { - int shift = byteShift(offset, width); - return (byte) (value >>> shift); - } - - private static byte getLongByte(long value, long offset) { - int shift = byteShift(offset, Long.BYTES); - return (byte) (value >>> shift); - } - - private static int setIntByte(int oldValue, long offset, byte value, int width) { - int shift = byteShift(offset, width); - int mask = 0xff << shift; - return (oldValue & ~mask) | ((value & 0xff) << shift); - } - - private static long setLongByte(long oldValue, long offset, byte value) { - int shift = byteShift(offset, Long.BYTES); - long mask = 0xffL << shift; - return (oldValue & ~mask) | ((long) (value & 0xff) << shift); - } - - private static int byteShift(long offset, int width) { - int byteIndex = (int) Math.floorMod(offset, width); - return (BIG_ENDIAN ? width - 1 - byteIndex : byteIndex) * Byte.SIZE; - } - - private static boolean isPrimitiveArray(Object object) { - Class cls = object.getClass(); - return cls.isArray() && cls.getComponentType().isPrimitive(); - } - - private static boolean aligned(long srcOffset, long dstOffset, int len, int width) { - return srcOffset % width == 0 && dstOffset % width == 0 && len % width == 0; - } - - 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 toIntLength(long length) { - if (length > Integer.MAX_VALUE) { - throw new IndexOutOfBoundsException("length out of int range: " + length); - } - return (int) length; - } - - private static void checkOffset(long offset) { - if (offset < 0) { - throw new IndexOutOfBoundsException("offset must be non-negative: " + offset); - } - } - - private static UnsupportedOperationException unsupportedObjectMemory() { - return new UnsupportedOperationException( - "Object field and reference-offset memory access is unsupported on JDK25 without " - + "sun.misc.Unsafe"); - } - - private static UnsupportedOperationException unsupportedNativeMemory() { - return new UnsupportedOperationException( - "Raw native-address memory access is unsupported on JDK25 without sun.misc.Unsafe"); - } -} 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 index 0c057eb336..56caf8e197 100644 --- 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 @@ -78,6 +78,10 @@ public class _JDKAccess { 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; 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 2995057ccd..56c9c45adc 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 @@ -35,6 +35,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 +213,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,\ 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 2a2e5f1e19..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.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/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 f395e60467..4a30f92a55 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 @@ -31,6 +31,7 @@ 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; @@ -114,6 +115,22 @@ public void testFromDirectByteBufferRejectsHeapBuffer() { () -> 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 = @@ -218,8 +235,8 @@ public static void main(String[] args) { byte[] bytes = new byte[4]; source.copyToByteArray(0, bytes, 0, 4); check(bytes, new byte[] {1, 2, 3, 4}); - assertThrows( - UnsupportedOperationException.class, () -> target.copyFromUnsafe(0, new byte[4], 0, 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) { @@ -281,6 +298,54 @@ 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 (ByteBuffer.class + .getModule() + .isOpen("java.nio", DirectByteBufferNoNioOpenProbe.class.getModule())) { + 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); + } + } + } + @Test public void testBufferUnsafeWrite() { { 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 667de876ae..990f09a730 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,14 +54,8 @@ 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()) + new ProcessBuilder(TestUtils.javaCommand(AndroidDynamicFeatureProbe.class)) .redirectErrorStream(true) .start(); String output = readFully(process.getInputStream()); 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 4d639a4236..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,13 +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( @@ -106,50 +103,12 @@ 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 diff --git a/java/fory-format/pom.xml b/java/fory-format/pom.xml index 15deca64b3..1e9c2adaeb 100644 --- a/java/fory-format/pom.xml +++ b/java/fory-format/pom.xml @@ -116,7 +116,7 @@ maven-surefire-plugin - --add-opens=java.base/java.nio=ALL-UNNAMED + ${argLine} --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED 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 093c61f71f..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; @@ -199,31 +203,61 @@ public byte[] toByteArray() { public short[] toShortArray() { short[] values = new short[numElements]; - buffer.copyToShortArray(elementOffset, values, 0, 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.copyToIntArray(elementOffset, values, 0, 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.copyToLongArray(elementOffset, values, 0, 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.copyToFloatArray(elementOffset, values, 0, 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.copyToDoubleArray(elementOffset, values, 0, 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/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/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-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/pom.xml b/java/pom.xml index 7884abb1bc..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 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); + } } } } From f1f7b2e0da703551774db3a926280d441351094b Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Mon, 25 May 2026 00:21:59 +0800 Subject: [PATCH 32/34] fix(java): restore android and graal ci --- .../serializer/ObjectStreamSerializer.java | 3 + .../fory/serializer/PlatformStringUtils.java | 61 +++++++++++++++++++ .../apache/fory/serializer/Serializers.java | 4 +- .../fory-core/native-image.properties | 1 + .../apache/fory/memory/MemoryBufferTest.java | 19 +++++- .../serializer/AndroidDynamicFeatureTest.java | 7 ++- .../serializer/AndroidJvmRoundTripTest.java | 3 + .../fory/serializer/ObjectSerializerTest.java | 7 ++- .../AndroidCollectionFeatureTest.java | 4 +- 9 files changed, 97 insertions(+), 12 deletions(-) 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 e03dd8cdf2..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 @@ -245,6 +245,9 @@ private static Serializer fallbackSerializer(TypeResolver typeResolver, Class /** 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); 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 index d9770e1892..5b6151b4b4 100644 --- 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 @@ -21,6 +21,7 @@ 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; @@ -104,19 +105,75 @@ private static String newBytesStringSlow(byte coder, byte[] data) { } 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), @@ -126,6 +183,10 @@ static void copyCharsToBytes( } 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/Serializers.java b/java/fory-core/src/main/java/org/apache/fory/serializer/Serializers.java index d8647f19d8..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 @@ -499,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 { @@ -532,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; } 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 56c9c45adc..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,\ 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 4a30f92a55..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 @@ -307,9 +307,7 @@ public static void main(String[] args) { throw new AssertionError("Unexpected java.nio open: " + inputArg); } } - if (ByteBuffer.class - .getModule() - .isOpen("java.nio", DirectByteBufferNoNioOpenProbe.class.getModule())) { + if (isNioOpenToProbe()) { throw new AssertionError("java.base/java.nio must not be open to this test probe"); } } @@ -344,6 +342,21 @@ private static void checkEqual(long actual, long 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 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 990f09a730..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 @@ -54,10 +54,11 @@ public class AndroidDynamicFeatureTest { @Test public void testAndroidDynamicFeaturePaths() throws Exception { - Process process = + ProcessBuilder processBuilder = new ProcessBuilder(TestUtils.javaCommand(AndroidDynamicFeatureProbe.class)) - .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/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/ObjectSerializerTest.java b/java/fory-core/src/test/java/org/apache/fory/serializer/ObjectSerializerTest.java index 178a1cac9e..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 @@ -774,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/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); From 0ff50fcad05d40ecda3db6922dbf1f9b10ed582d Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Mon, 25 May 2026 00:50:35 +0800 Subject: [PATCH 33/34] fix(java): open arrow memory on classpath --- ci/run_ci.sh | 2 +- .../java/org/apache/fory/format/vectorized/ArrowUtils.java | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/ci/run_ci.sh b/ci/run_ci.sh index 76f9aecbd3..34434a13d8 100755 --- a/ci/run_ci.sh +++ b/ci/run_ci.sh @@ -204,7 +204,7 @@ jdk17_plus_tests() { else java_major=$(echo "$java_version" | cut -d. -f1) fi - JDK_JAVA_OPTIONS="--add-opens=java.base/java.nio=org.apache.arrow.memory.core" + 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 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 226117b7df..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 @@ -150,6 +150,11 @@ private static boolean isUnsafeMemoryAccessFailure(Throwable throwable) { } } } + if ("java.lang.reflect.InaccessibleObjectException".equals(cause.getClass().getName()) + && cause.getMessage() != null + && cause.getMessage().contains("java.nio")) { + return true; + } cause = cause.getCause(); } return false; From 15177584772ff7e1484616458f42c412d8f4c984 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Mon, 25 May 2026 01:15:40 +0800 Subject: [PATCH 34/34] fix(java): restore graalvm array offset recompute --- .../org/apache/fory/memory/LittleEndian.java | 12 ++++- .../org/apache/fory/memory/MemoryBuffer.java | 49 +++++++++++++------ .../fory/serializer/PlatformStringUtils.java | 18 +++++-- 3 files changed, 57 insertions(+), 22 deletions(-) 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 1b58ed70d6..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 @@ -25,8 +25,16 @@ public class LittleEndian { private static final Unsafe UNSAFE = AndroidSupport.IS_ANDROID ? null : _JDKAccess.UNSAFE; - private static final int BYTE_ARRAY_OFFSET = - AndroidSupport.IS_ANDROID ? 0 : UNSAFE.arrayBaseOffset(byte[].class); + 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) { 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 6f5f8b5a8d..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 @@ -69,22 +69,39 @@ public final class MemoryBuffer { 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 = - AndroidSupport.IS_ANDROID ? 0 : UNSAFE.arrayBaseOffset(boolean[].class); - private static final int BYTE_ARRAY_OFFSET = - AndroidSupport.IS_ANDROID ? 0 : UNSAFE.arrayBaseOffset(byte[].class); - private static final int CHAR_ARRAY_OFFSET = - AndroidSupport.IS_ANDROID ? 0 : UNSAFE.arrayBaseOffset(char[].class); - private static final int SHORT_ARRAY_OFFSET = - AndroidSupport.IS_ANDROID ? 0 : UNSAFE.arrayBaseOffset(short[].class); - private static final int INT_ARRAY_OFFSET = - AndroidSupport.IS_ANDROID ? 0 : UNSAFE.arrayBaseOffset(int[].class); - private static final int LONG_ARRAY_OFFSET = - AndroidSupport.IS_ANDROID ? 0 : UNSAFE.arrayBaseOffset(long[].class); - private static final int FLOAT_ARRAY_OFFSET = - AndroidSupport.IS_ANDROID ? 0 : UNSAFE.arrayBaseOffset(float[].class); - private static final int DOUBLE_ARRAY_OFFSET = - AndroidSupport.IS_ANDROID ? 0 : UNSAFE.arrayBaseOffset(double[].class); + 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; 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 index 5b6151b4b4..e2335af192 100644 --- 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 @@ -30,10 +30,20 @@ /** 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 = - AndroidSupport.IS_ANDROID ? 0 : UNSAFE.arrayBaseOffset(byte[].class); - private static final int CHAR_ARRAY_OFFSET = - AndroidSupport.IS_ANDROID ? 0 : UNSAFE.arrayBaseOffset(char[].class); + 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