Repository: numba/numba

Issue:
Bug on pyapi.call when args is None
## Reporting a bug

- [x] I am using the latest released version of Numba (most recent is visible in
 the change log (https://github.com/numba/numba/blob/master/CHANGE_LOG).
- [x] I have included below a minimal working reproducer (if you are unsure how
 to write one see http://matthewrocklin.com/blog/work/2018/02/28/minimal-bug-reports).

tldr: Calling `pyapi.call(obj, args=None, kws)` will result in a segfault.

Code to reproduce:

```python
from numba import njit
from numba.core import types, cgutils, extending
from numba.core.datamodel import registry, models
from llvmlite import ir

class Dense():
    def __init__(self, *, N):
        self.N = N

class DenseType(types.Type):
    def __init__(self, *, N_type):
        self.N_type: int = N_type
        name = f"Dense<{N_type}>"
        super().__init__(name)

    # Type is mutable
    mutable = True

    @property
    def key(self):
        return self.N_type


@registry.register_default(DenseType)
class DenseModel(models.StructModel):
    def __init__(self, dmm, fe_type: DenseType):
        members = [
            ("N", fe_type.N_type),
        ]
        super().__init__(dmm, fe_type, members)


@extending.type_callable(Dense)
def type_dense(context):
    def typer(N):
        return DenseType(N_type=N)
    return typer


@extending.lower_builtin(Dense, types.Any)
def sparse_dense_constructor(context, builder, sig, args):
    [N] = args
    dense = cgutils.create_struct_proxy(sig.return_type)(context, builder)
    dense.N = N
    return dense._getvalue()

@extending.box(DenseType)
def box_dense(typ: DenseType, val, c):
    """
    Convert a native dense structure to a Dense object.
    """
    i1 = ir.IntType(1)

    dense = cgutils.create_struct_proxy(typ)(c.context, c.builder, value=val)

    N_obj = c.pyapi.long_from_long(dense.N)
    class_obj = c.pyapi.unserialize(c.pyapi.serialize_object(Dense))

    kwds = c.pyapi.dict_pack({"N": N_obj}.items())
    res = c.pyapi.call(class_obj, None, kwds)  # segfault

    # empty_tuple = c.pyapi.tuple_new(0)
    # res = c.pyapi.call(class_obj, empty_tuple, kwds)  # works
    # c.pyapi.decref(empty_tuple)

    c.pyapi.decref(N_obj)
    c.pyapi.decref(class_obj)
    c.pyapi.decref(kwds)
    return res

@njit
def foo():
    d = Dense(N=3)
    return d

print(foo())
```

The interesting part happens inside `extending.box(DenseType)` in which uses `c.pyapi.call(class_obj, None, kwds)` to call the dense constructor. Note that the constructor is a keyword-only function. Inside the `c.pyapi.call`, if args is `None`, args is assigned to the return value of `PythonApi.get_null_object()`, which calls `PyObject_call` function from CPython. The problem with this is that the CPython docs explicit says that `args` must not be `NULL`:

> Call a callable Python object callable, with arguments given by the tuple args, and named arguments given by the dictionary kwargs.
>
>args must not be NULL, use an empty tuple if no arguments are needed. If no named arguments are needed, kwargs can be NULL.

If this is really an issue, I think is just a matter of replacing `args = self.get_null_object()` by `args = self.tuple_new(0)` to solve this problem.






Discussion:
> If this is really an issue, I think is just a matter of replacing args = self.get_null_object() by args = self.tuple_new(0) to solve this problem.

Yes, we should replace it with an empty tuple, following the CPython doc.

Resolving patch:
diff --git a/numba/core/pythonapi.py b/numba/core/pythonapi.py
--- a/numba/core/pythonapi.py
+++ b/numba/core/pythonapi.py
@@ -944,13 +944,16 @@ def call_method(self, callee, method, objargs=()):
         return self.builder.call(fn, args)
 
     def call(self, callee, args=None, kws=None):
-        if args is None:
-            args = self.get_null_object()
+        if args_was_none := args is None:
+            args = self.tuple_new(0)
         if kws is None:
             kws = self.get_null_object()
         fnty = ir.FunctionType(self.pyobj, [self.pyobj] * 3)
         fn = self._get_function(fnty, name="PyObject_Call")
-        return self.builder.call(fn, (callee, args, kws))
+        result = self.builder.call(fn, (callee, args, kws))
+        if args_was_none:
+            self.decref(args)
+        return result
 
     def object_type(self, obj):
         """Emit a call to ``PyObject_Type(obj)`` to get the type of ``obj``.


Test patch:
diff --git a/numba/tests/test_pythonapi.py b/numba/tests/test_pythonapi.py
--- a/numba/tests/test_pythonapi.py
+++ b/numba/tests/test_pythonapi.py
@@ -2,7 +2,8 @@
 import unittest
 from numba.core import types
 from numba.core.extending import intrinsic
-from numba import jit
+from numba import jit, njit
+from numba.tests.support import captured_stdout
 
 
 @intrinsic
@@ -78,5 +79,45 @@ def test_PyBytes_AsStringAndSize(self):
         self.assertEqual(obj, b'hello\x00world')
 
 
+class PythonAPIEmptyArgs(unittest.TestCase):
+    def test_empty_args(self):
+        def callme(**kwargs):
+            print("callme", kwargs)
+
+        @intrinsic
+        def py_call(tyctx):
+            def codegen(context, builder, sig, args):
+                pyapi = context.get_python_api(builder)
+                gil = pyapi.gil_ensure()
+
+                num = pyapi.long_from_longlong(
+                    context.get_constant(types.intp, 0xCAFE)
+                )
+                kwds = pyapi.dict_pack({"key": num}.items())
+                fn_print = pyapi.unserialize(pyapi.serialize_object(callme))
+                # segfault: https://github.com/numba/numba/issues/5871
+                res = pyapi.call(fn_print, None, kwds)
+
+                pyapi.decref(res)
+                pyapi.decref(fn_print)
+                pyapi.decref(kwds)
+                pyapi.decref(num)
+
+                pyapi.gil_release(gil)
+                return res
+
+            return types.none(), codegen
+
+        @njit
+        def foo():
+            py_call()
+
+        with captured_stdout() as out:
+            foo()
+        d = {"key": 0xCAFE}
+        expected = f"callme {d}\n"
+        self.assertEqual(out.getvalue(), expected)
+
+
 if __name__ == '__main__':
     unittest.main()