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:
59
capi/napi_errtest.prg
Normal file
59
capi/napi_errtest.prg
Normal 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 )
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user