회사 일을 하던 도중 오류를 마주했는데, 에러가 터졌는데 에러가 없다는 신기한 오류 메세지를 마주쳤다. 그래서 매번 디버거를 키고 에러 코드를 확인하고 JNI 호출부를 확인해야 했는데, Stream이 클 때는 디버거 다음 버튼을 너무 많이 눌러야돼서 꽤나 불편했다.
com.github.luben.zstd.ZstdException: No error detected
코드를 살펴보니 ZstdCompressCtx와 ZstdDecompressCtx 에서 아래와 같은 에러 처리가 문제였다.
long result = compressDirectByteBufferStream0(nativePtr, dst, dst.position(), dst.limit(), src, src.position(), src.limit(), endOp.value());
if ((result & 0x80000000L) != 0) {
long code = result & 0xFF;
throw new ZstdException(code, Zstd.getErrorName(code));
}
0xFF 와 & 연산으로 하위 8비트를 뜯어내는 방식인데, ZstdErrorCode 가 현재 0~120 으로 구성되어 있어서 그렇다.
typedef enum {
ZSTD_error_no_error = 0,
ZSTD_error_GENERIC = 1,
ZSTD_error_prefix_unknown = 10,
ZSTD_error_version_unsupported = 12,
ZSTD_error_frameParameter_unsupported = 14,
ZSTD_error_frameParameter_windowTooLarge = 16,
ZSTD_error_corruption_detected = 20,
ZSTD_error_checksum_wrong = 22,
ZSTD_error_literals_headerWrong = 24,
ZSTD_error_dictionary_corrupted = 30,
ZSTD_error_dictionary_wrong = 32,
ZSTD_error_dictionaryCreation_failed = 34,
ZSTD_error_parameter_unsupported = 40,
ZSTD_error_parameter_combination_unsupported = 41,
ZSTD_error_parameter_outOfBound = 42,
ZSTD_error_tableLog_tooLarge = 44,
ZSTD_error_maxSymbolValue_tooLarge = 46,
ZSTD_error_maxSymbolValue_tooSmall = 48,
ZSTD_error_cannotProduce_uncompressedBlock = 49,
ZSTD_error_stabilityCondition_notRespected = 50,
ZSTD_error_stage_wrong = 60,
ZSTD_error_init_missing = 62,
ZSTD_error_memory_allocation = 64,
ZSTD_error_workSpace_tooSmall= 66,
ZSTD_error_dstSize_tooSmall = 70,
ZSTD_error_srcSize_wrong = 72,
ZSTD_error_dstBuffer_null = 74,
ZSTD_error_noForwardProgress_destFull = 80,
ZSTD_error_noForwardProgress_inputEmpty = 82,
/* following error codes are __NOT STABLE__, they can be removed or changed in future versions */
ZSTD_error_frameIndex_tooLarge = 100,
ZSTD_error_seekableIO = 102,
ZSTD_error_dstBuffer_wrong = 104,
ZSTD_error_srcBuffer_wrong = 105,
ZSTD_error_sequenceProducer_failed = 106,
ZSTD_error_externalSequences_invalid = 107,
ZSTD_error_maxCode = 120 /* never EVER use this value directly, it can change in future versions! Use ZSTD_isError() instead */
} ZSTD_ErrorCode;
근데 에러 코드를 실제 던지는 함수를 보면 아래처럼 구현되어 있다. 에러 코드를 음수로 던진다. ERR_getErrorCode 함수에서 0-code로 구현되어 있어, 음수로 에러코드를 던지면 양수로 전환되어 에러코드를 인식하는 방식이다.
/*
* Class: com_github_luben_zstd_ZstdDecompressCtx
* Method: decompressDirectByteBufferStream0
* Signature: (JLjava/nio/ByteBuffer;IILjava/nio/ByteBuffer;II)J
*/
JNIEXPORT jlong JNICALL Java_com_github_luben_zstd_ZstdDecompressCtx_decompressDirectByteBufferStream0
(JNIEnv *env, jclass jclazz, jlong ptr, jobject dst, jint dst_offset, jint dst_size, jobject src, jint src_offset, jint src_size)
{
size_t result = decompress_direct_buffer_stream(env, ptr, dst, &dst_offset, dst_size, src, &src_offset, src_size);
if (ZSTD_isError(result)) {
return (1ULL << 31) | ZSTD_getErrorCode(result);
}
jlong encoded_result = ((jlong)dst_offset << 32) | src_offset;
if (result == 0) {
encoded_result |= 1ULL << 63;
}
return encoded_result;
}
static size_t decompress_direct_buffer_stream
(JNIEnv *env, jlong ptr, jobject dst, jint *dst_offset, jint dst_size, jobject src, jint *src_offset, jint src_size)
{
if (NULL == dst) return -ZSTD_error_dstSize_tooSmall;
if (NULL == src) return -ZSTD_error_srcSize_wrong;
if (0 > *dst_offset) return -ZSTD_error_dstSize_tooSmall;
if (0 > *src_offset) return -ZSTD_error_srcSize_wrong;
if (0 > dst_size) return -ZSTD_error_dstSize_tooSmall;
if (0 > src_size) return -ZSTD_error_srcSize_wrong;
jsize dst_cap = (*env)->GetDirectBufferCapacity(env, dst);
if (dst_size > dst_cap) return -ZSTD_error_dstSize_tooSmall;
jsize src_cap = (*env)->GetDirectBufferCapacity(env, src);
if (src_size > src_cap) return -ZSTD_error_srcSize_wrong;
ZSTD_DCtx* dctx = (ZSTD_DCtx*)(intptr_t)ptr;
ZSTD_outBuffer out;
out.pos = *dst_offset;
out.size = dst_size;
out.dst = (*env)->GetDirectBufferAddress(env, dst);
if (out.dst == NULL) return -ZSTD_error_memory_allocation;
ZSTD_inBuffer in;
in.pos = *src_offset;
in.size = src_size;
in.src = (*env)->GetDirectBufferAddress(env, src);
if (in.src == NULL) return -ZSTD_error_memory_allocation;
size_t result = ZSTD_decompressStream(dctx, &out, &in);
*dst_offset = out.pos;
*src_offset = in.pos;
return result;
}
/*! ZSTD_getError() :
* convert a `size_t` function result into a proper ZSTD_errorCode enum */
ZSTD_ErrorCode ZSTD_getErrorCode(size_t code) { return ERR_getErrorCode(code); }
ERR_STATIC ERR_enum ERR_getErrorCode(size_t code) { if (!ERR_isError(code)) return (ERR_enum)0; return (ERR_enum) (0-code); }
하지만 0xFF 와 & 연산은 하위 8비트를 양수로만 인식하기 때문에, Java에서는 구조적으로 음수를 던질 수 없는 상태이다. 처음에는 Java_com_github_luben_zstd_ZstdDecompressCtx_decompressDirectByteBufferStream0 함수나 long code = result & 0xFF; 부분에서 상위 1비트를 사용해 부호를 나타낼까 했다. 그러다가 stream0 native 함수의 주석을 보니, 상위 1비트는 stream의 EOF를 나타내기 위한 비트로 사용 중이라는 것을 알았다.
/**
* 4 pieces of information are packed into the return value of this method, which must be
* treated as an unsigned long. The highest bit is set if all data has been flushed from
* internal buffers. The next 31 bits are the new position of the destination buffer. The next
* bit is set if an error occurred. If an error occurred, the lowest 31 bits encode a zstd error
* code. Otherwise, the lowest 31 bits are the new position of the source buffer.
*/
private static native long decompressDirectByteBufferStream0(long nativePtr, ByteBuffer dst, int dstOffset, int dstSize, ByteBuffer src, int srcOffset, int srcSize);
그래서 정말 단순하게 음수 부호만 넣어줬다..ㅋㅋ 단순명료한게 이해하기 가장 좋을 것 같았다. 해당 PR은 merge가 된 상태이다. 패치된게 언제쯤 maven에 올라오려나...
// before
long code = result & 0xFF;
// after
long code = -(result & 0xFF);
그동안 오픈소스 기여를 안해봐서, 오픈소스 기여는 시도하기도 굉장히 어렵다고 생각한 적이 많았다. 환경 셋팅부터 다 해야하니까... 근데 라이브러리 사용하다가 불편한게 있어서 고치고 싶다는 마음으로 시작하니까, 가벼운 마음으로 금방 시작할 수 있었다. 불편하면 자세를 고쳐 어쩌구 저쩌구 가이드 문서들도 잘 되어있어서, 처음해보는 scala 테스트 코드 환경 셋팅까지 금방할 수 있었다. 정말 간단한거 2줄 수정한거 뿐이지만.. 그래도 다음 기여부터는 좀 더 가벼운 마음으로 시작할 수 있을 것 같다.
멋지십니다.