Skip to content
Merged
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 154 additions & 20 deletions src/core/IronPython.Modules/fcntl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#nullable enable

using System;
using System.Buffers;
using System.Diagnostics;
using System.Numerics;
using System.Reflection;
Expand Down Expand Up @@ -119,32 +120,165 @@ public static object fcntl(CodeContext context, object? fd, int cmd, object? arg
#endregion


#region ioctl
#region ioctl

// The actual signature of ioctl is
//
// int ioctl(int, unsigned long, ...)
//
// but .NET, as of Jan 2025, still does not support varargs in P/Invoke [1]
// so as a workaround, nonvararg prototypes are defined for each architecture.
// [1]: https://github.com/dotnet/runtime/issues/48796

#if NET10_0_OR_GREATER
#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
#endif

[DllImport("libc", SetLastError = true, EntryPoint = "ioctl")]
private static extern unsafe int _ioctl(int fd, ulong request, void* arg);
[DllImport("libc", SetLastError = true, EntryPoint = "ioctl")]
private static extern int _ioctl(int fd, ulong request, long arg);

[DllImport("libc", SetLastError = true, EntryPoint = "ioctl")]
private static extern unsafe int _ioctl_arm64(int fd, ulong request,
// pad register arguments (first 8) to force vararg on stack
// ARM: https://github.com/ARM-software/abi-aa/blob/main/aapcs64/aapcs64.rst#appendix-variable-argument-lists
// Apple: https://developer.apple.com/documentation/xcode/writing-arm64-code-for-apple-platforms
nint r2, nint r3, nint r4, nint r5, nint r6, nint r7,
void* arg);
[DllImport("libc", SetLastError = true, EntryPoint = "ioctl")]
private static extern int _ioctl_arm64(int fd, ulong request,
nint r2, nint r3, nint r4, nint r5, nint r6, nint r7,
long arg);


// request will be int, uint or BigInteger, and in Python is limited to values that can fit in 32 bits (unchecked)
// long should capture all allowed request values
// return value is int, bytes, or LightException
[LightThrowing]
public static object ioctl(int fd, long request, [NotNone] IBufferProtocol arg, bool mutate_flag = true) {
CheckFileDescriptor(fd);

ulong cmd = unchecked((ulong)request);

const int defaultBufSize = 1024;
int bufSize;
IPythonBuffer? buf = null;

// supporting fcntl.ioctl(fileno, termios.TIOCGWINSZ, buf)
// where buf = array.array('h', [0, 0, 0, 0])
public static object ioctl(CodeContext context, int fd, int cmd, [NotNone] IBufferProtocol arg, int mutate_flag = 1) {
if (cmd == PythonTermios.TIOCGWINSZ) {
using IPythonBuffer buf = arg.GetBuffer();
if (mutate_flag) {
buf = arg.GetBufferNoThrow(BufferFlags.Writable);
}
if (buf is not null) {
bufSize = buf.AsSpan().Length; // check early if buf is indeed writable
} else {
buf = arg.GetBuffer(BufferFlags.Simple);
bufSize = buf.AsReadOnlySpan().Length;
if (bufSize > defaultBufSize) {
buf.Dispose();
throw PythonOps.ValueError("ioctl bytes arg too long");
}
mutate_flag = false; // return a buffer, not integer
}
bool in_place = bufSize > defaultBufSize; // only large buffers are mutated in place

#if !NETCOREAPP
throw new PlatformNotSupportedException("ioctl is not supported on Mono");
#else
try {
Debug.Assert(!in_place || mutate_flag); // in_place implies mutate_flag

Span<byte> workSpan;
if (in_place) {
workSpan = buf.AsSpan();
} else {
workSpan = new byte[defaultBufSize + 1]; // +1 for extra NUL byte
Debug.Assert(bufSize <= defaultBufSize);
buf.AsReadOnlySpan().CopyTo(workSpan);
}
int result;
Errno errno;
unsafe {
fixed (byte* ptr = workSpan) {
do {
if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64) {
// workaround for Arm64 vararg calling convention (but not for ARM64EC on Windows)
result = _ioctl_arm64(fd, cmd, 0, 0, 0, 0, 0, 0, ptr);
} else {
result = _ioctl(fd, cmd, ptr);
}
} while (UnixMarshal.ShouldRetrySyscall(result, out errno));
}
}

Span<short> winsize = stackalloc short[4];
winsize[0] = (short)Console.WindowHeight;
winsize[1] = (short)Console.WindowWidth;
winsize[2] = (short)Console.BufferHeight; // buffer height and width are not accurate on macOS
winsize[3] = (short)Console.BufferWidth;
Span<byte> payload = MemoryMarshal.Cast<short, byte>(winsize);
if (result == -1) {
return LightExceptions.Throw(PythonNT.GetOsError(NativeConvert.FromErrno(errno)));
}
if (mutate_flag) {
if (!in_place) {
workSpan.Slice(0, bufSize).CopyTo(buf.AsSpan());
}
return ScriptingRuntimeHelpers.Int32ToObject(result);
} else {
Debug.Assert(!in_place);
byte[] response = new byte[bufSize];
workSpan.Slice(0, bufSize).CopyTo(response);
return Bytes.Make(response);
}
} finally {
buf.Dispose();
}
#endif
}


[LightThrowing]
public static object ioctl(int fd, long request, [Optional] object? arg, bool mutate_flag = true) {
CheckFileDescriptor(fd);

ulong cmd = unchecked((ulong)request);

long data = arg switch {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I should have suggested this in the last PR, but what about using PythonOps.Index(arg)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked at it too, but it doesn't do what is needed here:

  • it doesn't handle System.Reflection.Missing
  • it doesn't handle non-standard integer types like uint, long, ulong
  • it returns object so a switch is still needed to cast to an unmanaged type
  • it tries to call __index__ and this is not what Python does here

But now this code is being used in two places so I guess I might as well factor it out. I'm not sure it deserves to be promoted to PythonOps, but something similar is used in termios, except for the error message. Maybe I just leave it here and make internal; termios is already using some helper methods from fcntl, and these two modules are somewhat coupled.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • it doesn't handle non-standard integer types like uint, long, ulong

They don't have a fast path since they're not typically expected in Python code, but they do end up being resolves via __index__ calls.

  • it returns object so a switch is still needed to cast to an unmanaged type

True. Could have an some helpers such as PythonOps.IndexAsLong.

  • it tries to call __index__ and this is not what Python does here

In recent versions of Python, __index__ is supported pretty much everywhere. For example (only tried on 3.10.12, I guess it's always possible they reverted since):

import fcntl
import termios

class Index:
    def __init__(self, idx):
        self.idx = idx
    def __index__(self):
        return self.idx

fcntl.ioctl(0, Index(termios.TIOCGPGRP), b"  ")

# not a valid call, but does throw TypeError: __index__ returned non-int (type bytes)
fcntl.ioctl(0, Index(termios.TIOCGPGRP), Index(b"  "))

Anyway, I don't have an objection to what you're proposing it just looked a lot like what index does.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In recent versions of Python, __index__ is supported pretty much everywhere. For example (only tried on 3.10.12, I guess it's always possible they reverted since):

Ah, I see. On maxOS I am using 3.7 and on Linux 3.6 for testing (which are the oldest I could easily get, 3.4 is nowhere easily available anymore). I see that 3.8 – 3.12 all support __index__. It's moving target…

Could have an some helpers such as PythonOps.IndexAsLong.

I like the idea. From my memory, this or a similar conversion is happening in more places. I think PythonOps.TryGetInt64 that doesn't do unnecessary try/catch would be more practical. Anyway, I'll keep it in mind, this is not something for this PR.

Missing => 0,
int i => i,
uint ui => ui,
long l => l,
ulong ul => (long)ul,
BigInteger bi => (long)bi,
Extensible<BigInteger> ebi => (long)ebi.Value,
_ => throw PythonOps.TypeErrorForBadInstance("integer argument expected, got {0}", arg)
};

if (buf.IsReadOnly || mutate_flag == 0) {
byte[] res = buf.ToArray();
payload.Slice(0, Math.Min(payload.Length, res.Length)).CopyTo(res);
return Bytes.Make(res);
#if !NETCOREAPP
throw new PlatformNotSupportedException("ioctl is not supported on Mono");
#else
int result;
Errno errno;
do {
if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64) {
// workaround for Arm64 vararg calling convention (but not for ARM64EC on Windows)
result = _ioctl_arm64(fd, cmd, 0, 0, 0, 0, 0, 0, data);
} else {
var res = buf.AsSpan();
payload.Slice(0, Math.Min(payload.Length, res.Length)).CopyTo(res);
return 0;
result = _ioctl(fd, cmd, data);
}
} while (UnixMarshal.ShouldRetrySyscall(result, out errno));

if (result == -1) {
return LightExceptions.Throw(PythonNT.GetOsError(NativeConvert.FromErrno(errno)));
}
return ScriptingRuntimeHelpers.Int32ToObject(result);
#endif
}


[LightThrowing]
public static object ioctl(CodeContext context, object? fd, long request, [Optional] object? arg, bool mutate_flag = true) {
int fileno = GetFileDescriptor(context, fd);

if (arg is IBufferProtocol bp) {
return ioctl(fileno, request, bp, mutate_flag);
}
throw new NotImplementedException($"ioctl: unsupported command {cmd}");

return ioctl(fileno, request, arg, mutate_flag);
}

#endregion
Expand Down