1- """Generate CPython API wrapper function for a native function."""
1+ """Generate CPython API wrapper functions for native functions.
2+
3+ The wrapper functions are used by the CPython runtime when calling
4+ native functions from interpreted code, and when the called function
5+ can't be determined statically in compiled code. They validate, match,
6+ unbox and type check function arguments, and box return values as
7+ needed. All wrappers accept and return 'PyObject *' (boxed) values.
8+
9+ The wrappers aren't used for most calls between two native functions
10+ or methods in a single compilation unit.
11+ """
212
313from typing import List , Optional
414
1424from mypyc .namegen import NameGenerator
1525
1626
27+ # Generic vectorcall wrapper functions (Python 3.7+)
28+ #
29+ # A wrapper function has a signature like this:
30+ #
31+ # PyObject *fn(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
32+ #
33+ # The function takes a self object, pointer to an array of arguments,
34+ # the number of positional arguments, and a tuple of keyword argument
35+ # names (that are stored starting in args[nargs]).
36+ #
37+ # It returns the returned object, or NULL on an exception.
38+ #
39+ # These are more efficient than legacy wrapper functions, since
40+ # usually no tuple or dict objects need to be created for the
41+ # arguments. Vectorcalls also use pre-constructed str objects for
42+ # keyword argument names and other pre-computed information, instead
43+ # of processing the argument format string on each call.
44+
45+
1746def wrapper_function_header (fn : FuncIR , names : NameGenerator ) -> str :
18- return 'PyObject *{prefix}{name}(PyObject *self, PyObject *args, PyObject *kw)' .format (
19- prefix = PREFIX ,
20- name = fn .cname (names ))
47+ """Return header of a vectorcall wrapper function.
48+
49+ See comment above for a summary of the arguments.
50+ """
51+ return (
52+ 'PyObject *{prefix}{name}('
53+ 'PyObject *self, PyObject *const *args, size_t nargs, PyObject *kwnames)' ).format (
54+ prefix = PREFIX ,
55+ name = fn .cname (names ))
56+
57+
58+ def generate_traceback_code (fn : FuncIR ,
59+ emitter : Emitter ,
60+ source_path : str ,
61+ module_name : str ) -> str :
62+ # If we hit an error while processing arguments, then we emit a
63+ # traceback frame to make it possible to debug where it happened.
64+ # Unlike traceback frames added for exceptions seen in IR, we do this
65+ # even if there is no `traceback_name`. This is because the error will
66+ # have originated here and so we need it in the traceback.
67+ globals_static = emitter .static_name ('globals' , module_name )
68+ traceback_code = 'CPy_AddTraceback("%s", "%s", %d, %s);' % (
69+ source_path .replace ("\\ " , "\\ \\ " ),
70+ fn .traceback_name or fn .name ,
71+ fn .line ,
72+ globals_static )
73+ return traceback_code
74+
75+
76+ def make_arg_groups (args : List [RuntimeArg ]) -> List [List [RuntimeArg ]]:
77+ """Group arguments by kind."""
78+ return [[arg for arg in args if arg .kind == k ] for k in range (ARG_NAMED_OPT + 1 )]
79+
80+
81+ def reorder_arg_groups (groups : List [List [RuntimeArg ]]) -> List [RuntimeArg ]:
82+ """Reorder argument groups to match their order in a format string."""
83+ return groups [ARG_POS ] + groups [ARG_OPT ] + groups [ARG_NAMED_OPT ] + groups [ARG_NAMED ]
84+
85+
86+ def make_static_kwlist (args : List [RuntimeArg ]) -> str :
87+ arg_names = '' .join ('"{}", ' .format (arg .name ) for arg in args )
88+ return 'static const char * const kwlist[] = {{{}0}};' .format (arg_names )
2189
2290
2391def make_format_string (func_name : str , groups : List [List [RuntimeArg ]]) -> str :
24- # Construct the format string. Each group requires the previous
25- # groups delimiters to be present first.
92+ """Return a format string that specifies the accepted arguments.
93+
94+ The format string is an extended subset of what is supported by
95+ PyArg_ParseTupleAndKeywords(). Only the type 'O' is used, and we
96+ also support some extensions:
97+
98+ - Required keyword-only arguments are introduced after '@'
99+ - If the function receives *args or **kwargs, we add a '%' prefix
100+
101+ Each group requires the previous groups' delimiters to be present
102+ first.
103+
104+ These are used by both vectorcall and legacy wrapper functions.
105+ """
26106 main_format = ''
27107 if groups [ARG_STAR ] or groups [ARG_STAR2 ]:
28108 main_format += '%'
@@ -40,24 +120,80 @@ def generate_wrapper_function(fn: FuncIR,
40120 emitter : Emitter ,
41121 source_path : str ,
42122 module_name : str ) -> None :
43- """Generates a CPython-compatible wrapper function for a native function.
123+ """Generate a CPython-compatible vectorcall wrapper for a native function.
44124
45125 In particular, this handles unboxing the arguments, calling the native function, and
46126 then boxing the return value.
47127 """
48128 emitter .emit_line ('{} {{' .format (wrapper_function_header (fn , emitter .names )))
49129
50- # If we hit an error while processing arguments, then we emit a
51- # traceback frame to make it possible to debug where it happened.
52- # Unlike traceback frames added for exceptions seen in IR, we do this
53- # even if there is no `traceback_name`. This is because the error will
54- # have originated here and so we need it in the traceback.
55- globals_static = emitter .static_name ('globals' , module_name )
56- traceback_code = 'CPy_AddTraceback("%s", "%s", %d, %s);' % (
57- source_path .replace ("\\ " , "\\ \\ " ),
58- fn .traceback_name or fn .name ,
59- fn .line ,
60- globals_static )
130+ # If fn is a method, then the first argument is a self param
131+ real_args = list (fn .args )
132+ if fn .class_name and not fn .decl .kind == FUNC_STATICMETHOD :
133+ arg = real_args .pop (0 )
134+ emitter .emit_line ('PyObject *obj_{} = self;' .format (arg .name ))
135+
136+ # Need to order args as: required, optional, kwonly optional, kwonly required
137+ # This is because CPyArg_ParseStackAndKeywords format string requires
138+ # them grouped in that way.
139+ groups = make_arg_groups (real_args )
140+ reordered_args = reorder_arg_groups (groups )
141+
142+ emitter .emit_line (make_static_kwlist (reordered_args ))
143+ fmt = make_format_string (fn .name , groups )
144+ # Define the arguments the function accepts (but no types yet)
145+ emitter .emit_line ('static CPyArg_Parser parser = {{"{}", kwlist, 0}};' .format (fmt ))
146+
147+ for arg in real_args :
148+ emitter .emit_line ('PyObject *obj_{}{};' .format (
149+ arg .name , ' = NULL' if arg .optional else '' ))
150+
151+ cleanups = ['CPy_DECREF(obj_{});' .format (arg .name )
152+ for arg in groups [ARG_STAR ] + groups [ARG_STAR2 ]]
153+
154+ arg_ptrs = [] # type: List[str]
155+ if groups [ARG_STAR ] or groups [ARG_STAR2 ]:
156+ arg_ptrs += ['&obj_{}' .format (groups [ARG_STAR ][0 ].name ) if groups [ARG_STAR ] else 'NULL' ]
157+ arg_ptrs += ['&obj_{}' .format (groups [ARG_STAR2 ][0 ].name ) if groups [ARG_STAR2 ] else 'NULL' ]
158+ arg_ptrs += ['&obj_{}' .format (arg .name ) for arg in reordered_args ]
159+
160+ emitter .emit_lines (
161+ 'if (!CPyArg_ParseStackAndKeywords(args, nargs, kwnames, &parser{})) {{' .format (
162+ '' .join (', ' + n for n in arg_ptrs )),
163+ 'return NULL;' ,
164+ '}' )
165+ traceback_code = generate_traceback_code (fn , emitter , source_path , module_name )
166+ generate_wrapper_core (fn , emitter , groups [ARG_OPT ] + groups [ARG_NAMED_OPT ],
167+ cleanups = cleanups ,
168+ traceback_code = traceback_code )
169+
170+ emitter .emit_line ('}' )
171+
172+
173+ # Legacy generic wrapper functions
174+ #
175+ # These take a self object, a Python tuple of positional arguments,
176+ # and a dict of keyword arguments. These are a lot slower than
177+ # vectorcall wrappers, especially in calls involving keyword
178+ # arguments.
179+
180+
181+ def legacy_wrapper_function_header (fn : FuncIR , names : NameGenerator ) -> str :
182+ return 'PyObject *{prefix}{name}(PyObject *self, PyObject *args, PyObject *kw)' .format (
183+ prefix = PREFIX ,
184+ name = fn .cname (names ))
185+
186+
187+ def generate_legacy_wrapper_function (fn : FuncIR ,
188+ emitter : Emitter ,
189+ source_path : str ,
190+ module_name : str ) -> None :
191+ """Generates a CPython-compatible legacy wrapper for a native function.
192+
193+ In particular, this handles unboxing the arguments, calling the native function, and
194+ then boxing the return value.
195+ """
196+ emitter .emit_line ('{} {{' .format (legacy_wrapper_function_header (fn , emitter .names )))
61197
62198 # If fn is a method, then the first argument is a self param
63199 real_args = list (fn .args )
@@ -68,11 +204,10 @@ def generate_wrapper_function(fn: FuncIR,
68204 # Need to order args as: required, optional, kwonly optional, kwonly required
69205 # This is because CPyArg_ParseTupleAndKeywords format string requires
70206 # them grouped in that way.
71- groups = [[ arg for arg in real_args if arg . kind == k ] for k in range ( ARG_NAMED_OPT + 1 )]
72- reordered_args = groups [ ARG_POS ] + groups [ ARG_OPT ] + groups [ ARG_NAMED_OPT ] + groups [ ARG_NAMED ]
207+ groups = make_arg_groups ( real_args )
208+ reordered_args = reorder_arg_groups ( groups )
73209
74- arg_names = '' .join ('"{}", ' .format (arg .name ) for arg in reordered_args )
75- emitter .emit_line ('static char *kwlist[] = {{{}0}};' .format (arg_names ))
210+ emitter .emit_line (make_static_kwlist (reordered_args ))
76211 for arg in real_args :
77212 emitter .emit_line ('PyObject *obj_{}{};' .format (
78213 arg .name , ' = NULL' if arg .optional else '' ))
@@ -91,13 +226,17 @@ def generate_wrapper_function(fn: FuncIR,
91226 make_format_string (fn .name , groups ), '' .join (', ' + n for n in arg_ptrs )),
92227 'return NULL;' ,
93228 '}' )
229+ traceback_code = generate_traceback_code (fn , emitter , source_path , module_name )
94230 generate_wrapper_core (fn , emitter , groups [ARG_OPT ] + groups [ARG_NAMED_OPT ],
95231 cleanups = cleanups ,
96232 traceback_code = traceback_code )
97233
98234 emitter .emit_line ('}' )
99235
100236
237+ # Specialized wrapper functions
238+
239+
101240def generate_dunder_wrapper (cl : ClassIR , fn : FuncIR , emitter : Emitter ) -> str :
102241 """Generates a wrapper for native __dunder__ methods to be able to fit into the mapping
103242 protocol slot. This specifically means that the arguments are taken as *PyObjects and returned
@@ -214,12 +353,16 @@ def generate_bool_wrapper(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str:
214353 return name
215354
216355
356+ # Helpers
357+
358+
217359def generate_wrapper_core (fn : FuncIR , emitter : Emitter ,
218360 optional_args : Optional [List [RuntimeArg ]] = None ,
219361 arg_names : Optional [List [str ]] = None ,
220362 cleanups : Optional [List [str ]] = None ,
221363 traceback_code : Optional [str ] = None ) -> None :
222364 """Generates the core part of a wrapper function for a native function.
365+
223366 This expects each argument as a PyObject * named obj_{arg} as a precondition.
224367 It converts the PyObject *s to the necessary types, checking and unboxing if necessary,
225368 makes the call, then boxes the result if necessary and returns it.
0 commit comments