diff --git a/docs/reference-manual/native-image/JFR.md b/docs/reference-manual/native-image/JFR.md index 2127209f6e52..63e80a49252d 100644 --- a/docs/reference-manual/native-image/JFR.md +++ b/docs/reference-manual/native-image/JFR.md @@ -15,8 +15,6 @@ GraalVM Native Image supports building a native executable with JFR events, and JFR support is disabled by default and must be explicitly enabled at build time. -> Note: JFR event recording is not yet available with Native Image on Windows. - To build a native executable with JFR, use the `--enable-monitoring=jfr` option: ```shell native-image --enable-monitoring=jfr JavaApplication diff --git a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/PosixRawFileOperationSupport.java b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/PosixRawFileOperationSupport.java index 06fa1699e23e..cdb4f3b5aaab 100644 --- a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/PosixRawFileOperationSupport.java +++ b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/PosixRawFileOperationSupport.java @@ -30,6 +30,7 @@ import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; +import org.graalvm.nativeimage.impl.InternalPlatform.WINDOWS_BASE; import org.graalvm.nativeimage.c.type.CCharPointer; import org.graalvm.nativeimage.c.type.CTypeConversion; import org.graalvm.word.Pointer; @@ -209,7 +210,7 @@ private static int parseMode(FileAccessMode mode) { class PosixRawFileOperationFeature implements InternalFeature { @Override public boolean isInConfiguration(IsInConfigurationAccess access) { - return ImageLayerBuildingSupport.firstImageBuild(); + return ImageLayerBuildingSupport.firstImageBuild() && !Platform.includedIn(WINDOWS_BASE.class); } @Override diff --git a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java index 996d43b60577..80119999b85d 100644 --- a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java +++ b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java @@ -33,6 +33,7 @@ import com.oracle.svm.core.posix.headers.Errno; import com.oracle.svm.core.posix.headers.Fcntl; import com.oracle.svm.core.posix.headers.Unistd; +import org.graalvm.nativeimage.impl.InternalPlatform; import org.graalvm.word.Pointer; import org.graalvm.word.impl.Word; import org.graalvm.nativeimage.c.type.CCharPointer; @@ -602,7 +603,7 @@ public static void resetEmergencyChunkPathCallCount(PosixJfrEmergencyDumpSupport class PosixJfrEmergencyDumpFeature implements InternalFeature { @Override public boolean isInConfiguration(IsInConfigurationAccess access) { - return VMInspectionOptions.hasJfrSupport(); + return VMInspectionOptions.hasJfrSupport() && !Platform.includedIn(InternalPlatform.WINDOWS_BASE.class); } @Override diff --git a/substratevm/src/com.oracle.svm.core.windows/src/com/oracle/svm/core/windows/WindowsRawFileOperationSupport.java b/substratevm/src/com.oracle.svm.core.windows/src/com/oracle/svm/core/windows/WindowsRawFileOperationSupport.java new file mode 100644 index 000000000000..1ccd26f1b802 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core.windows/src/com/oracle/svm/core/windows/WindowsRawFileOperationSupport.java @@ -0,0 +1,300 @@ +/* + * Copyright (c) 2026, 2026, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2026, 2026, IBM Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.core.windows; + +import static com.oracle.svm.shared.Uninterruptible.CALLED_FROM_UNINTERRUPTIBLE_CODE; + +import java.io.File; +import java.nio.ByteOrder; + +import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; +import org.graalvm.nativeimage.c.type.CCharPointer; +import org.graalvm.nativeimage.impl.InternalPlatform.WINDOWS_BASE; +import org.graalvm.word.Pointer; +import org.graalvm.word.UnsignedWord; +import org.graalvm.word.impl.Word; + +import com.oracle.svm.shared.feature.AutomaticallyRegisteredFeature; +import com.oracle.svm.core.feature.InternalFeature; +import com.oracle.svm.core.graal.stackvalue.UnsafeStackValue; +import com.oracle.svm.core.imagelayer.ImageLayerBuildingSupport; +import com.oracle.svm.core.jdk.SystemPropertiesSupport; +import com.oracle.svm.core.memory.UntrackedNullableNativeMemory; +import com.oracle.svm.core.os.AbstractRawFileOperationSupport; +import com.oracle.svm.core.os.AbstractRawFileOperationSupport.RawFileOperationSupportHolder; +import com.oracle.svm.core.windows.headers.FileAPI; +import com.oracle.svm.core.windows.headers.FileAPI.LARGE_INTEGER; +import com.oracle.svm.core.windows.headers.StringAPISet; +import com.oracle.svm.core.windows.headers.WinBase; +import com.oracle.svm.core.windows.headers.WinBase.HANDLE; +import com.oracle.svm.core.windows.headers.WindowsLibC.WCharPointer; +import com.oracle.svm.shared.singletons.traits.BuiltinTraits.BuildtimeAccessOnly; +import com.oracle.svm.shared.singletons.traits.BuiltinTraits.SingleLayer; +import com.oracle.svm.shared.singletons.traits.SingletonTraits; +import com.oracle.svm.shared.Uninterruptible; +import com.oracle.svm.shared.util.VMError; + +public class WindowsRawFileOperationSupport extends AbstractRawFileOperationSupport { + + @Platforms(Platform.HOSTED_ONLY.class) + public WindowsRawFileOperationSupport(boolean useNativeByteOrder) { + super(useNativeByteOrder); + } + + @Override + public CCharPointer allocateCPath(String path) { + byte[] data = path.getBytes(); + CCharPointer filename = UntrackedNullableNativeMemory.malloc(Word.unsigned(data.length + 1)); + if (filename.isNull()) { + return Word.nullPointer(); + } + for (int i = 0; i < data.length; i++) { + filename.write(i, data[i]); + } + filename.write(data.length, (byte) 0); + return filename; + } + + @Override + public RawFileDescriptor create(File file, FileCreationMode creationMode, FileAccessMode accessMode) { + try (WindowsUtils.WCharPointerHolder wide = WindowsUtils.toWideCString(file.getPath())) { + return createWide(wide.get(), creationMode, accessMode); + } + } + + @Override + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + public RawFileDescriptor create(CCharPointer cPath, FileCreationMode creationMode, FileAccessMode accessMode) { + WCharPointer widePath = convertUtf8ToWide(cPath); + RawFileDescriptor result = createWide(widePath, creationMode, accessMode); + UntrackedNullableNativeMemory.free(widePath); + return result; + } + + @Override + public String getTempDirectory() { + return SystemPropertiesSupport.singleton().getInitialProperty("java.io.tmpdir"); + } + + @Override + public RawFileDescriptor open(File file, FileAccessMode mode) { + try (WindowsUtils.WCharPointerHolder wide = WindowsUtils.toWideCString(file.getPath())) { + return openWide(wide.get(), mode); + } + } + + @Override + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + public RawFileDescriptor open(CCharPointer cPath, FileAccessMode accessMode) { + WCharPointer widePath = convertUtf8ToWide(cPath); + RawFileDescriptor result = openWide(widePath, accessMode); + UntrackedNullableNativeMemory.free(widePath); + return result; + } + + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + private static RawFileDescriptor openWide(WCharPointer widePath, FileAccessMode accessMode) { + int access = parseAccess(accessMode); + HANDLE h = FileAPI.CreateFileW( + widePath, + access, + FileAPI.FILE_SHARE_READ() | FileAPI.FILE_SHARE_WRITE(), + Word.nullPointer(), FileAPI.OPEN_EXISTING(), + FileAPI.FILE_ATTRIBUTE_NORMAL(), + Word.nullPointer()); + return fromHandle(h); + } + + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + private static WCharPointer convertUtf8ToWide(CCharPointer pathUtf8) { + int nchars = StringAPISet.MultiByteToWideChar(StringAPISet.CP_UTF8(), 0, pathUtf8, -1, Word.nullPointer(), 0); + if (nchars <= 0) { + return Word.nullPointer(); + } + + UnsignedWord bytes = Word.unsigned(nchars).multiply(Word.unsigned(Character.BYTES)); + WCharPointer wide = UntrackedNullableNativeMemory.malloc(bytes); + if (wide.isNull()) { + return Word.nullPointer(); + } + + if (StringAPISet.MultiByteToWideChar(StringAPISet.CP_UTF8(), 0, pathUtf8, -1, wide, nchars) == 0) { + UntrackedNullableNativeMemory.free(wide); + return Word.nullPointer(); + } + return wide; + } + + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + private static RawFileDescriptor createWide(WCharPointer widePath, FileCreationMode creationMode, FileAccessMode accessMode) { + int disposition = creationMode == FileCreationMode.CREATE ? FileAPI.CREATE_NEW() : FileAPI.CREATE_ALWAYS(); + int access = parseAccess(accessMode); + HANDLE h = FileAPI.CreateFileW( + widePath, + access, + FileAPI.FILE_SHARE_READ() | FileAPI.FILE_SHARE_WRITE(), + Word.nullPointer(), disposition, FileAPI.FILE_ATTRIBUTE_NORMAL(), + Word.nullPointer()); + return fromHandle(h); + } + + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + private static int parseAccess(FileAccessMode mode) { + switch (mode) { + case READ: + return FileAPI.GENERIC_READ(); + case READ_WRITE: + return FileAPI.GENERIC_READ() | FileAPI.GENERIC_WRITE(); + case WRITE: + return FileAPI.GENERIC_WRITE(); + default: + throw VMError.shouldNotReachHereUnexpectedInput(mode); // ExcludeFromJacocoGeneratedReport + } + } + + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + private static RawFileDescriptor fromHandle(HANDLE h) { + if (h.isNull() || h.equal(WinBase.INVALID_HANDLE_VALUE())) { + return Word.nullPointer(); + } + return (RawFileDescriptor) Word.pointer(h.rawValue()); + } + + @SuppressWarnings("unchecked") + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + private static HANDLE asHandle(RawFileDescriptor fd) { + return (HANDLE) fd; + } + + @Override + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + public boolean isValid(RawFileDescriptor fd) { + if (fd.rawValue() == 0) { + return false; + } + HANDLE h = asHandle(fd); + return !h.isNull() && !h.equal(WinBase.INVALID_HANDLE_VALUE()); + } + + @Override + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + public boolean close(RawFileDescriptor fd) { + if (!isValid(fd)) { + return false; + } + HANDLE h = asHandle(fd); + return WinBase.CloseHandle(h) != 0; + } + + @Override + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + public long size(RawFileDescriptor fd) { + if (!isValid(fd)) { + return -1; + } + HANDLE h = asHandle(fd); + // We support 64 bit, so we only really care about QuadPart + LARGE_INTEGER size = UnsafeStackValue.get(LARGE_INTEGER.class); + size.setQuadPart(0); + if (FileAPI.NoTransition.GetFileSizeEx(h, size) == 0) { + return -1; + } + return size.getQuadPart(); + } + + @Override + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + public long position(RawFileDescriptor fd) { + if (!isValid(fd)) { + return -1; + } + HANDLE h = asHandle(fd); + LARGE_INTEGER newPos = UnsafeStackValue.get(LARGE_INTEGER.class); + newPos.setQuadPart(0); + if (FileAPI.NoTransition.SetFilePointerEx(h, 0, newPos, FileAPI.FILE_CURRENT()) == 0) { + return -1; + } + return newPos.getQuadPart(); + } + + @Override + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + public boolean seek(RawFileDescriptor fd, long position) { + if (!isValid(fd)) { + return false; + } + HANDLE h = asHandle(fd); + LARGE_INTEGER newPos = UnsafeStackValue.get(LARGE_INTEGER.class); + newPos.setQuadPart(0); + if (FileAPI.NoTransition.SetFilePointerEx(h, position, newPos, FileAPI.FILE_BEGIN()) == 0) { + return false; + } + return newPos.getQuadPart() == position; + } + + @Override + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + public boolean write(RawFileDescriptor fd, Pointer data, UnsignedWord size) { + if (!isValid(fd)) { + return false; + } + HANDLE h = asHandle(fd); + return WindowsUtils.writeUninterruptibly(h, (CCharPointer) data, size); + } + + @Override + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + public long read(RawFileDescriptor fd, Pointer buffer, UnsignedWord bufferSize) { + if (!isValid(fd)) { + return -1; + } + HANDLE h = asHandle(fd); + return WindowsUtils.readUninterruptibly(h, (CCharPointer) buffer, bufferSize); + } +} + +@SingletonTraits(access = BuildtimeAccessOnly.class, layeredCallbacks = SingleLayer.class) +@AutomaticallyRegisteredFeature +class WindowsRawFileOperationFeature implements InternalFeature { + @Override + public boolean isInConfiguration(IsInConfigurationAccess access) { + return ImageLayerBuildingSupport.firstImageBuild() && Platform.includedIn(WINDOWS_BASE.class); + } + + @Override + public void afterRegistration(AfterRegistrationAccess access) { + ByteOrder nativeByteOrder = ByteOrder.nativeOrder(); + assert nativeByteOrder == ByteOrder.LITTLE_ENDIAN || nativeByteOrder == ByteOrder.BIG_ENDIAN; + + WindowsRawFileOperationSupport littleEndian = new WindowsRawFileOperationSupport(ByteOrder.LITTLE_ENDIAN == nativeByteOrder); + WindowsRawFileOperationSupport bigEndian = new WindowsRawFileOperationSupport(ByteOrder.BIG_ENDIAN == nativeByteOrder); + WindowsRawFileOperationSupport nativeOrder = nativeByteOrder == ByteOrder.LITTLE_ENDIAN ? littleEndian : bigEndian; + + ImageSingletons.add(RawFileOperationSupportHolder.class, new RawFileOperationSupportHolder(littleEndian, bigEndian, nativeOrder)); + } +} diff --git a/substratevm/src/com.oracle.svm.core.windows/src/com/oracle/svm/core/windows/WindowsUtils.java b/substratevm/src/com.oracle.svm.core.windows/src/com/oracle/svm/core/windows/WindowsUtils.java index 672b82717288..7fff50cfd1c8 100644 --- a/substratevm/src/com.oracle.svm.core.windows/src/com/oracle/svm/core/windows/WindowsUtils.java +++ b/substratevm/src/com.oracle.svm.core.windows/src/com/oracle/svm/core/windows/WindowsUtils.java @@ -148,6 +148,33 @@ public static boolean writeUninterruptibly(HANDLE handle, CCharPointer bytes, Un return true; } + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + public static long readUninterruptibly(HANDLE handle, CCharPointer buffer, UnsignedWord length) { + if (handle == INVALID_HANDLE_VALUE()) { + return -1; + } + + CCharPointer pos = buffer; + UnsignedWord bytesRemaining = length; + long totalRead = 0; + while (bytesRemaining.notEqual(0)) { + CIntPointer bytesRead = UnsafeStackValue.get(CIntPointer.class); + if (FileAPI.NoTransition.ReadFile(handle, pos, bytesRemaining, bytesRead, Word.nullPointer()) == 0) { + return -1; + } + + int readCount = bytesRead.read(); + if (readCount == 0) { + break; + } + + totalRead += readCount; + pos = pos.addressOf(readCount); + bytesRemaining = bytesRemaining.subtract(readCount); + } + return totalRead; + } + static boolean flush(HANDLE handle) { if (handle == INVALID_HANDLE_VALUE()) { return false; diff --git a/substratevm/src/com.oracle.svm.core.windows/src/com/oracle/svm/core/windows/headers/FileAPI.java b/substratevm/src/com.oracle.svm.core.windows/src/com/oracle/svm/core/windows/headers/FileAPI.java index cfefb1a74f9f..82759bddcf32 100644 --- a/substratevm/src/com.oracle.svm.core.windows/src/com/oracle/svm/core/windows/headers/FileAPI.java +++ b/substratevm/src/com.oracle.svm.core.windows/src/com/oracle/svm/core/windows/headers/FileAPI.java @@ -29,6 +29,8 @@ import org.graalvm.nativeimage.c.CContext; import org.graalvm.nativeimage.c.constant.CConstant; import org.graalvm.nativeimage.c.function.CFunction; +import org.graalvm.nativeimage.c.struct.CField; +import org.graalvm.nativeimage.c.struct.CStruct; import org.graalvm.nativeimage.c.type.CCharPointer; import org.graalvm.nativeimage.c.type.CIntPointer; import org.graalvm.word.PointerBase; @@ -49,6 +51,9 @@ public class FileAPI { @CConstant public static native int GENERIC_READ(); + @CConstant + public static native int GENERIC_WRITE(); + /** Creates or opens a file or I/O device. */ @CFunction(transition = NO_TRANSITION) public static native HANDLE CreateFileW(WCharPointer lpFileName, int dwDesiredAccess, int dwShareMode, @@ -59,6 +64,9 @@ public static native HANDLE CreateFileW(WCharPointer lpFileName, int dwDesiredAc @CConstant public static native int FILE_SHARE_READ(); + @CConstant + public static native int FILE_SHARE_WRITE(); + @CConstant public static native int FILE_SHARE_DELETE(); @@ -66,6 +74,33 @@ public static native HANDLE CreateFileW(WCharPointer lpFileName, int dwDesiredAc @CConstant public static native int OPEN_EXISTING(); + @CConstant + public static native int CREATE_NEW(); + + @CConstant + public static native int CREATE_ALWAYS(); + + @CConstant + public static native int FILE_ATTRIBUTE_NORMAL(); + + @CConstant + public static native int FILE_BEGIN(); + + @CConstant + public static native int FILE_CURRENT(); + + /** + * 64-bit integer needed by various APIs. + */ + @CStruct + public interface LARGE_INTEGER extends PointerBase { + @CField("QuadPart") + long getQuadPart(); + + @CField("QuadPart") + void setQuadPart(long value); + } + @CFunction public static native int WriteFile(HANDLE hFile, CCharPointer lpBuffer, UnsignedWord nNumberOfBytesToWrite, CIntPointer lpNumberOfBytesWritten, PointerBase lpOverlapped); @@ -89,6 +124,15 @@ public static native int WriteFile(HANDLE hFile, CCharPointer lpBuffer, Unsigned public static native int GetTempPathW(int nBufferLength, WCharPointer lpBuffer); public static class NoTransition { + @CFunction(transition = NO_TRANSITION) + public static native int ReadFile(HANDLE hFile, CCharPointer lpBuffer, UnsignedWord nNumberOfBytesToRead, CIntPointer lpNumberOfBytesRead, PointerBase lpOverlapped); + + @CFunction(transition = NO_TRANSITION) + public static native int GetFileSizeEx(HANDLE hFile, LARGE_INTEGER lpFileSize); + + @CFunction(transition = NO_TRANSITION) + public static native int SetFilePointerEx(HANDLE hFile, long liDistanceToMove, LARGE_INTEGER lpNewFilePointer, int dwMoveMethod); + @CFunction(transition = NO_TRANSITION) public static native int WriteFile(HANDLE hFile, CCharPointer lpBuffer, UnsignedWord nNumberOfBytesToWrite, CIntPointer lpNumberOfBytesWritten, PointerBase lpOverlapped); diff --git a/substratevm/src/com.oracle.svm.core.windows/src/com/oracle/svm/core/windows/headers/StringAPISet.java b/substratevm/src/com.oracle.svm.core.windows/src/com/oracle/svm/core/windows/headers/StringAPISet.java index fe42efb6590a..58ff74acf174 100644 --- a/substratevm/src/com.oracle.svm.core.windows/src/com/oracle/svm/core/windows/headers/StringAPISet.java +++ b/substratevm/src/com.oracle.svm.core.windows/src/com/oracle/svm/core/windows/headers/StringAPISet.java @@ -43,6 +43,10 @@ public class StringAPISet { @CConstant public static native int CP_ACP(); + /** UTF-8 code page. */ + @CConstant + public static native int CP_UTF8(); + /** Maps a character string to a UTF-16 (wide character) string. */ @CFunction(transition = CFunction.Transition.NO_TRANSITION) public static native int MultiByteToWideChar(int CodePage, int dwFlags, CCharPointer lpMultiByteStr, int cbMultiByte, WCharPointer lpWideCharStr, int cchWideChar); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/VMInspectionOptions.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/VMInspectionOptions.java index a87114e08bc9..c1c4c9a844de 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/VMInspectionOptions.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/VMInspectionOptions.java @@ -210,7 +210,7 @@ public static boolean dumpImageHeap() { */ @Fold public static boolean hasJfrSupport() { - return hasAllOrKeywordMonitoringSupport(MONITORING_JFR_NAME) && !Platform.includedIn(WINDOWS_BASE.class); + return hasAllOrKeywordMonitoringSupport(MONITORING_JFR_NAME); } @Fold diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrFeature.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrFeature.java index 9fcd461fbb59..ea48023ea1c5 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrFeature.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrFeature.java @@ -28,7 +28,6 @@ import java.util.List; import org.graalvm.nativeimage.ImageSingletons; -import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.hosted.Feature; import org.graalvm.nativeimage.impl.RuntimeClassInitializationSupport; @@ -45,7 +44,6 @@ import com.oracle.svm.core.sampler.SamplerStatistics; import com.oracle.svm.core.thread.ThreadListenerSupport; import com.oracle.svm.core.thread.ThreadListenerSupportFeature; -import com.oracle.svm.core.util.UserError; import com.oracle.svm.shared.Uninterruptible; import com.oracle.svm.shared.singletons.traits.BuiltinTraits.BuildtimeAccessOnly; import com.oracle.svm.shared.singletons.traits.BuiltinTraits.Disallowed; @@ -125,11 +123,6 @@ public boolean isInConfiguration(IsInConfigurationAccess access) { } public static boolean isInConfiguration(boolean allowPrinting) { - boolean systemSupported = osSupported(); - if (HOSTED_ENABLED && !systemSupported) { - throw UserError.abort("FlightRecorder cannot be used to profile the image generator on this platform. " + - "The image generator can only be profiled on platforms where FlightRecoder is also supported at run time."); - } boolean runtimeEnabled = VMInspectionOptions.hasJfrSupport(); if (HOSTED_ENABLED && !runtimeEnabled) { if (allowPrinting) { @@ -142,11 +135,7 @@ public static boolean isInConfiguration(boolean allowPrinting) { // GR-68066 support JFR in layered images return false; } - return runtimeEnabled && systemSupported; - } - - private static boolean osSupported() { - return Platform.includedIn(Platform.LINUX.class) || Platform.includedIn(Platform.DARWIN.class); + return runtimeEnabled; } /** diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/RawFileOperationSupportTest.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/RawFileOperationSupportTest.java new file mode 100644 index 000000000000..c9ad3f98eba4 --- /dev/null +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/RawFileOperationSupportTest.java @@ -0,0 +1,231 @@ +/* + * Copyright (c) 2026, 2026, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2026, 2026, IBM Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.test; + +import static com.oracle.svm.core.os.RawFileOperationSupport.FileAccessMode.READ; +import static com.oracle.svm.core.os.RawFileOperationSupport.FileAccessMode.READ_WRITE; +import static com.oracle.svm.core.os.RawFileOperationSupport.FileCreationMode.CREATE; +import static com.oracle.svm.core.os.RawFileOperationSupport.FileCreationMode.CREATE_OR_REPLACE; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; + +import java.io.File; +import java.nio.file.Files; + +import org.graalvm.nativeimage.StackValue; +import org.graalvm.nativeimage.c.type.CCharPointer; +import org.graalvm.word.Pointer; +import org.graalvm.word.impl.Word; +import org.junit.Before; +import org.junit.Test; + +import com.oracle.svm.core.memory.UntrackedNullableNativeMemory; +import com.oracle.svm.core.os.RawFileOperationSupport; +import com.oracle.svm.core.os.RawFileOperationSupport.RawFileDescriptor; + +public class RawFileOperationSupportTest { + + @Before + public void requireRawFileSupport() { + assumeTrue("RawFileOperationSupport is registered only in native images", RawFileOperationSupport.isPresent()); + } + + @Test + public void testTempDirectoryAccessible() { + String tmp = RawFileOperationSupport.nativeByteOrder().getTempDirectory(); + assertNotNull(tmp); + assertFalse(tmp.isEmpty()); + } + + @Test + public void testCreateWriteReadAndClose() { + byte[] content = "just some text".getBytes(); + assertTrue(content.length <= 64); // StackValue requires a constant + RawFileOperationSupport fileSupport = RawFileOperationSupport.nativeByteOrder(); + + // Create + File file = new File(fileSupport.getTempDirectory(), "testCreateWriteReadAndClose_" + System.nanoTime() + ".txt"); + file.deleteOnExit(); + RawFileDescriptor fd = fileSupport.create(file, CREATE, READ_WRITE); + assertTrue(fileSupport.isValid(fd)); + + try { + // Write + assertTrue(fileSupport.write(fd, content)); + assertEquals(content.length, fileSupport.position(fd)); + assertEquals(content.length, fileSupport.size(fd)); + + // Read + assertTrue(fileSupport.seek(fd, 0)); + Pointer buf = StackValue.get(64); + long n = fileSupport.read(fd, buf, Word.unsigned(content.length)); + assertEquals(content.length, n); + + // Ensure we got what's expected + for (int i = 0; i < content.length; i++) { + assertEquals(content[i], buf.readByte(i)); + } + } finally { + assertTrue(fileSupport.close(fd)); + } + } + + @Test + public void testWriteIntEndianness() throws Exception { + /* Unique bytes so LE vs BE on-disk layouts differ (same int, reversed byte order). */ + int value = 0x01020304; + File file = File.createTempFile("testWriteIntEndianness", ".txt"); + file.deleteOnExit(); + + RawFileOperationSupport fileSupportLE = RawFileOperationSupport.littleEndian(); + // Check we can open a pre-existing file. + RawFileDescriptor fdLE = fileSupportLE.open(file, READ_WRITE); + assertTrue(fileSupportLE.isValid(fdLE)); + try { + assertTrue(fileSupportLE.writeInt(fdLE, value)); + } finally { + assertTrue(fileSupportLE.close(fdLE)); + } + byte[] leBytes = Files.readAllBytes(file.toPath()); + assertEquals(4, leBytes.length); + + RawFileOperationSupport fileSupportBE = RawFileOperationSupport.bigEndian(); + RawFileDescriptor fdBE = fileSupportBE.open(file, READ_WRITE); + assertTrue(fileSupportBE.isValid(fdBE)); + try { + assertTrue(fileSupportBE.seek(fdBE, 0)); + assertTrue(fileSupportBE.writeInt(fdBE, value)); + } finally { + assertTrue(fileSupportBE.close(fdBE)); + } + byte[] beBytes = Files.readAllBytes(file.toPath()); + assertEquals(4, beBytes.length); + + byte[] expectedLe = expectedLittleEndianOnDisk(value); + byte[] expectedBe = expectedBigEndianOnDisk(value); + assertArrayEquals(expectedLe, leBytes); + assertArrayEquals(expectedBe, beBytes); + assertArrayEquals(leBytes, reverse4bytes(beBytes)); + } + + private static byte[] expectedLittleEndianOnDisk(int v) { + return new byte[]{(byte) v, (byte) (v >>> 8), (byte) (v >>> 16), (byte) (v >>> 24)}; + } + + private static byte[] expectedBigEndianOnDisk(int v) { + return new byte[]{(byte) (v >>> 24), (byte) (v >>> 16), (byte) (v >>> 8), (byte) v}; + } + + private static byte[] reverse4bytes(byte[] b) { + assertEquals(4, b.length); + return new byte[]{b[3], b[2], b[1], b[0]}; + } + + @Test + public void testCreateOrReplace() throws Exception { + // Create the actual file to later be replaced. + File file = File.createTempFile("testCreateOrReplace", ".txt"); + file.deleteOnExit(); + + RawFileOperationSupport fileSupport = RawFileOperationSupport.nativeByteOrder(); + RawFileDescriptor fd = fileSupport.create(file, CREATE_OR_REPLACE, READ_WRITE); + assertTrue(fileSupport.isValid(fd)); + try { + readAndWriteOnFile(fd); + } finally { + assertTrue(fileSupport.close(fd)); + } + } + + @Test + public void testOpenMissingFileIsInvalid() { + RawFileOperationSupport fileSupport = RawFileOperationSupport.nativeByteOrder(); + File dir = new File(fileSupport.getTempDirectory()); + File missing = new File(dir, "testOpenMissingFileIsInvalid_-" + System.nanoTime()); + RawFileDescriptor fd = fileSupport.open(missing, READ); + assertFalse(fileSupport.isValid(fd)); + } + + @Test + public void testCreateFailsWhenFileExists() throws Exception { + RawFileOperationSupport fileSupport = RawFileOperationSupport.nativeByteOrder(); + File file = new File(fileSupport.getTempDirectory(), "testCreateFailsWhenFileExists_" + System.nanoTime() + ".txt"); + file.deleteOnExit(); + + RawFileDescriptor first = fileSupport.create(file, CREATE, READ_WRITE); + assertTrue(fileSupport.isValid(first)); + assertTrue(fileSupport.close(first)); + + RawFileDescriptor second = fileSupport.create(file, CREATE, READ_WRITE); + assertFalse(fileSupport.isValid(second)); + } + + @Test + public void testCPathReadWrite() { + RawFileOperationSupport fileSupport = RawFileOperationSupport.nativeByteOrder(); + File file = new File(fileSupport.getTempDirectory(), "testCPathReadWrite_" + System.nanoTime() + ".txt"); + file.deleteOnExit(); + String pathString = file.getPath(); + byte[] pathBytes = pathString.getBytes(); + CCharPointer cpath = fileSupport.allocateCPath(pathString); + assertFalse(cpath.isNull()); + try { + for (int i = 0; i < pathBytes.length; i++) { + assertEquals(pathBytes[i], cpath.read(i)); + } + assertEquals(0, cpath.read(pathBytes.length)); + + // On Windows this tests UTF-8 conversion + RawFileDescriptor fd = fileSupport.create(cpath, CREATE, READ_WRITE); + assertTrue(fileSupport.isValid(fd)); + readAndWriteOnFile(fd); + assertTrue(fileSupport.close(fd)); + } finally { + UntrackedNullableNativeMemory.free(cpath); + } + } + + /** Tests write, seek, position, and read. */ + private static void readAndWriteOnFile(RawFileDescriptor fd) { + RawFileOperationSupport fileSupport = RawFileOperationSupport.nativeByteOrder(); + byte[] payload = {1, 2, 3, 4, 5}; + int payloadLength = 5; + assertTrue(fileSupport.write(fd, payload)); + assertEquals(payloadLength, fileSupport.position(fd)); + assertTrue(fileSupport.seek(fd, 0)); + assertEquals(0, fileSupport.position(fd)); + Pointer buf = StackValue.get(payloadLength); + long n = fileSupport.read(fd, buf, Word.unsigned(payloadLength)); + assertEquals(payloadLength, n); + for (int i = 0; i < payloadLength; i++) { + assertEquals(payload[i], buf.readByte(i)); + } + } +} diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/JfrEmergencyDumpTest.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/JfrEmergencyDumpTest.java new file mode 100644 index 000000000000..fa739bd05f02 --- /dev/null +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/JfrEmergencyDumpTest.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2026, 2026, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2026, 2026, IBM Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.test.jfr; + +import org.graalvm.nativeimage.Platform; +import static org.junit.Assume.assumeTrue; +import org.junit.BeforeClass; + +public abstract class JfrEmergencyDumpTest extends JfrRecordingTest { + @BeforeClass + public static void checkNotWindows() { + assumeTrue("skipping emergency dump tests on Windows", !Platform.includedIn(Platform.WINDOWS.class)); + } +} diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDump.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDump.java index 8eb45b3ebef2..46ecc97bce8d 100644 --- a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDump.java +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDump.java @@ -49,7 +49,7 @@ * emergency dump. This would indicate that the chunk files from the disk repository we merged * correctly along with in-flight data. */ -public class TestEmergencyDump extends JfrRecordingTest { +public class TestEmergencyDump extends JfrEmergencyDumpTest { private static final String STRING_EVENT_NAME = "com.jfr.String"; private static final String OUT_OF_MEMORY_REASON = "Out of Memory"; diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpConstantPool.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpConstantPool.java index 4a5c12a96dfc..b165e67e37c5 100644 --- a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpConstantPool.java +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpConstantPool.java @@ -49,7 +49,7 @@ * Verifies that the previous-epoch type and symbol constant pools required by in-flight class * events are serialized correctly during an emergency dump. */ -public class TestEmergencyDumpConstantPool extends JfrRecordingTest { +public class TestEmergencyDumpConstantPool extends JfrEmergencyDumpTest { private static final String CLASS_EVENT_NAME = "com.jfr.Class"; @Test diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpMetadataOnly.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpMetadataOnly.java index baf402e1a7cd..9a1013e7ab3a 100644 --- a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpMetadataOnly.java +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpMetadataOnly.java @@ -42,7 +42,7 @@ import jdk.jfr.Recording; import jdk.jfr.consumer.RecordedEvent; -public class TestEmergencyDumpMetadataOnly extends JfrRecordingTest { +public class TestEmergencyDumpMetadataOnly extends JfrEmergencyDumpTest { private static final String OUT_OF_MEMORY_REASON = "Out of Memory"; @Test diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpRecoveredOutOfMemory.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpRecoveredOutOfMemory.java index ff4f992d8adf..df375b2abe9e 100644 --- a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpRecoveredOutOfMemory.java +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpRecoveredOutOfMemory.java @@ -50,7 +50,7 @@ import jdk.jfr.Recording; import jdk.jfr.consumer.RecordedEvent; -public class TestEmergencyDumpRecoveredOutOfMemory extends JfrRecordingTest { +public class TestEmergencyDumpRecoveredOutOfMemory extends JfrEmergencyDumpTest { private static final String STRING_EVENT_NAME = "com.jfr.String"; private static final String OUT_OF_MEMORY_REASON = "Out of Memory"; private static final int ITERATIONS = 3; diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpRepositoryFallback.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpRepositoryFallback.java index e32920a8b5f2..05eba2b832e3 100644 --- a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpRepositoryFallback.java +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpRepositoryFallback.java @@ -49,7 +49,7 @@ import jdk.jfr.Recording; import jdk.jfr.consumer.RecordedEvent; -public class TestEmergencyDumpRepositoryFallback extends AbstractJfrTest { +public class TestEmergencyDumpRepositoryFallback extends JfrEmergencyDumpTest { private static final String STRING_EVENT_NAME = "com.jfr.String"; private static final String OUT_OF_MEMORY_REASON = "Out of Memory"; @@ -58,7 +58,7 @@ public void testRepositoryEmergencyChunkIsMergedIntoEmergencyDump() throws Throw if (!HasJfrSupport.get()) { return; } - if (!(JfrEmergencyDumpSupport.singleton() instanceof PosixJfrEmergencyDumpSupport support)) { + if (!JfrEmergencyDumpSupport.isPresent() || !(JfrEmergencyDumpSupport.singleton() instanceof PosixJfrEmergencyDumpSupport support)) { return; } diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpSupportLifecycle.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpSupportLifecycle.java index 3bf04dec87fd..ea95cf973425 100644 --- a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpSupportLifecycle.java +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpSupportLifecycle.java @@ -34,13 +34,13 @@ import com.oracle.svm.core.jfr.JfrEmergencyDumpSupport; import com.oracle.svm.core.posix.jfr.PosixJfrEmergencyDumpSupport; -public class TestEmergencyDumpSupportLifecycle extends AbstractJfrTest { +public class TestEmergencyDumpSupportLifecycle extends JfrEmergencyDumpTest { @Test public void testRepeatedInitializeReusesPathBuffer() { if (!HasJfrSupport.get()) { return; } - if (!(JfrEmergencyDumpSupport.singleton() instanceof PosixJfrEmergencyDumpSupport support)) { + if (!JfrEmergencyDumpSupport.isPresent() || !(JfrEmergencyDumpSupport.singleton() instanceof PosixJfrEmergencyDumpSupport support)) { return; } diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/utils/JfrFileParser.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/utils/JfrFileParser.java index 461ff4020a29..0ee59e0e90a1 100644 --- a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/utils/JfrFileParser.java +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/utils/JfrFileParser.java @@ -112,12 +112,13 @@ public HashMap getSupportedConstantPools() { } public void verify() throws IOException { - RecordingInput input = new RecordingInput(path.toFile()); - FileHeaderInfo header = parseFileHeader(input); - parseMetadata(input, header.metadataPosition); + try (RecordingInput input = new RecordingInput(path.toFile())) { + FileHeaderInfo header = parseFileHeader(input); + parseMetadata(input, header.metadataPosition); - Collection constantPoolOffsets = getConstantPoolOffsets(input, header.checkpointPosition); - verifyConstantPools(input, constantPoolOffsets); + Collection constantPoolOffsets = getConstantPoolOffsets(input, header.checkpointPosition); + verifyConstantPools(input, constantPoolOffsets); + } } private static void parseMetadata(RecordingInput input, long metadataPosition) throws IOException {