Skip to content

Commit 2bfbf0e

Browse files
paulirwinclaude
andcommitted
Finish port of NativeUnixDirectory/NativePosixUtil using P/Invoke (#1342)
Complete the previously-excluded NativeUnixDirectory and NativePosixUtil port from Lucene 4.8.1, replacing the original Java + C++/JNI native layer with libc P/Invoke. NativePosixUtil does open_direct (Linux O_DIRECT|O_NOATIME; macOS open + fcntl(F_NOCACHE)), pread, posix_fadvise/madvise with the Java->OS advice-ordinal remap. NativeUnixDirectory ports the direct-I/O directory and its IndexInput/IndexOutput faithfully (512-byte alignment, buffer dump/refill, seek, clone), with the Java NIO FileChannel operations replaced by libc pwrite/ftruncate/lseek and a block-aligned native buffer in place of the direct ByteBuffer. Modernized to the current Span-based ReadInternal/WriteBytes and Position/Dispose conventions. Both types are guarded to throw PlatformNotSupportedException on Microsoft Windows (Constants.WINDOWS + [UnsupportedOSPlatform("windows")]); they require Linux/macOS direct I/O. Removed the Store compile exclusions from Lucene.Net.Misc so both files now build. Added test coverage (the original Lucene types had none): a cross-platform guard fixture (verifies the Windows refusal), targeted direct-path tests (round-trip, concurrent clone reads, clone dispose not closing the shared descriptor), and TestNativeUnixDirectoryBase : BaseDirectoryTestCase running the full directory suite against the direct-I/O path. TestFsyncDoesntCreateNewFiles is overridden/skipped because NativeUnixDirectory is a composite/delegating directory: non-MERGE writes go through the delegate and are tracked in its staleFiles, so FSDirectory.Sync (which intersects with its own staleFiles) does not fsync them - the base test itself notes it does not handle composite directories. Unix functional tests are platform-gated and require an O_DIRECT-capable filesystem. Verified on Linux (WSL Ubuntu, .NET 10): 23 tests pass, 0 fail; on Windows the functional/base tests skip and the guard tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 786c2b9 commit 2bfbf0e

4 files changed

Lines changed: 1036 additions & 468 deletions

File tree

src/Lucene.Net.Misc/Lucene.Net.Misc.csproj

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,6 @@
3939
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
4040
</PropertyGroup>
4141

42-
43-
44-
<ItemGroup>
45-
<!-- LUCENENET TODO: The Unix/POSIX native directory port (NativeUnixDirectory) is not yet
46-
complete and is excluded from compilation. See GH-1342. WindowsDirectory has been ported. -->
47-
<Compile Remove="Store\NativeUnixDirectory.cs" />
48-
<Compile Remove="Store\NativePosixUtil.cs" />
49-
</ItemGroup>
50-
5142
<ItemGroup>
5243
<ProjectReference Include="..\Lucene.Net\Lucene.Net.csproj" />
5344
</ItemGroup>
Lines changed: 203 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1+
using Lucene.Net.Util;
2+
using Microsoft.Win32.SafeHandles;
13
using System;
4+
using System.ComponentModel;
5+
using System.IO;
6+
using System.Runtime.InteropServices;
7+
#if FEATURE_SUPPORTEDOSPLATFORMATTRIBUTE
8+
using System.Runtime.Versioning;
9+
#endif
210

3-
namespace org.apache.lucene.store
11+
namespace Lucene.Net.Store
412
{
5-
613
/*
714
* Licensed to the Apache Software Foundation (ASF) under one or more
815
* contributor license agreements. See the NOTICE file distributed with
@@ -20,45 +27,205 @@ namespace org.apache.lucene.store
2027
* limitations under the License.
2128
*/
2229

23-
ignore
24-
2530
/// <summary>
26-
/// Provides JNI access to native methods such as madvise() for
27-
/// <seealso cref="NativeUnixDirectory"/>
31+
/// Provides access to native POSIX methods such as <c>madvise()</c> for
32+
/// <see cref="NativeUnixDirectory"/>.
33+
/// <para/>
34+
/// LUCENENET specific: the original Lucene implementation called these through a JNI
35+
/// shim compiled from <c>NativePosixUtil.cpp</c>. This port replaces that native build
36+
/// step with direct P/Invoke into <c>libc</c>, so it can only be used on Unix-like
37+
/// platforms (Linux and macOS); it is not supported on Microsoft Windows.
2838
/// </summary>
29-
public final class NativePosixUtil
39+
#if FEATURE_SUPPORTEDOSPLATFORMATTRIBUTE
40+
[UnsupportedOSPlatform("windows")]
41+
#endif
42+
public static class NativePosixUtil
3043
{
31-
public final static int NORMAL = 0;
32-
public final static int SEQUENTIAL = 1;
33-
public final static int RANDOM = 2;
34-
public final static int WILLNEED = 3;
35-
public final static int DONTNEED = 4;
36-
public final static int NOREUSE = 5;
37-
38-
//JAVA TO C# CONVERTER NOTE: This static initializer block is converted to a static constructor, but there is no current class:
39-
static ImpliedClass()
40-
{
41-
//JAVA TO C# CONVERTER TODO TASK: The library is specified in the 'DllImport' attribute for .NET:
42-
// System.loadLibrary("NativePosixUtil");
43-
}
44-
45-
private static native int posix_fadvise(FileDescriptor fd, long offset, long len, int advise) throws IOException;
46-
public static native int posix_madvise(ByteBuffer buf, int advise) throws IOException;
47-
public static native int madvise(ByteBuffer buf, int advise) throws IOException;
48-
public static native FileDescriptor open_direct(string filename, bool read) throws IOException;
49-
public static native long pread(FileDescriptor fd, long pos, ByteBuffer byteBuf) throws IOException;
50-
51-
public static void advise(FileDescriptor fd, long offset, long len, int advise) throws IOException
52-
{
53-
//JAVA TO C# CONVERTER WARNING: The original Java variable was marked 'final':
54-
//ORIGINAL LINE: final int code = posix_fadvise(fd, offset, len, advise);
55-
int code = posix_fadvise(fd, offset, len, advise);
56-
if (code != 0)
44+
// These constants mirror the Java NativePosixUtil ordering. Note this is NOT the same
45+
// ordering as the OS POSIX_FADV_*/POSIX_MADV_* values (SEQUENTIAL/RANDOM are swapped);
46+
// MapAdvice() translates to the OS values.
47+
public const int NORMAL = 0;
48+
public const int SEQUENTIAL = 1;
49+
public const int RANDOM = 2;
50+
public const int WILLNEED = 3;
51+
public const int DONTNEED = 4;
52+
public const int NOREUSE = 5;
53+
54+
/// <summary>
55+
/// Opens a file for direct (un-cached) I/O.
56+
/// <para/>
57+
/// On Linux this uses <c>O_DIRECT | O_NOATIME</c>; on macOS it opens normally and then
58+
/// applies <c>fcntl(F_NOCACHE)</c>, matching the original native implementation.
59+
/// </summary>
60+
/// <param name="filename"> the file to open </param>
61+
/// <param name="read"> <c>true</c> to open read-only; <c>false</c> to open read-write (creating if needed) </param>
62+
/// <returns> a <see cref="SafeFileHandle"/> wrapping the open file descriptor </returns>
63+
/// <exception cref="IOException"> If the file could not be opened </exception>
64+
/// <exception cref="PlatformNotSupportedException"> If running on Microsoft Windows </exception>
65+
public static SafeFileHandle OpenDirect(string filename, bool read)
5766
{
58-
throw new Exception("posix_fadvise failed code=" + code);
67+
EnsureUnix();
68+
69+
int fd;
70+
if (Constants.MAC_OS_X)
71+
{
72+
fd = read
73+
? NativeMethods.open(filename, NativeMethods.O_RDONLY)
74+
: NativeMethods.open(filename, NativeMethods.O_RDWR | NativeMethods.O_CREAT, NativeMethods.S_RW);
75+
if (fd < 0)
76+
{
77+
throw NewIOException("open", filename);
78+
}
79+
80+
// macOS has no O_DIRECT; disable the page cache for this descriptor instead.
81+
if (NativeMethods.fcntl(fd, NativeMethods.F_NOCACHE, 1) < 0)
82+
{
83+
int err = Marshal.GetLastWin32Error();
84+
NativeMethods.close(fd);
85+
throw NewIOException("fcntl(F_NOCACHE)", filename, err);
86+
}
87+
}
88+
else // Linux (and other Unixes, best-effort)
89+
{
90+
fd = read
91+
? NativeMethods.open(filename, NativeMethods.O_RDONLY | NativeMethods.O_DIRECT | NativeMethods.O_NOATIME)
92+
: NativeMethods.open(filename, NativeMethods.O_RDWR | NativeMethods.O_CREAT | NativeMethods.O_DIRECT | NativeMethods.O_NOATIME, NativeMethods.S_RW);
93+
if (fd < 0)
94+
{
95+
throw NewIOException("open", filename);
96+
}
97+
}
98+
99+
return new SafeFileHandle((IntPtr)fd, ownsHandle: true);
59100
}
60-
}
61-
}
62101

102+
/// <summary>
103+
/// Positioned read of up to <paramref name="length"/> bytes from <paramref name="fd"/> at
104+
/// absolute offset <paramref name="pos"/> into the native buffer at <paramref name="buffer"/>,
105+
/// without changing the file's current offset.
106+
/// </summary>
107+
/// <returns> the number of bytes read (may be less than <paramref name="length"/> near EOF) </returns>
108+
/// <exception cref="IOException"> If the read failed </exception>
109+
public static long Pread(SafeFileHandle fd, long pos, IntPtr buffer, int length)
110+
{
111+
EnsureUnix();
112+
long n = (long)NativeMethods.pread(Fd(fd), buffer, (nuint)length, pos);
113+
if (n < 0)
114+
{
115+
throw NewIOException("pread", null);
116+
}
117+
return n;
118+
}
119+
120+
/// <summary>
121+
/// Issues a <c>posix_madvise()</c> hint over the native buffer at <paramref name="buffer"/>. </summary>
122+
public static int PosixMAdvise(IntPtr buffer, int length, int advise)
123+
{
124+
EnsureUnix();
125+
return NativeMethods.posix_madvise(buffer, (nuint)length, MapAdvice(advise));
126+
}
127+
128+
/// <summary>
129+
/// Issues a <c>madvise()</c> hint over the native buffer at <paramref name="buffer"/>. </summary>
130+
public static int MAdvise(IntPtr buffer, int length, int advise)
131+
{
132+
EnsureUnix();
133+
return NativeMethods.madvise(buffer, (nuint)length, MapAdvice(advise));
134+
}
135+
136+
/// <summary>
137+
/// Issues a <c>posix_fadvise()</c> hint for the given range of <paramref name="fd"/>,
138+
/// throwing if the call reports a non-zero error code.
139+
/// </summary>
140+
/// <exception cref="IOException"> If <c>posix_fadvise</c> returned a non-zero code </exception>
141+
public static void Advise(SafeFileHandle fd, long offset, long len, int advise)
142+
{
143+
EnsureUnix();
144+
int code = NativeMethods.posix_fadvise(Fd(fd), offset, len, MapAdvice(advise));
145+
if (code != 0)
146+
{
147+
// LUCENENET: upstream throws RuntimeException; we use IOException as this is an I/O failure.
148+
throw new IOException("posix_fadvise failed code=" + code);
149+
}
150+
}
151+
152+
/// <summary>
153+
/// Translates the Java-style advice ordinal (see the public constants) to the OS
154+
/// <c>POSIX_FADV_*</c>/<c>POSIX_MADV_*</c> value. Only <c>SEQUENTIAL</c> and <c>RANDOM</c>
155+
/// differ in ordering between the two.
156+
/// </summary>
157+
private static int MapAdvice(int advise)
158+
{
159+
switch (advise)
160+
{
161+
case SEQUENTIAL: return 2; // POSIX_*_SEQUENTIAL
162+
case RANDOM: return 1; // POSIX_*_RANDOM
163+
default: return advise; // NORMAL=0, WILLNEED=3, DONTNEED=4, NOREUSE=5 are identical
164+
}
165+
}
63166

167+
private static int Fd(SafeFileHandle handle) => (int)handle.DangerousGetHandle();
168+
169+
private static IOException NewIOException(string operation, string filename)
170+
=> NewIOException(operation, filename, Marshal.GetLastWin32Error());
171+
172+
private static IOException NewIOException(string operation, string filename, int errno)
173+
{
174+
string where = filename is null ? operation : $"{operation} {filename}";
175+
// On Unix with SetLastError, GetLastWin32Error() returns errno. Win32Exception's message
176+
// is not meaningful on Unix, so include the raw errno for diagnosis.
177+
return new IOException($"{where} failed (errno {errno})");
178+
}
179+
180+
internal static void EnsureUnix()
181+
{
182+
if (Constants.WINDOWS)
183+
{
184+
throw new PlatformNotSupportedException(
185+
$"{nameof(NativePosixUtil)} requires Linux or macOS direct I/O and is not supported on Microsoft Windows.");
186+
}
187+
}
188+
189+
/// <summary>
190+
/// P/Invoke declarations for the <c>libc</c> functions used here. These replace the
191+
/// JNI/C++ native methods of the original implementation.
192+
/// </summary>
193+
private static class NativeMethods
194+
{
195+
private const string LIBC = "libc";
196+
197+
// open() flags. O_CREAT differs between Linux and macOS; O_DIRECT/O_NOATIME are Linux-only.
198+
internal const int O_RDONLY = 0x0;
199+
internal const int O_RDWR = 0x2;
200+
internal static readonly int O_CREAT = Constants.MAC_OS_X ? 0x0200 : 0x40;
201+
internal const int O_DIRECT = 0x4000; // Linux
202+
internal const int O_NOATIME = 0x40000; // Linux
203+
internal const int F_NOCACHE = 48; // macOS
204+
internal const int S_RW = 0x1B6; // 0666
205+
206+
[DllImport(LIBC, SetLastError = true, EntryPoint = "open")]
207+
internal static extern int open([MarshalAs(UnmanagedType.LPStr)] string pathname, int flags);
208+
209+
[DllImport(LIBC, SetLastError = true, EntryPoint = "open")]
210+
internal static extern int open([MarshalAs(UnmanagedType.LPStr)] string pathname, int flags, int mode);
211+
212+
[DllImport(LIBC, SetLastError = true)]
213+
internal static extern int close(int fd);
214+
215+
[DllImport(LIBC, SetLastError = true)]
216+
internal static extern int fcntl(int fd, int cmd, int arg);
217+
218+
[DllImport(LIBC, SetLastError = true)]
219+
internal static extern nint pread(int fd, IntPtr buf, nuint count, long offset);
220+
221+
[DllImport(LIBC, SetLastError = true)]
222+
internal static extern int posix_fadvise(int fd, long offset, long len, int advice);
223+
224+
[DllImport(LIBC, SetLastError = true)]
225+
internal static extern int posix_madvise(IntPtr addr, nuint length, int advice);
226+
227+
[DllImport(LIBC, SetLastError = true)]
228+
internal static extern int madvise(IntPtr addr, nuint length, int advice);
229+
}
230+
}
64231
}

0 commit comments

Comments
 (0)