feat(napi): error handling — missing member, protected write, type errors

- 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) <noreply@anthropic.com>
This commit is contained in:
CharlesKWON
2026-06-16 09:56:07 +09:00
parent 6a8ada16b2
commit 5f019e76cb
2 changed files with 115 additions and 17 deletions

59
capi/napi_errtest.prg Normal file
View File

@@ -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 )

View File

@@ -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))
}
}