55#nullable enable
66
77using System ;
8+ using System . Buffers ;
89using System . Diagnostics ;
910using System . Numerics ;
1011using System . Reflection ;
1112using System . Runtime . InteropServices ;
1213using System . Runtime . Versioning ;
14+ using System . Text ;
1315
1416using Mono . Unix ;
1517using 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