Interpreting strings with C-style escapes
Use &lt;cctype&gt; for character classification

Instead of defining is_hex() (and depending on abcdef and ABCDEF being contiguously coded), better to use the standard isxdigit().

get_nibble makes a similar assumption - it's arguably safer to use a switch for this.

On the whole, the logic looks good.  I'm not a fan of names beginning with _ - even if you do fully understand the rules about reserved identifiers, many of your readers will not!

Some expressions can be simplified with post-increment.  Consider this fragment:

if (!_err)
    tmp[_len] = esc, ++_len;
} else {
    tmp[_len] = *in; ++in, ++_len;
}


That could be

if (!_err)
    tmp[_len++] = esc;
} else {
    tmp[_len++] = *in++;
}


That's fairly idiomatic usage, and should be familiar to anyone who's looked at strcpy or the like.  In fact, slightly more idiomatic would be to use a pointer, rather than indexing:

char *post_string(char *const in, size_t *len, const char **err) {
    if (!in || !len)
        return NULL;

    char *out = in, *p = in;
    const char *int_err = NULL;

    while (*p &amp;&amp; !int_err) {
        if (*p != '\\') {
            /* normal case */
            *out++ = *p++;
        } else {
            /* escape sequence */
            switch (*++p) {
            case 'a': *out++ = '\a'; ++p; break;
            case 'b': *out++ = '\b'; ++p; break;
            case 'f': *out++ = '\f'; ++p; break;
            case 'n': *out++ = '\n'; ++p; break;
            case 'r': *out++ = '\r'; ++p; break;
            case 't': *out++ = '\t'; ++p; break;
            case 'v': *out++ = '\v'; ++p; break;

            case '"': case '\'': case '\\': *out++ = *p++; case '?': break;

            case 'x': case 'X':
                if (!isxdigit(p[1]) || !isxdigit(p[2])) {
                    int_err = "Invalid character on hexadecimal escape.";
                } else {
                    *out++ = (char)(get_nibble(p[1]) * 0x10 + get_nibble(p[2]));
                    p += 3;
                }
                break;

            default:
                int_err = "Unexpected '\\' with no escape sequence."; break;
            }
        }
    }

    *out = '\0';                /* Set the end of string. */
    *len = p - in;              /* Set the final string length. */
    if (int_err &amp;&amp; err)         /* Set error message back. */
        *err = int_err;
    return in;
}


Testing

Some of the code appears completely untested (and I've fixed above).  In particular, passing NULL as the err parameter - as in the sample main() - will provoke undefined behaviour if your string is invalid).



Modified code

I've also added code to accept 1-3 octal digits, and modified the hexadecimal reader to accept 1+ digits, and included tests of each error branch.

#include &lt;ctype.h&gt;
#include &lt;limits.h&gt;
#include &lt;stddef.h&gt;

static unsigned int get_nibble(char c)
{
    switch(c) {
        case '0': return 0;
        case '1': return 1;
        case '2': return 2;
        case '3': return 3;
        case '4': return 4;
        case '5': return 5;
        case '6': return 6;
        case '7': return 7;
        case '8': return 8;
        case '9': return 9;
        case 'A': case 'a': return 0xa;
        case 'B': case 'b': return 0xb;
        case 'C': case 'c': return 0xc;
        case 'D': case 'd': return 0xd;
        case 'E': case 'e': return 0xe;
        case 'F': case 'f': return 0xf;
    }
    /* shouldn't happen */
    return 0;
}

char *post_string(char *const in, size_t *len, const char **err) {
    if (!in || !len)
        return NULL;

    char *out = in, *p = in;
    const char *int_err = NULL;

    while (*p &amp;&amp; !int_err) {
        if (*p != '\\') {
            /* normal case */
            *out++ = *p++;
        } else {
            /* escape sequence */
            switch (*++p) {
            case 'a': *out++ = '\a'; ++p; break;
            case 'b': *out++ = '\b'; ++p; break;
            case 'f': *out++ = '\f'; ++p; break;
            case 'n': *out++ = '\n'; ++p; break;
            case 'r': *out++ = '\r'; ++p; break;
            case 't': *out++ = '\t'; ++p; break;
            case 'v': *out++ = '\v'; ++p; break;

            case '"': case '\'': case '\\': case '?': *out++ = *p++; break;

            case 'x': case 'X':
                if (!isxdigit(*++p)) {
                    int_err = "Invalid character on hexadecimal escape."; break;
                } else {
                    unsigned int x = 0;  /* assuming int is larger than char */
                    while (isxdigit(*p)) {
                        x *= 0x10;
                        x += get_nibble(*p++);
                        if (x &gt; UCHAR_MAX) {
                            int_err = "Out of range hexadecimal escape."; break;
                        }
                    }
                    *out++ = (char)x; break;
                }

            default:
                if (isdigit(*p) &amp;&amp; *p &lt; '8') {
                    /* octal escape */
                    unsigned int x = 0;  /* assuming int is larger than char */
                    for (int i = 0;  i &lt; 3 &amp;&amp; isdigit(*p) &amp;&amp; *p &lt; '8';  ++i) {
                        x = x * 010 + (unsigned)*p++ - '0';
                    }
                    if (x &gt; UCHAR_MAX) {
                        int_err = "Out of range octal escape."; break;
                    }
                    *out++ = (char)x; break;
                } else {
                    int_err = "Unexpected '\\' with no escape sequence."; break;
                }
            }
        }
    }

    *out = '\0';                /* Set the end of string. */
    *len = (size_t)(p - in);    /* Set the final string length. */
    if (int_err &amp;&amp; err)         /* Set error message back. */
        *err = int_err;
    return in;
}


#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;

int main(void) {
  char t[256] = "This is a \\\"string\\\" with \\x43 \\X063 hex\\"
      "nnewline\\ttab\\t\\?\\'\\\"\\\\octal\\132\\538 \\0null";
  size_t len = 0;
  printf("Before: %s\n", t);
  post_string(t, &amp;len, NULL);
  printf("After : %s\n\n", t);

  const char *err = NULL;
  strcpy(t, "This has a hex error: \\xg");
  printf("Before: %s\n", t);
  post_string(t, &amp;len, &amp;err);
  printf("Error: %s\n\n", err);

  strcpy(t, "Hex beyond ASCII: \\xfff");
  printf("Before: %s\n", t);
  post_string(t, &amp;len, &amp;err);
  printf("Error: %s\n\n", err);

  strcpy(t, "Octal beyond ASCII: \\777");
  printf("Before: %s\n", t);
  post_string(t, &amp;len, &amp;err);
  printf("Error: %s\n\n", err);

  strcpy(t, "not an escape: \\9");
  printf("Before: %s\n", t);
  post_string(t, &amp;len, &amp;err);
  printf("Error: %s\n\n", err);

  strcpy(t, "backslash at end: \\");
  printf("Before: %s\n", t);
  post_string(t, &amp;len, &amp;err);
  printf("Error: %s\n\n", err);
}