save(PicklerObject *self, PyObject *obj, int pers_save)
{
    PyTypeObject *type;
    PyObject *reduce_func = NULL;
    PyObject *reduce_value = NULL;
    int status = 0;

    if (_Pickler_OpcodeBoundary(self) < 0)
        return -1;

    /* The extra pers_save argument is necessary to avoid calling save_pers()
       on its returned object. */
    if (!pers_save && self->pers_func) {
        /* save_pers() returns:
            -1   to signal an error;
             0   if it did nothing successfully;
             1   if a persistent id was saved.
         */
        if ((status = save_pers(self, obj)) != 0)
            return status;
    }

    type = Py_TYPE(obj);

    /* The old cPickle had an optimization that used switch-case statement
       dispatching on the first letter of the type name.  This has was removed
       since benchmarks shown that this optimization was actually slowing
       things down. */

    /* Atom types; these aren't memoized, so don't check the memo. */

    if (obj == Py_None) {
        return save_none(self, obj);
    }
    else if (obj == Py_False || obj == Py_True) {
        return save_bool(self, obj);
    }
    else if (type == &PyLong_Type) {
        return save_long(self, obj);
    }
    else if (type == &PyFloat_Type) {
        return save_float(self, obj);
    }

    /* Check the memo to see if it has the object. If so, generate
       a GET (or BINGET) opcode, instead of pickling the object
       once again. */
    if (PyMemoTable_Get(self->memo, obj)) {
        return memo_get(self, obj);
    }

    if (type == &PyBytes_Type) {
        return save_bytes(self, obj);
    }
    else if (type == &PyUnicode_Type) {
        return save_unicode(self, obj);
    }

    /* We're only calling Py_EnterRecursiveCall here so that atomic
       types above are pickled faster. */
    if (Py_EnterRecursiveCall(" while pickling an object")) {
        return -1;
    }

    if (type == &PyDict_Type) {
        status = save_dict(self, obj);
        goto done;
    }
    else if (type == &PySet_Type) {
        status = save_set(self, obj);
        goto done;
    }
    else if (type == &PyFrozenSet_Type) {
        status = save_frozenset(self, obj);
        goto done;
    }
    else if (type == &PyList_Type) {
        status = save_list(self, obj);
        goto done;
    }
    else if (type == &PyTuple_Type) {
        status = save_tuple(self, obj);
        goto done;
    }
    else if (type == &PyType_Type) {
        status = save_type(self, obj);
        goto done;
    }
    else if (type == &PyFunction_Type) {
        status = save_global(self, obj, NULL);
        goto done;
    }

    /* XXX: This part needs some unit tests. */

    /* Get a reduction callable, and call it.  This may come from
     * self.dispatch_table, copyreg.dispatch_table, the object's
     * __reduce_ex__ method, or the object's __reduce__ method.
     */
    if (self->dispatch_table == NULL) {
        PickleState *st = _Pickle_GetGlobalState();
        reduce_func = PyDict_GetItemWithError(st->dispatch_table,
                                              (PyObject *)type);
        if (reduce_func == NULL) {
            if (PyErr_Occurred()) {
                goto error;
            }
        } else {
            /* PyDict_GetItemWithError() returns a borrowed reference.
               Increase the reference count to be consistent with
               PyObject_GetItem and _PyObject_GetAttrId used below. */
            Py_INCREF(reduce_func);
        }
    } else {
        reduce_func = PyObject_GetItem(self->dispatch_table,
                                       (PyObject *)type);
        if (reduce_func == NULL) {
            if (PyErr_ExceptionMatches(PyExc_KeyError))
                PyErr_Clear();
            else
                goto error;
        }
    }
    if (reduce_func != NULL) {
        Py_INCREF(obj);
        reduce_value = _Pickle_FastCall(reduce_func, obj);
    }
    else if (PyType_IsSubtype(type, &PyType_Type)) {
        status = save_global(self, obj, NULL);
        goto done;
    }
    else {
        _Py_IDENTIFIER(__reduce__);
        _Py_IDENTIFIER(__reduce_ex__);


        /* XXX: If the __reduce__ method is defined, __reduce_ex__ is
           automatically defined as __reduce__. While this is convenient, this
           make it impossible to know which method was actually called. Of
           course, this is not a big deal. But still, it would be nice to let
           the user know which method was called when something go
           wrong. Incidentally, this means if __reduce_ex__ is not defined, we
           don't actually have to check for a __reduce__ method. */

        /* Check for a __reduce_ex__ method. */
        if (_PyObject_LookupAttrId(obj, &PyId___reduce_ex__, &reduce_func) < 0) {
            goto error;
        }
        if (reduce_func != NULL) {
            PyObject *proto;
            proto = PyLong_FromLong(self->proto);
            if (proto != NULL) {
                reduce_value = _Pickle_FastCall(reduce_func, proto);
            }
        }
        else {
            PickleState *st = _Pickle_GetGlobalState();

            /* Check for a __reduce__ method. */
            reduce_func = _PyObject_GetAttrId(obj, &PyId___reduce__);
            if (reduce_func != NULL) {
                reduce_value = _PyObject_CallNoArg(reduce_func);
            }
            else {
                PyErr_Format(st->PicklingError,
                             "can't pickle '%.200s' object: %R",
                             type->tp_name, obj);
                goto error;
            }
        }
    }

    if (reduce_value == NULL)
        goto error;

    if (PyUnicode_Check(reduce_value)) {
        status = save_global(self, obj, reduce_value);
        goto done;
    }

    if (!PyTuple_Check(reduce_value)) {
        PickleState *st = _Pickle_GetGlobalState();
        PyErr_SetString(st->PicklingError,
                        "__reduce__ must return a string or tuple");
        goto error;
    }

    status = save_reduce(self, reduce_value, obj);

    if (0) {
  error:
        status = -1;
    }
  done:

    Py_LeaveRecursiveCall();
    Py_XDECREF(reduce_func);
    Py_XDECREF(reduce_value);

    return status;
}