Skip to content

Commit b5fce64

Browse files
authored
Implement syscall ioctl (#1903)
* Implement syscall `ioctl` * Simplify by avoiding unmanaged memory allocation * Accept `str` as argument to `fcntl` and `ioctl`
1 parent f0fbee1 commit b5fce64

File tree

1 file changed

+173
-36
lines changed

1 file changed

+173
-36
lines changed

src/core/IronPython.Modules/fcntl.cs

Lines changed: 173 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
#nullable enable
66

77
using System;
8+
using System.Buffers;
89
using System.Diagnostics;
910
using System.Numerics;
1011
using System.Reflection;
1112
using System.Runtime.InteropServices;
1213
using System.Runtime.Versioning;
14+
using System.Text;
1315

1416
using Mono.Unix;
1517
using Mono.Unix.Native;
@@ -77,15 +79,13 @@ public static object fcntl(int fd, int cmd, [NotNone] Bytes arg) {
7779
public static object fcntl(int fd, int cmd, [Optional] object? arg) {
7880
CheckFileDescriptor(fd);
7981

80-
long data = arg switch {
81-
Missing => 0,
82-
int i => i,
83-
uint ui => ui,
84-
long l => l,
85-
ulong ul => (long)ul,
86-
BigInteger bi => (long)bi,
87-
Extensible<BigInteger> ebi => (long)ebi.Value,
88-
_ => throw PythonOps.TypeErrorForBadInstance("integer argument expected, got {0}", arg)
82+
if (!TryGetInt64(arg, out long data)) {
83+
return arg switch {
84+
Bytes bytes => fcntl(fd, cmd, bytes),
85+
string s => fcntl(fd, cmd, Bytes.Make(Encoding.UTF8.GetBytes(s))),
86+
Extensible<string> es => fcntl(fd, cmd, Bytes.Make(Encoding.UTF8.GetBytes(es.Value))),
87+
_ => throw PythonOps.TypeErrorForBadInstance("integer or bytes argument expected, got {0}", arg)
88+
};
8989
};
9090

9191
if (!NativeConvert.TryToFcntlCommand(cmd, out FcntlCommand fcntlCommand)) {
@@ -106,47 +106,164 @@ public static object fcntl(int fd, int cmd, [Optional] object? arg) {
106106

107107

108108
[LightThrowing]
109-
public static object fcntl(CodeContext context, object? fd, int cmd, object? arg = null) {
110-
int fileno = GetFileDescriptor(context, fd);
109+
public static object fcntl(CodeContext context, object? fd, int cmd, [Optional] object? arg)
110+
=> fcntl(GetFileDescriptor(context, fd), cmd, arg);
111+
112+
#endregion
113+
114+
115+
#region ioctl
116+
117+
// The actual signature of ioctl is
118+
//
119+
// int ioctl(int, unsigned long, ...)
120+
//
121+
// but .NET, as of Jan 2025, still does not support varargs in P/Invoke [1]
122+
// so as a workaround, nonvararg prototypes are defined for each architecture.
123+
// [1]: https://github.com/dotnet/runtime/issues/48796
124+
125+
#if NET10_0_OR_GREATER
126+
#error Check if this version of .NET supports P/Invoke of variadic functions; if not, change the condition to recheck at next major .NET version
127+
#endif
128+
129+
[DllImport("libc", SetLastError = true, EntryPoint = "ioctl")]
130+
private static extern unsafe int _ioctl(int fd, ulong request, void* arg);
131+
[DllImport("libc", SetLastError = true, EntryPoint = "ioctl")]
132+
private static extern int _ioctl(int fd, ulong request, long arg);
133+
134+
[DllImport("libc", SetLastError = true, EntryPoint = "ioctl")]
135+
private static extern unsafe int _ioctl_arm64(int fd, ulong request,
136+
// pad register arguments (first 8) to force vararg on stack
137+
// ARM: https://github.com/ARM-software/abi-aa/blob/main/aapcs64/aapcs64.rst#appendix-variable-argument-lists
138+
// Apple: https://developer.apple.com/documentation/xcode/writing-arm64-code-for-apple-platforms
139+
nint r2, nint r3, nint r4, nint r5, nint r6, nint r7,
140+
void* arg);
141+
[DllImport("libc", SetLastError = true, EntryPoint = "ioctl")]
142+
private static extern int _ioctl_arm64(int fd, ulong request,
143+
nint r2, nint r3, nint r4, nint r5, nint r6, nint r7,
144+
long arg);
145+
146+
147+
// request will be int, uint or BigInteger, and in Python is limited to values that can fit in 32 bits (unchecked)
148+
// long should capture all allowed request values
149+
// return value is int, bytes, or LightException
150+
[LightThrowing]
151+
public static object ioctl(int fd, long request, [NotNone] IBufferProtocol arg, bool mutate_flag = true) {
152+
CheckFileDescriptor(fd);
153+
154+
ulong cmd = unchecked((ulong)request);
111155

112-
if (arg is Bytes bytes) {
113-
return fcntl(fileno, cmd, bytes);
156+
const int defaultBufSize = 1024;
157+
int bufSize;
158+
IPythonBuffer? buf = null;
159+
160+
if (mutate_flag) {
161+
buf = arg.GetBufferNoThrow(BufferFlags.Writable);
162+
}
163+
if (buf is not null) {
164+
bufSize = buf.AsSpan().Length; // check early if buf is indeed writable
165+
} else {
166+
buf = arg.GetBuffer(BufferFlags.Simple);
167+
bufSize = buf.AsReadOnlySpan().Length;
168+
if (bufSize > defaultBufSize) {
169+
buf.Dispose();
170+
throw PythonOps.ValueError("ioctl bytes arg too long");
171+
}
172+
mutate_flag = false; // return a buffer, not integer
114173
}
174+
bool in_place = bufSize > defaultBufSize; // only large buffers are mutated in place
115175

116-
return fcntl(fileno, cmd, arg);
117-
}
176+
#if !NETCOREAPP
177+
throw new PlatformNotSupportedException("ioctl is not supported on Mono");
178+
#else
179+
try {
180+
Debug.Assert(!in_place || mutate_flag); // in_place implies mutate_flag
118181

119-
#endregion
182+
Span<byte> workSpan;
183+
if (in_place) {
184+
workSpan = buf.AsSpan();
185+
} else {
186+
workSpan = new byte[defaultBufSize + 1]; // +1 for extra NUL byte
187+
Debug.Assert(bufSize <= defaultBufSize);
188+
buf.AsReadOnlySpan().CopyTo(workSpan);
189+
}
190+
int result;
191+
Errno errno;
192+
unsafe {
193+
fixed (byte* ptr = workSpan) {
194+
do {
195+
if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64) {
196+
// workaround for Arm64 vararg calling convention (but not for ARM64EC on Windows)
197+
result = _ioctl_arm64(fd, cmd, 0, 0, 0, 0, 0, 0, ptr);
198+
} else {
199+
result = _ioctl(fd, cmd, ptr);
200+
}
201+
} while (UnixMarshal.ShouldRetrySyscall(result, out errno));
202+
}
203+
}
204+
205+
if (result == -1) {
206+
return LightExceptions.Throw(PythonNT.GetOsError(NativeConvert.FromErrno(errno)));
207+
}
208+
if (mutate_flag) {
209+
if (!in_place) {
210+
workSpan.Slice(0, bufSize).CopyTo(buf.AsSpan());
211+
}
212+
return ScriptingRuntimeHelpers.Int32ToObject(result);
213+
} else {
214+
Debug.Assert(!in_place);
215+
byte[] response = new byte[bufSize];
216+
workSpan.Slice(0, bufSize).CopyTo(response);
217+
return Bytes.Make(response);
218+
}
219+
} finally {
220+
buf.Dispose();
221+
}
222+
#endif
223+
}
120224

121225

122-
#region ioctl
226+
[LightThrowing]
227+
public static object ioctl(int fd, long request, [Optional] object? arg, bool mutate_flag = true) {
228+
CheckFileDescriptor(fd);
123229

124-
// supporting fcntl.ioctl(fileno, termios.TIOCGWINSZ, buf)
125-
// where buf = array.array('h', [0, 0, 0, 0])
126-
public static object ioctl(CodeContext context, int fd, int cmd, [NotNone] IBufferProtocol arg, int mutate_flag = 1) {
127-
if (cmd == PythonTermios.TIOCGWINSZ) {
128-
using IPythonBuffer buf = arg.GetBuffer();
230+
if (!TryGetInt64(arg, out long data)) {
231+
return arg switch {
232+
IBufferProtocol bp => ioctl(fd, request, bp),
233+
string s => ioctl(fd, request, Bytes.Make(Encoding.UTF8.GetBytes(s))),
234+
Extensible<string> es => ioctl(fd, request, Bytes.Make(Encoding.UTF8.GetBytes(es.Value))),
235+
_ => throw PythonOps.TypeErrorForBadInstance("integer or a bytes-like argument expected, got {0}", arg)
236+
};
237+
};
129238

130-
Span<short> winsize = stackalloc short[4];
131-
winsize[0] = (short)Console.WindowHeight;
132-
winsize[1] = (short)Console.WindowWidth;
133-
winsize[2] = (short)Console.BufferHeight; // buffer height and width are not accurate on macOS
134-
winsize[3] = (short)Console.BufferWidth;
135-
Span<byte> payload = MemoryMarshal.Cast<short, byte>(winsize);
239+
ulong cmd = unchecked((ulong)request);
136240

137-
if (buf.IsReadOnly || mutate_flag == 0) {
138-
byte[] res = buf.ToArray();
139-
payload.Slice(0, Math.Min(payload.Length, res.Length)).CopyTo(res);
140-
return Bytes.Make(res);
241+
#if !NETCOREAPP
242+
throw new PlatformNotSupportedException("ioctl is not supported on Mono");
243+
#else
244+
int result;
245+
Errno errno;
246+
do {
247+
if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64) {
248+
// workaround for Arm64 vararg calling convention (but not for ARM64EC on Windows)
249+
result = _ioctl_arm64(fd, cmd, 0, 0, 0, 0, 0, 0, data);
141250
} else {
142-
var res = buf.AsSpan();
143-
payload.Slice(0, Math.Min(payload.Length, res.Length)).CopyTo(res);
144-
return 0;
251+
result = _ioctl(fd, cmd, data);
145252
}
253+
} while (UnixMarshal.ShouldRetrySyscall(result, out errno));
254+
255+
if (result == -1) {
256+
return LightExceptions.Throw(PythonNT.GetOsError(NativeConvert.FromErrno(errno)));
146257
}
147-
throw new NotImplementedException($"ioctl: unsupported command {cmd}");
258+
return ScriptingRuntimeHelpers.Int32ToObject(result);
259+
#endif
148260
}
149261

262+
263+
[LightThrowing]
264+
public static object ioctl(CodeContext context, object? fd, long request, [Optional] object? arg, bool mutate_flag = true)
265+
=> ioctl(GetFileDescriptor(context, fd), request, arg, mutate_flag);
266+
150267
#endregion
151268

152269

@@ -242,6 +359,26 @@ private static void CheckFileDescriptor(int fd) {
242359
}
243360
}
244361

362+
363+
private static bool TryGetInt64(object? obj, out long value) {
364+
int success = 1;
365+
value = obj switch {
366+
Missing => 0,
367+
int i => i,
368+
uint ui => ui,
369+
long l => l,
370+
ulong ul => (long)ul,
371+
BigInteger bi => (long)bi,
372+
Extensible<BigInteger> ebi => (long)ebi.Value,
373+
byte b => b,
374+
sbyte sb => sb,
375+
short s => s,
376+
ushort us => us,
377+
_ => success = 0
378+
};
379+
return success != 0;
380+
}
381+
245382
#endregion
246383

247384

0 commit comments

Comments
 (0)