char *destination needs a size
Any new string function can be made far more robust by passing in the size of the destination array. The minor performance saving by not having to do size limitations is far outweighed by the grief cause by message overruns.
Use size_t for all string sizes and indexes. Type int may be of insufficient range especially for this function whose goal is to make sub-strings of larger strings.
Avoid an argument name like string. Use a name that describes its role.
// size_t substring(char *destination, char const *string, int start, int len) {
size_t substring(char *destination, size_t dsize, char const *source,
size_t start, size_t len) {
Investigate restrict. Your present code has trouble if destination and string overlap. Either adjust code to handle that or use restrict to indicate your function can not cope with overlap.
Define corner cases
Instead of returning (size_t)-1 , on "start and len must be 0 < start/len < length of string", define a plausible functionality.
Along with using size_t dsize, there should be no error conditions possible. All possible legitimate inputs should result in a defined result.
// dest and source may overlap
void substring1(char *dest, size_t dsize, const char *source, size_t start, size_t length) {
size_t source_len = strlen(source);
if (start > source_len) start = source_len;
if (start + length*1ull > source_len) length = source_len - start;
if (length + 1 > dsize) {
if (dsize == 0) {
return;
}
length = dsize - 1;
}
memmove(dest, &source[start], length);
dest[length] = 0;
}