From 5f019e76cb2eed366810b85c5939d89e88d575dc Mon Sep 17 00:00:00 2001 From: CharlesKWON Date: Tue, 16 Jun 2026 09:56:07 +0900 Subject: [PATCH] =?UTF-8?q?feat(napi):=20error=20handling=20=E2=80=94=20mi?= =?UTF-8?q?ssing=20member,=20protected=20write,=20type=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - raiseIfErr traps the JS error sentinel (\x01FNERR:) and raises a catchable PRG runtime error instead of handing back undefined/{error} as data. - __GET__/__SET__ methods: explicit data read/write with existence + writability (getter-only/non-writable/non-extensible) checks. - per-VM lastErr (FN_LASTERROR) so parallel requests don't cross messages. - capi/napi_errtest.prg exercises the 4 cases via BEGIN SEQUENCE/RECOVER USING. Co-Authored-By: Claude Opus 4.8 (1M context) --- capi/napi_errtest.prg | 59 ++++++++++++++++++++++++ hbrtl_ext/napibridge/napibridge.go | 73 +++++++++++++++++++++++------- 2 files changed, 115 insertions(+), 17 deletions(-) create mode 100644 capi/napi_errtest.prg diff --git a/capi/napi_errtest.prg b/capi/napi_errtest.prg new file mode 100644 index 0000000..8d1cc1a --- /dev/null +++ b/capi/napi_errtest.prg @@ -0,0 +1,59 @@ +FUNCTION FN_HANDLE() + LOCAL oM := FN_REQUIRE( "/Users/charleskwon/fivenode/fivenode/fivenode/napi/test/errmod" ) + LOCAL aRes := {} + LOCAL oErr + IF ! HB_ISOBJECT( oM ) + RETURN hb_jsonEncode( { "err" => FN_LASTERROR() } ) + ENDIF + + // 1) 없는 method (미등록 → VM 오류, 이제 오류 객체로 잡힘) + BEGIN SEQUENCE + oM:nosuchmethod() + RECOVER USING oErr + AADD( aRes, "1 missing_method: " + ErrInfo( oErr ) ) + END SEQUENCE + + // 2) 없는 data 읽기 + BEGIN SEQUENCE + oM:__get__( "nope" ) + RECOVER USING oErr + AADD( aRes, "2 missing_read: " + ErrInfo( oErr ) ) + END SEQUENCE + + // 3) protected(getter-only) 쓰기 + BEGIN SEQUENCE + oM:__set__( "readonly", 99 ) + RECOVER USING oErr + AADD( aRes, "3 protected_write: " + ErrInfo( oErr ) ) + END SEQUENCE + + // 3b) 없는 data 쓰기 + BEGIN SEQUENCE + oM:__set__( "nope", 1 ) + RECOVER USING oErr + AADD( aRes, "3b missing_write: " + ErrInfo( oErr ) ) + END SEQUENCE + + // 4) type 틀림 / throw (boom 은 등록된 메서드 → 브리지 경유) + BEGIN SEQUENCE + oM:boom() + RECOVER USING oErr + AADD( aRes, "4 type_error: " + ErrInfo( oErr ) ) + END SEQUENCE + + // 정상 read/write + AADD( aRes, "ok_read: " + hb_CStr( oM:__get__( "name" ) ) ) + oM:__set__( "name", "changed" ) + AADD( aRes, "ok_write: " + hb_CStr( oM:__get__( "name" ) ) ) + + RETURN hb_jsonEncode( { ; + "status" => 200, ; + "headers" => { "Content-Type" => "application/json" }, ; + "body" => hb_jsonEncode( aRes ) ; + } ) + +STATIC FUNCTION ErrInfo( oErr ) + IF HB_ISOBJECT( oErr ) + RETURN oErr:Description + " [op=" + hb_CStr( oErr:Operation ) + " gc=" + hb_ntos( oErr:GenCode ) + "]" + ENDIF + RETURN "NON-OBJECT: " + hb_CStr( oErr ) diff --git a/hbrtl_ext/napibridge/napibridge.go b/hbrtl_ext/napibridge/napibridge.go index 129d123..48f76df 100644 --- a/hbrtl_ext/napibridge/napibridge.go +++ b/hbrtl_ext/napibridge/napibridge.go @@ -67,7 +67,24 @@ func init() { // 가로챌 수 없다(파서가 AwaitExpr 로 잡고 gengo 백엔드는 코드 미생성→NIL). // 그래서 npm Promise 해소는 FN_AWAIT 로 노출한다. hbrt.HB_FUNC("FN_AWAIT", fnAwait) - hbrt.HB_FUNC("FN_LASTERROR", func(ctx *hbrt.HBContext) { ctx.RetC(lastErr) }) + hbrt.HB_FUNC("FN_LASTERROR", func(ctx *hbrt.HBContext) { ctx.RetC(getLastErr(ctx.T.VM())) }) +} + +// errSentinel — JS 브리지가 오류를 데이터와 구분해 돌려보내는 제어 접두사. +// SVG('<')·JSON('{'/'[')·handle('handle:')·일반 문자열과 절대 충돌하지 않도록 +// 제어문자(0x01)로 시작한다. resultToValue 가 아니라 raiseIfErr 가 가로채 +// PRG 런타임 오류로 올린다. +const errSentinel = "\x01FNERR:" + +// raiseIfErr — npm 호출 결과가 오류 센티넬이면 lastErr 를 기록하고 잡을 수 있는 +// 런타임 오류를 올린다(없는 메서드/프로퍼티, protected 쓰기, 타입 불일치 등). +// PRG 는 BEGIN SEQUENCE / RECOVER 로 잡거나, RECOVER 후 FN_LASTERROR() 로 확인. +func raiseIfErr(t *hbrt.Thread, s string) { + if strings.HasPrefix(s, errSentinel) { + msg := s[len(errSentinel):] + setLastErr(t.VM(), msg) + t.RaiseError("npm: "+msg, "FN_NPM", 1004) // 1004 ~ EG_NOMETHOD 계열 + } } // FN_AWAIT( oPromise ) — async npm 메서드가 돌려준 Promise 핸들 객체를 해소한다. @@ -91,43 +108,58 @@ func fnAwait(ctx *hbrt.HBContext) { h = int(a.Items[0].AsNumInt()) } if h < 0 { - lastErr = "AWAIT: not a promise handle" + setLastErr(ctx.T.VM(), "AWAIT: not a promise handle") ctx.RetNil() return } - ctx.RetVal(resultToValue(ctx.T.VM(), cbAwait(h))) + res := cbAwait(h) + raiseIfErr(ctx.T, res) // rejected promise / async 오류를 잡을 수 있게 올림 + ctx.RetVal(resultToValue(ctx.T.VM(), res)) } var ( - lastErr string clsMu sync.Mutex clsBySig = map[string]uint16{} - // acquired — VM(=요청)별로 발급된 npm 핸들. PRG 가 oH:__end__() 을 안 불러도 - // 요청이 끝나면 ReleaseAll(vm) 이 그 요청 핸들만 정리한다(누수 방지). 병렬 - // 요청은 각자 다른 VM 이라 요청끼리 핸들이 섞이지 않는다(VM 풀 모델). - acqMu sync.Mutex - acquired = map[*hbrt.VM][]int{} + // acquired / lastErrByVM — VM(=요청)별 상태. 병렬 요청은 각자 다른 VM 이라 + // 핸들·마지막오류가 요청끼리 섞이지 않는다(전역이면 race). PRG 가 + // oH:__end__() 을 안 불러도 요청 종료 시 ReleaseAll(vm) 이 그 요청 것만 정리. + stMu sync.Mutex + acquired = map[*hbrt.VM][]int{} + lastErrByVM = map[*hbrt.VM]string{} ) // StoreCallbacks — 애드온이 tfn_register_callbacks 로 넘긴 콜백 묶음을 보관. func StoreCallbacks(cb unsafe.Pointer) { C.nb_store(cb) } +func setLastErr(vm *hbrt.VM, s string) { + stMu.Lock() + lastErrByVM[vm] = s + stMu.Unlock() +} + +func getLastErr(vm *hbrt.VM) string { + stMu.Lock() + defer stMu.Unlock() + return lastErrByVM[vm] +} + // track — 발급된 핸들을 해당 VM(요청)의 정리 대상에 등록. func track(vm *hbrt.VM, h int) { - acqMu.Lock() + stMu.Lock() acquired[vm] = append(acquired[vm], h) - acqMu.Unlock() + stMu.Unlock() } // ReleaseAll — 요청 종료 시 호출(shim 의 defer). 그 VM 에서 발급된 핸들에만 -// cbEnd 를 보낸다. JS end 액션은 delete(이미 __end__/await 로 지워졌으면 no-op) -// 라 수동/중복 호출과 충돌 없음. +// cbEnd 를 보내고, 그 VM 의 마지막오류도 비운다. JS end 액션은 delete(이미 +// __end__/await 로 지워졌으면 no-op)라 수동/중복 호출과 충돌 없음. func ReleaseAll(vm *hbrt.VM) { - acqMu.Lock() + stMu.Lock() hs := acquired[vm] delete(acquired, vm) - acqMu.Unlock() + delete(lastErrByVM, vm) + stMu.Unlock() for _, h := range hs { cbEnd(h) } @@ -178,13 +210,13 @@ func fnRequire(ctx *hbrt.HBContext) { return } if C.nb_has() == 0 { - lastErr = "npm callbacks not registered (addon not loaded?)" + setLastErr(ctx.T.VM(), "npm callbacks not registered (addon not loaded?)") ctx.RetL(false) return } h := cbRequire(ctx.ParC(1)) if h < 0 { - lastErr = "require failed: " + ctx.ParC(1) + setLastErr(ctx.T.VM(), "require failed: "+ctx.ParC(1)) ctx.RetL(false) return } @@ -216,6 +248,12 @@ func classForSig(names []string) uint16 { for _, n := range names { def.AddMethod(n, makeDispatch(n)) } + // 명시적 프로퍼티 접근(데이터 read/write) — JS call 핸들러가 가로채 + // 존재/쓰기가능 여부를 검사하고 오류면 센티넬을 돌려준다. + // xVal := oObj:__get__("prop") 없으면 오류 + // oObj:__set__("prop", xVal) 없거나 read-only/getter면 오류 + def.AddMethod("__GET__", makeDispatch("__GET__")) + def.AddMethod("__SET__", makeDispatch("__SET__")) def.AddMethod("__END__", makeEnd()) id := def.Register() clsBySig[sig] = id @@ -234,6 +272,7 @@ func makeDispatch(jsName string) hbrt.MethodFunc { } argsJSON, _ := json.Marshal(args) res := cbCall(h, jsName, string(argsJSON)) + raiseIfErr(t, res) // 없는 메서드/프로퍼티·protected 쓰기·타입오류를 올림 t.RetVal(resultToValue(t.VM(), res)) } }