오픈 소스 첫 기여 (ztsd-jni)

장성호·2025년 7월 31일

[Opensource]

목록 보기
1/1

2줄만 수정해서 날로 먹기

회사 일을 하던 도중 오류를 마주했는데, 에러가 터졌는데 에러가 없다는 신기한 오류 메세지를 마주쳤다. 그래서 매번 디버거를 키고 에러 코드를 확인하고 JNI 호출부를 확인해야 했는데, Stream이 클 때는 디버거 다음 버튼을 너무 많이 눌러야돼서 꽤나 불편했다.

com.github.luben.zstd.ZstdException: No error detected

코드를 살펴보니 ZstdCompressCtxZstdDecompressCtx 에서 아래와 같은 에러 처리가 문제였다.

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줄 수정한거 뿐이지만.. 그래도 다음 기여부터는 좀 더 가벼운 마음으로 시작할 수 있을 것 같다.

profile
일벌리기 좋아하는 사람

1개의 댓글

comment-user-thumbnail
2026년 4월 12일

멋지십니다.

답글 달기