save_reduce(PicklerObject *self, PyObject *args, PyObject *obj)
{
    PyObject *callable;
    PyObject *argtup;
    PyObject *state = NULL;
    PyObject *listitems = Py_None;
    PyObject *dictitems = Py_None;
    PickleState *st = _Pickle_GetGlobalState();
    Py_ssize_t size;
    int use_newobj = 0, use_newobj_ex = 0;

    const char reduce_op = REDUCE;
    const char build_op = BUILD;
    const char newobj_op = NEWOBJ;
    const char newobj_ex_op = NEWOBJ_EX;

    size = PyTuple_Size(args);
    if (size < 2 || size > 5) {
        PyErr_SetString(st->PicklingError, "tuple returned by "
                        "__reduce__ must contain 2 through 5 elements");
        return -1;
    }

    if (!PyArg_UnpackTuple(args, "save_reduce", 2, 5,
                           &callable, &argtup, &state, &listitems, &dictitems))
        return -1;

    if (!PyCallable_Check(callable)) {
        PyErr_SetString(st->PicklingError, "first item of the tuple "
                        "returned by __reduce__ must be callable");
        return -1;
    }
    if (!PyTuple_Check(argtup)) {
        PyErr_SetString(st->PicklingError, "second item of the tuple "
                        "returned by __reduce__ must be a tuple");
        return -1;
    }

    if (state == Py_None)
        state = NULL;

    if (listitems == Py_None)
        listitems = NULL;
    else if (!PyIter_Check(listitems)) {
        PyErr_Format(st->PicklingError, "fourth element of the tuple "
                     "returned by __reduce__ must be an iterator, not %s",
                     Py_TYPE(listitems)->tp_name);
        return -1;
    }

    if (dictitems == Py_None)
        dictitems = NULL;
    else if (!PyIter_Check(dictitems)) {
        PyErr_Format(st->PicklingError, "fifth element of the tuple "
                     "returned by __reduce__ must be an iterator, not %s",
                     Py_TYPE(dictitems)->tp_name);
        return -1;
    }

    if (self->proto >= 2) {
        PyObject *name;
        _Py_IDENTIFIER(__name__);

        if (_PyObject_LookupAttrId(callable, &PyId___name__, &name) < 0) {
            return -1;
        }
        if (name != NULL && PyUnicode_Check(name)) {
            _Py_IDENTIFIER(__newobj_ex__);
            use_newobj_ex = _PyUnicode_EqualToASCIIId(
                    name, &PyId___newobj_ex__);
            if (!use_newobj_ex) {
                _Py_IDENTIFIER(__newobj__);
                use_newobj = _PyUnicode_EqualToASCIIId(name, &PyId___newobj__);
            }
        }
        Py_XDECREF(name);
    }

    if (use_newobj_ex) {
        PyObject *cls;
        PyObject *args;
        PyObject *kwargs;

        if (PyTuple_GET_SIZE(argtup) != 3) {
            PyErr_Format(st->PicklingError,
                         "length of the NEWOBJ_EX argument tuple must be "
                         "exactly 3, not %zd", PyTuple_GET_SIZE(argtup));
            return -1;
        }

        cls = PyTuple_GET_ITEM(argtup, 0);
        if (!PyType_Check(cls)) {
            PyErr_Format(st->PicklingError,
                         "first item from NEWOBJ_EX argument tuple must "
                         "be a class, not %.200s", Py_TYPE(cls)->tp_name);
            return -1;
        }
        args = PyTuple_GET_ITEM(argtup, 1);
        if (!PyTuple_Check(args)) {
            PyErr_Format(st->PicklingError,
                         "second item from NEWOBJ_EX argument tuple must "
                         "be a tuple, not %.200s", Py_TYPE(args)->tp_name);
            return -1;
        }
        kwargs = PyTuple_GET_ITEM(argtup, 2);
        if (!PyDict_Check(kwargs)) {
            PyErr_Format(st->PicklingError,
                         "third item from NEWOBJ_EX argument tuple must "
                         "be a dict, not %.200s", Py_TYPE(kwargs)->tp_name);
            return -1;
        }

        if (self->proto >= 4) {
            if (save(self, cls, 0) < 0 ||
                save(self, args, 0) < 0 ||
                save(self, kwargs, 0) < 0 ||
                _Pickler_Write(self, &newobj_ex_op, 1) < 0) {
                return -1;
            }
        }
        else {
            PyObject *newargs;
            PyObject *cls_new;
            Py_ssize_t i;
            _Py_IDENTIFIER(__new__);

            newargs = PyTuple_New(PyTuple_GET_SIZE(args) + 2);
            if (newargs == NULL)
                return -1;

            cls_new = _PyObject_GetAttrId(cls, &PyId___new__);
            if (cls_new == NULL) {
                Py_DECREF(newargs);
                return -1;
            }
            PyTuple_SET_ITEM(newargs, 0, cls_new);
            Py_INCREF(cls);
            PyTuple_SET_ITEM(newargs, 1, cls);
            for (i = 0; i < PyTuple_GET_SIZE(args); i++) {
                PyObject *item = PyTuple_GET_ITEM(args, i);
                Py_INCREF(item);
                PyTuple_SET_ITEM(newargs, i + 2, item);
            }

            callable = PyObject_Call(st->partial, newargs, kwargs);
            Py_DECREF(newargs);
            if (callable == NULL)
                return -1;

            newargs = PyTuple_New(0);
            if (newargs == NULL) {
                Py_DECREF(callable);
                return -1;
            }

            if (save(self, callable, 0) < 0 ||
                save(self, newargs, 0) < 0 ||
                _Pickler_Write(self, &reduce_op, 1) < 0) {
                Py_DECREF(newargs);
                Py_DECREF(callable);
                return -1;
            }
            Py_DECREF(newargs);
            Py_DECREF(callable);
        }
    }
    else if (use_newobj) {
        PyObject *cls;
        PyObject *newargtup;
        PyObject *obj_class;
        int p;

        /* Sanity checks. */
        if (PyTuple_GET_SIZE(argtup) < 1) {
            PyErr_SetString(st->PicklingError, "__newobj__ arglist is empty");
            return -1;
        }

        cls = PyTuple_GET_ITEM(argtup, 0);
        if (!PyType_Check(cls)) {
            PyErr_SetString(st->PicklingError, "args[0] from "
                            "__newobj__ args is not a type");
            return -1;
        }

        if (obj != NULL) {
            obj_class = get_class(obj);
            p = obj_class != cls;    /* true iff a problem */
            Py_DECREF(obj_class);
            if (p) {
                PyErr_SetString(st->PicklingError, "args[0] from "
                                "__newobj__ args has the wrong class");
                return -1;
            }
        }
        /* XXX: These calls save() are prone to infinite recursion. Imagine
           what happen if the value returned by the __reduce__() method of
           some extension type contains another object of the same type. Ouch!

           Here is a quick example, that I ran into, to illustrate what I
           mean:

             >>> import pickle, copyreg
             >>> copyreg.dispatch_table.pop(complex)
             >>> pickle.dumps(1+2j)
             Traceback (most recent call last):
               ...
             RecursionError: maximum recursion depth exceeded

           Removing the complex class from copyreg.dispatch_table made the
           __reduce_ex__() method emit another complex object:

             >>> (1+1j).__reduce_ex__(2)
             (<function __newobj__ at 0xb7b71c3c>,
               (<class 'complex'>, (1+1j)), None, None, None)

           Thus when save() was called on newargstup (the 2nd item) recursion
           ensued. Of course, the bug was in the complex class which had a
           broken __getnewargs__() that emitted another complex object. But,
           the point, here, is it is quite easy to end up with a broken reduce
           function. */

        /* Save the class and its __new__ arguments. */
        if (save(self, cls, 0) < 0)
            return -1;

        newargtup = PyTuple_GetSlice(argtup, 1, PyTuple_GET_SIZE(argtup));
        if (newargtup == NULL)
            return -1;

        p = save(self, newargtup, 0);
        Py_DECREF(newargtup);
        if (p < 0)
            return -1;

        /* Add NEWOBJ opcode. */
        if (_Pickler_Write(self, &newobj_op, 1) < 0)
            return -1;
    }
    else { /* Not using NEWOBJ. */
        if (save(self, callable, 0) < 0 ||
            save(self, argtup, 0) < 0 ||
            _Pickler_Write(self, &reduce_op, 1) < 0)
            return -1;
    }

    /* obj can be NULL when save_reduce() is used directly. A NULL obj means
       the caller do not want to memoize the object. Not particularly useful,
       but that is to mimic the behavior save_reduce() in pickle.py when
       obj is None. */
    if (obj != NULL) {
        /* If the object is already in the memo, this means it is
           recursive. In this case, throw away everything we put on the
           stack, and fetch the object back from the memo. */
        if (PyMemoTable_Get(self->memo, obj)) {
            const char pop_op = POP;

            if (_Pickler_Write(self, &pop_op, 1) < 0)
                return -1;
            if (memo_get(self, obj) < 0)
                return -1;

            return 0;
        }
        else if (memo_put(self, obj) < 0)
            return -1;
    }

    if (listitems && batch_list(self, listitems) < 0)
        return -1;

    if (dictitems && batch_dict(self, dictitems) < 0)
        return -1;

    if (state) {
        if (save(self, state, 0) < 0 ||
            _Pickler_Write(self, &build_op, 1) < 0)
            return -1;
    }

    return 0;
}