From 7a4ddfa9691b3b3ff5e50694082744d1aff14182 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B8iman?= Date: Thu, 28 Nov 2024 17:39:12 +0100 Subject: [PATCH 01/23] Skeleton code to create an inspector for a client Currently there is no actual usable client, and no handling of diposal yet. --- inspector.cc | 30 ++++++++++++++++++++++++++++++ inspector.go | 32 ++++++++++++++++++++++++++++++++ inspector.h | 43 +++++++++++++++++++++++++++++++++++++++++++ inspector_test.go | 17 +++++++++++++++++ 4 files changed, 122 insertions(+) create mode 100644 inspector.cc create mode 100644 inspector.go create mode 100644 inspector.h create mode 100644 inspector_test.go diff --git a/inspector.cc b/inspector.cc new file mode 100644 index 00000000..8e92d1e4 --- /dev/null +++ b/inspector.cc @@ -0,0 +1,30 @@ +#include "deps/include/v8-inspector.h" + +#include "inspector.h" + +using namespace v8; +using namespace v8_inspector; + +class InspectorClient : public V8InspectorClient {}; + +extern "C" { + +InspectorPtr CreateInspector(v8Isolate* iso, InspectorClientPtr client) { + InspectorPtr inspector = V8Inspector::create(iso, client).release(); + return inspector; +} + +void DeleteInspector(InspectorPtr inspector) { + delete inspector; +} + +/********** InspectorClient **********/ + +InspectorClientPtr NewInspectorClient() { + return new InspectorClient(); +} + +void DeleteInspectorClient(InspectorClientPtr client) { + delete client; +} +} diff --git a/inspector.go b/inspector.go new file mode 100644 index 00000000..5bb289e1 --- /dev/null +++ b/inspector.go @@ -0,0 +1,32 @@ +package v8go + +// #include "inspector.h" +import "C" + +type Inspector struct { + ptr C.InspectorPtr +} + +type InspectorClient struct { + ptr C.InspectorClientPtr +} + +func NewInspector(iso *Isolate, client *InspectorClient) *Inspector { + ptr := C.CreateInspector(iso.ptr, client.ptr) + return &Inspector{ + ptr: ptr, + } +} + +func (i *Inspector) Dispose() { + C.DeleteInspector(i.ptr) +} + +func NewInspectorClient() *InspectorClient { + ptr := C.NewInspectorClient() + return &InspectorClient{ptr: ptr} +} + +func (c *InspectorClient) Dispose() { + C.DeleteInspectorClient(c.ptr) +} diff --git a/inspector.h b/inspector.h new file mode 100644 index 00000000..02d75f4f --- /dev/null +++ b/inspector.h @@ -0,0 +1,43 @@ +#ifndef V8GO_INSPECTOR_H +#define V8GO_INSPECTOR_H + +#ifdef __cplusplus + +namespace v8 { +class Isolate; +}; + +namespace v8_inspector { +class V8Inspector; +class V8InspectorClient; +}; // namespace v8_inspector + +typedef v8::Isolate v8Isolate; +typedef v8_inspector::V8Inspector* InspectorPtr; +typedef v8_inspector::V8InspectorClient* InspectorClientPtr; + +extern "C" { +#else +typedef struct v8Inspector v8Inspector; +typedef v8Inspector* InspectorPtr; + +typedef struct v8InspectorClient v8InspectorClient; +typedef v8InspectorClient* InspectorClientPtr; + +typedef struct v8Isolate v8Isolate; + +#endif + +#include +#include + +extern InspectorPtr CreateInspector(v8Isolate* iso, InspectorClientPtr client); +extern void DeleteInspector(InspectorPtr inspector); +extern InspectorClientPtr NewInspectorClient(); +extern void DeleteInspectorClient(InspectorClientPtr client); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif diff --git a/inspector_test.go b/inspector_test.go new file mode 100644 index 00000000..2bc8494a --- /dev/null +++ b/inspector_test.go @@ -0,0 +1,17 @@ +package v8go_test + +import ( + "testing" + + v8 "github.com/tommie/v8go" +) + +func TestMonitorCreateDispose(t *testing.T) { + t.Parallel() + iso := v8.NewIsolate() + defer iso.Dispose() + client := v8.NewInspectorClient() + defer client.Dispose() + inspector := v8.NewInspector(iso, client) + defer inspector.Dispose() +} From e83f0c5db9c73fd7b0e45d66ffca9974266987c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B8iman?= Date: Thu, 5 Dec 2024 07:49:16 +0100 Subject: [PATCH 02/23] Skeleton callback mechanism setup --- inspector.cc | 11 +++++-- inspector.go | 80 +++++++++++++++++++++++++++++++++++++++++++++-- inspector.h | 2 +- inspector_test.go | 11 ++++++- 4 files changed, 96 insertions(+), 8 deletions(-) diff --git a/inspector.cc b/inspector.cc index 8e92d1e4..4952b2ba 100644 --- a/inspector.cc +++ b/inspector.cc @@ -5,7 +5,12 @@ using namespace v8; using namespace v8_inspector; -class InspectorClient : public V8InspectorClient {}; +class InspectorClient : public V8InspectorClient { + int _callbackRef; + + public: + InspectorClient(int callbackRef) { _callbackRef = callbackRef; } +}; extern "C" { @@ -20,8 +25,8 @@ void DeleteInspector(InspectorPtr inspector) { /********** InspectorClient **********/ -InspectorClientPtr NewInspectorClient() { - return new InspectorClient(); +InspectorClientPtr NewInspectorClient(int callbackRef) { + return new InspectorClient(callbackRef); } void DeleteInspectorClient(InspectorClientPtr client) { diff --git a/inspector.go b/inspector.go index 5bb289e1..bd93a9bf 100644 --- a/inspector.go +++ b/inspector.go @@ -2,6 +2,10 @@ package v8go // #include "inspector.h" import "C" +import ( + "sync" + "unsafe" +) type Inspector struct { ptr C.InspectorPtr @@ -11,6 +15,68 @@ type InspectorClient struct { ptr C.InspectorClientPtr } +type MessageErrorLevel int + +// registry is a simple map of int->something. Allows passing an int to C-code +// that can be used in a callback; then Go code can retrieve the right object +// afterwards. +// This can be generalised, e.g. Isolate has similar functionality handling +// callback functions +type registry[T any] struct { + entries map[C.int]T + id C.int + lock sync.RWMutex +} + +func newRegistry[T any]() *registry[T] { + return ®istry[T]{ + entries: make(map[C.int]T), + } +} + +var clientRegistry = newRegistry[*InspectorClient]() + +func (r *registry[T]) register(instance T) C.int { + r.lock.Lock() + defer r.lock.Unlock() + r.id++ + r.entries[r.id] = instance + return r.id +} + +func (r *registry[T]) unregister(id C.int) { + r.lock.Lock() + defer r.lock.Unlock() + delete(r.entries, id) +} + +func (r *registry[T]) get(id C.int) T { + r.lock.RLock() + defer r.lock.RUnlock() + // What if not found? Return two parameters? Panic? (my preference) + return r.entries[id] +} + +type ConsoleAPIMessage struct { + contextGroupId int + errorLevel MessageErrorLevel + message string + url string + lineNumber int + columnNumber int + // stackTrace StackTrace +} + +type ConsoleAPIMessageHandler interface { + ConsoleAPIMessage(message ConsoleAPIMessage) +} + +type ConsoleAPIMessageHandlerFunc func(msg ConsoleAPIMessage) + +func (f ConsoleAPIMessageHandlerFunc) ConsoleAPIMessage(msg ConsoleAPIMessage) { + f(msg) +} + func NewInspector(iso *Isolate, client *InspectorClient) *Inspector { ptr := C.CreateInspector(iso.ptr, client.ptr) return &Inspector{ @@ -22,11 +88,19 @@ func (i *Inspector) Dispose() { C.DeleteInspector(i.ptr) } -func NewInspectorClient() *InspectorClient { - ptr := C.NewInspectorClient() - return &InspectorClient{ptr: ptr} +func NewInspectorClient(handler ConsoleAPIMessageHandler) *InspectorClient { + result := &InspectorClient{} + ref := clientRegistry.register(result) + result.ptr = C.NewInspectorClient(ref) + return result } func (c *InspectorClient) Dispose() { C.DeleteInspectorClient(c.ptr) } + +func handleConsoleAPIMessageCallback(callbackRef C.int, data unsafe.Pointer) { + // Convert data to Go data + // client := clientRegistry.get(callbackRef) + // client.handleConsoleAPIMessageCallback(data) +} diff --git a/inspector.h b/inspector.h index 02d75f4f..7c452964 100644 --- a/inspector.h +++ b/inspector.h @@ -33,7 +33,7 @@ typedef struct v8Isolate v8Isolate; extern InspectorPtr CreateInspector(v8Isolate* iso, InspectorClientPtr client); extern void DeleteInspector(InspectorPtr inspector); -extern InspectorClientPtr NewInspectorClient(); +extern InspectorClientPtr NewInspectorClient(int callbackRef); extern void DeleteInspectorClient(InspectorClientPtr client); #ifdef __cplusplus diff --git a/inspector_test.go b/inspector_test.go index 2bc8494a..1c25ef87 100644 --- a/inspector_test.go +++ b/inspector_test.go @@ -6,11 +6,20 @@ import ( v8 "github.com/tommie/v8go" ) +type consoleAPIMessageRecorder struct { + messages []v8.ConsoleAPIMessage +} + +func (r *consoleAPIMessageRecorder) ConsoleAPIMessage(msg v8.ConsoleAPIMessage) { + r.messages = append(r.messages, msg) +} + func TestMonitorCreateDispose(t *testing.T) { + recorder := consoleAPIMessageRecorder{} t.Parallel() iso := v8.NewIsolate() defer iso.Dispose() - client := v8.NewInspectorClient() + client := v8.NewInspectorClient(&recorder) defer client.Dispose() inspector := v8.NewInspector(iso, client) defer inspector.Dispose() From 68bccb5977e55fcd99274b4515019d631f09abff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B8iman?= Date: Thu, 5 Dec 2024 08:20:04 +0100 Subject: [PATCH 03/23] Create C++ functions to register inspector --- inspector.cc | 46 +++++++++++++++++++++++++++++++++++++++++++++- inspector.go | 29 +++++++++++++++++++++++------ inspector.h | 7 +++++++ inspector_test.go | 14 ++++++++++++++ 4 files changed, 89 insertions(+), 7 deletions(-) diff --git a/inspector.cc b/inspector.cc index 4952b2ba..720ce4af 100644 --- a/inspector.cc +++ b/inspector.cc @@ -1,5 +1,7 @@ #include "deps/include/v8-inspector.h" +#include "_cgo_export.h" +#include "context-macros.h" #include "inspector.h" using namespace v8; @@ -9,9 +11,37 @@ class InspectorClient : public V8InspectorClient { int _callbackRef; public: - InspectorClient(int callbackRef) { _callbackRef = callbackRef; } + InspectorClient(int callbackRef) { + _callbackRef = callbackRef; + printf("Client from C++"); + } + void consoleAPIMessage(int contextGroupId, + v8::Isolate::MessageErrorLevel level, + const StringView& message, + const StringView& url, + unsigned lineNumber, + unsigned columnNumber, + V8StackTrace*) override; }; +void InspectorClient::consoleAPIMessage(int contextGroupId, + v8::Isolate::MessageErrorLevel level, + const StringView& message, + const StringView& url, + unsigned lineNumber, + unsigned columnNumber, + V8StackTrace*) { + printf("Hello from C++"); + goHandleConsoleAPIMessageCallback(_callbackRef); + // if (message.is8Bit()) { + // goConsoleAPIMessageUtf8(level, lineNumber, (char*)message.characters8()); + // } else { + // goConsoleAPIMessageUtf16(level, lineNumber, + // (char*)message.characters16(), + // message.length() * 2); + // } +} + extern "C" { InspectorPtr CreateInspector(v8Isolate* iso, InspectorClientPtr client) { @@ -29,6 +59,20 @@ InspectorClientPtr NewInspectorClient(int callbackRef) { return new InspectorClient(callbackRef); } +void InspectorContextCreated(InspectorPtr inspector, ContextPtr context) { + printf("Create from C++"); + LOCAL_CONTEXT(context); + int groupId = 1; + StringView name = StringView((const uint8_t*)"Test", 4); + V8ContextInfo info = V8ContextInfo(local_ctx, groupId, name); + inspector->contextCreated(info); +} + +void InspectorContextDestroyed(InspectorPtr inspector, ContextPtr context) { + LOCAL_CONTEXT(context); + inspector->contextDestroyed(local_ctx); +} + void DeleteInspectorClient(InspectorClientPtr client) { delete client; } diff --git a/inspector.go b/inspector.go index bd93a9bf..a9b13c38 100644 --- a/inspector.go +++ b/inspector.go @@ -3,8 +3,8 @@ package v8go // #include "inspector.h" import "C" import ( + "fmt" "sync" - "unsafe" ) type Inspector struct { @@ -12,7 +12,8 @@ type Inspector struct { } type InspectorClient struct { - ptr C.InspectorClientPtr + ptr C.InspectorClientPtr + handler ConsoleAPIMessageHandler } type MessageErrorLevel int @@ -60,7 +61,7 @@ func (r *registry[T]) get(id C.int) T { type ConsoleAPIMessage struct { contextGroupId int errorLevel MessageErrorLevel - message string + Message string url string lineNumber int columnNumber int @@ -88,8 +89,19 @@ func (i *Inspector) Dispose() { C.DeleteInspector(i.ptr) } +func (i *Inspector) ContextCreated(ctx *Context) { + fmt.Println("Created from Go") + C.InspectorContextCreated(i.ptr, ctx.ptr) +} + +func (i *Inspector) ContextDestroyed(ctx *Context) { + C.InspectorContextDestroyed(i.ptr, ctx.ptr) +} + func NewInspectorClient(handler ConsoleAPIMessageHandler) *InspectorClient { - result := &InspectorClient{} + result := &InspectorClient{ + handler: handler, + } ref := clientRegistry.register(result) result.ptr = C.NewInspectorClient(ref) return result @@ -99,8 +111,13 @@ func (c *InspectorClient) Dispose() { C.DeleteInspectorClient(c.ptr) } -func handleConsoleAPIMessageCallback(callbackRef C.int, data unsafe.Pointer) { +//export goHandleConsoleAPIMessageCallback +func goHandleConsoleAPIMessageCallback(callbackRef C.int) { + fmt.Println("Callback from Go") // Convert data to Go data - // client := clientRegistry.get(callbackRef) + client := clientRegistry.get(callbackRef) + client.handler.ConsoleAPIMessage(ConsoleAPIMessage{ + Message: "Hello", + }) // client.handleConsoleAPIMessageCallback(data) } diff --git a/inspector.h b/inspector.h index 7c452964..cb426664 100644 --- a/inspector.h +++ b/inspector.h @@ -31,8 +31,15 @@ typedef struct v8Isolate v8Isolate; #include #include +typedef struct m_ctx m_ctx; +typedef m_ctx* ContextPtr; + extern InspectorPtr CreateInspector(v8Isolate* iso, InspectorClientPtr client); extern void DeleteInspector(InspectorPtr inspector); +extern void InspectorContextCreated(InspectorPtr inspector, ContextPtr context); +extern void InspectorContextDestroyed(InspectorPtr inspector, + ContextPtr context); + extern InspectorClientPtr NewInspectorClient(int callbackRef); extern void DeleteInspectorClient(InspectorClientPtr client); diff --git a/inspector_test.go b/inspector_test.go index 1c25ef87..473ccc7f 100644 --- a/inspector_test.go +++ b/inspector_test.go @@ -23,4 +23,18 @@ func TestMonitorCreateDispose(t *testing.T) { defer client.Dispose() inspector := v8.NewInspector(iso, client) defer inspector.Dispose() + context := v8.NewContext(iso) + defer context.Close() + inspector.ContextCreated(context) + defer inspector.ContextDestroyed(context) + _, err := context.RunScript("console.log('Hello, world!')", "") + if err != nil { + t.Error("Error occurred: " + err.Error()) + return + } + if len(recorder.messages) != 1 { + t.Error("Expected exactly one message") + } else if recorder.messages[0].Message != "Hello, world!" { + t.Error("Expected Hello, World") + } } From 18242acf8a57d5bb5cd998fa9703b1d16a27a4a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B8iman?= Date: Thu, 5 Dec 2024 09:21:29 +0100 Subject: [PATCH 04/23] Get strings from message --- function_template.go | 12 ++++++++++-- inspector.cc | 19 ++++++------------- inspector.go | 41 ++++++++++++++++++++++++++++++++++++----- inspector.h | 8 ++++++++ inspector_test.go | 21 +++++++++++++++++---- 5 files changed, 77 insertions(+), 24 deletions(-) diff --git a/function_template.go b/function_template.go index 5caff65e..d4699226 100644 --- a/function_template.go +++ b/function_template.go @@ -78,7 +78,10 @@ func NewFunctionTemplate(iso *Isolate, callback FunctionCallback) *FunctionTempl // NewFunctionTemplateWithError creates a FunctionTemplate for a given // callback. If the callback returns an error, it will be thrown as a // JS error. -func NewFunctionTemplateWithError(iso *Isolate, callback FunctionCallbackWithError) *FunctionTemplate { +func NewFunctionTemplateWithError( + iso *Isolate, + callback FunctionCallbackWithError, +) *FunctionTemplate { if iso == nil { panic("nil Isolate argument not supported") } @@ -111,7 +114,12 @@ func (tmpl *FunctionTemplate) GetFunction(ctx *Context) *Function { // to workaround an ERROR_COMMITMENT_LIMIT error on windows that was detected in CI. // //export goFunctionCallback -func goFunctionCallback(ctxref int, cbref int, thisAndArgs *C.ValuePtr, argsCount int) (rval C.ValuePtr, rerr C.ValuePtr) { +func goFunctionCallback( + ctxref int, + cbref int, + thisAndArgs *C.ValuePtr, + argsCount int, +) (rval C.ValuePtr, rerr C.ValuePtr) { ctx := getContext(ctxref) this := *thisAndArgs diff --git a/inspector.cc b/inspector.cc index 720ce4af..e4906cc8 100644 --- a/inspector.cc +++ b/inspector.cc @@ -11,10 +11,7 @@ class InspectorClient : public V8InspectorClient { int _callbackRef; public: - InspectorClient(int callbackRef) { - _callbackRef = callbackRef; - printf("Client from C++"); - } + InspectorClient(int callbackRef) { _callbackRef = callbackRef; } void consoleAPIMessage(int contextGroupId, v8::Isolate::MessageErrorLevel level, const StringView& message, @@ -31,15 +28,11 @@ void InspectorClient::consoleAPIMessage(int contextGroupId, unsigned lineNumber, unsigned columnNumber, V8StackTrace*) { - printf("Hello from C++"); - goHandleConsoleAPIMessageCallback(_callbackRef); - // if (message.is8Bit()) { - // goConsoleAPIMessageUtf8(level, lineNumber, (char*)message.characters8()); - // } else { - // goConsoleAPIMessageUtf16(level, lineNumber, - // (char*)message.characters16(), - // message.length() * 2); - // } + StringViewData msg; + msg.is8bit = message.is8Bit(); + msg.data = message.characters8(); + msg.length = message.length(); + goHandleConsoleAPIMessageCallback(_callbackRef, contextGroupId, level, msg); } extern "C" { diff --git a/inspector.go b/inspector.go index a9b13c38..badc11b1 100644 --- a/inspector.go +++ b/inspector.go @@ -5,6 +5,18 @@ import "C" import ( "fmt" "sync" + "unicode/utf16" +) + +type MessageErrorLevel uint8 + +const ( + ErrorLevelLog MessageErrorLevel = 1 << iota + ErrorLevelDebug + ErrorLevelInfo + ErrorLevelError + ErrorLevelWarning + ErrorLevelAll = ErrorLevelLog | ErrorLevelDebug | ErrorLevelInfo | ErrorLevelError | ErrorLevelWarning ) type Inspector struct { @@ -16,8 +28,6 @@ type InspectorClient struct { handler ConsoleAPIMessageHandler } -type MessageErrorLevel int - // registry is a simple map of int->something. Allows passing an int to C-code // that can be used in a callback; then Go code can retrieve the right object // afterwards. @@ -60,7 +70,7 @@ func (r *registry[T]) get(id C.int) T { type ConsoleAPIMessage struct { contextGroupId int - errorLevel MessageErrorLevel + ErrorLevel MessageErrorLevel Message string url string lineNumber int @@ -111,13 +121,34 @@ func (c *InspectorClient) Dispose() { C.DeleteInspectorClient(c.ptr) } +func stringViewToString(d C.StringViewData) string { + if d.is8bit { + data := C.GoBytes(d.data, d.length) + return string(data) + } else { + data := C.GoBytes(d.data, d.length*2) + shorts := make([]uint16, len(data)/2) + for i := 0; i < len(data); i += 2 { + shorts[i/2] = (uint16(data[i+1]) << 8) | uint16(data[i]) + } + return string(utf16.Decode(shorts)) + } +} + +// //export goHandleConsoleAPIMessageCallback -func goHandleConsoleAPIMessageCallback(callbackRef C.int) { +func goHandleConsoleAPIMessageCallback( + callbackRef C.int, + contextGroupId C.int, + errorLevel C.int, + message C.StringViewData, +) { fmt.Println("Callback from Go") // Convert data to Go data client := clientRegistry.get(callbackRef) client.handler.ConsoleAPIMessage(ConsoleAPIMessage{ - Message: "Hello", + Message: stringViewToString(message), + ErrorLevel: MessageErrorLevel(errorLevel), }) // client.handleConsoleAPIMessageCallback(data) } diff --git a/inspector.h b/inspector.h index cb426664..6c6c521f 100644 --- a/inspector.h +++ b/inspector.h @@ -26,6 +26,8 @@ typedef v8InspectorClient* InspectorClientPtr; typedef struct v8Isolate v8Isolate; +typedef _Bool bool; + #endif #include @@ -43,6 +45,12 @@ extern void InspectorContextDestroyed(InspectorPtr inspector, extern InspectorClientPtr NewInspectorClient(int callbackRef); extern void DeleteInspectorClient(InspectorClientPtr client); +typedef struct StringViewData { + bool is8bit; + void const* data; + int length; +} StringViewData; + #ifdef __cplusplus } // extern "C" #endif diff --git a/inspector_test.go b/inspector_test.go index 473ccc7f..78933be9 100644 --- a/inspector_test.go +++ b/inspector_test.go @@ -27,14 +27,27 @@ func TestMonitorCreateDispose(t *testing.T) { defer context.Close() inspector.ContextCreated(context) defer inspector.ContextDestroyed(context) - _, err := context.RunScript("console.log('Hello, world!')", "") + _, err := context.RunScript("console.log('Hello, world!'); console.error('Error, world!');", "") if err != nil { t.Error("Error occurred: " + err.Error()) return } - if len(recorder.messages) != 1 { + if len(recorder.messages) != 2 { t.Error("Expected exactly one message") - } else if recorder.messages[0].Message != "Hello, world!" { - t.Error("Expected Hello, World") + } else { + msg1 := recorder.messages[0] + msg2 := recorder.messages[1] + if msg1.ErrorLevel != v8.ErrorLevelLog { + t.Errorf("Expected Log error level. Got %d", msg1.ErrorLevel) + } + if msg2.ErrorLevel != v8.ErrorLevelError { + t.Errorf("Expected Error error level. Got: %d", msg2.ErrorLevel) + } + if msg1.Message != "Hello, world!" { + t.Errorf("Expected Hello, World, got %s", msg1.Message) + } + if msg2.Message != "Error, world!" { + t.Errorf("Expected Error, world!, got %s", msg2.Message) + } } } From 0a33595285052e52280fdb802d759803796d277a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B8iman?= Date: Thu, 5 Dec 2024 09:31:58 +0100 Subject: [PATCH 05/23] Create wrappers for cleanup --- inspector_test.go | 54 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/inspector_test.go b/inspector_test.go index 78933be9..fad8af5f 100644 --- a/inspector_test.go +++ b/inspector_test.go @@ -14,19 +14,53 @@ func (r *consoleAPIMessageRecorder) ConsoleAPIMessage(msg v8.ConsoleAPIMessage) r.messages = append(r.messages, msg) } +type IsolateWithInspector struct { + iso *v8.Isolate + inspector *v8.Inspector + inspectorClient *v8.InspectorClient +} + +func NewIsolateWithInspectorClient(handler v8.ConsoleAPIMessageHandler) *IsolateWithInspector { + iso := v8.NewIsolate() + client := v8.NewInspectorClient(handler) + inspector := v8.NewInspector(iso, client) + return &IsolateWithInspector{ + iso, + inspector, + client, + } +} + +func (iso *IsolateWithInspector) Dispose() { + iso.inspector.Dispose() + iso.inspectorClient.Dispose() + iso.iso.Dispose() +} + +type ContextWithInspector struct { + *v8.Context + iso *IsolateWithInspector +} + +func (iso *IsolateWithInspector) NewContext() *ContextWithInspector { + context := v8.NewContext(iso.iso) + iso.inspector.ContextCreated(context) + return &ContextWithInspector{context, iso} +} + +func (ctx *ContextWithInspector) Dispose() { + ctx.iso.inspector.ContextDestroyed(ctx.Context) + ctx.Context.Close() +} + func TestMonitorCreateDispose(t *testing.T) { - recorder := consoleAPIMessageRecorder{} t.Parallel() - iso := v8.NewIsolate() + recorder := consoleAPIMessageRecorder{} + iso := NewIsolateWithInspectorClient(&recorder) defer iso.Dispose() - client := v8.NewInspectorClient(&recorder) - defer client.Dispose() - inspector := v8.NewInspector(iso, client) - defer inspector.Dispose() - context := v8.NewContext(iso) - defer context.Close() - inspector.ContextCreated(context) - defer inspector.ContextDestroyed(context) + context := iso.NewContext() + defer context.Dispose() + _, err := context.RunScript("console.log('Hello, world!'); console.error('Error, world!');", "") if err != nil { t.Error("Error occurred: " + err.Error()) From 39d6d179556be1c81abe6ec0a8d159c3c02cecba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B8iman?= Date: Thu, 5 Dec 2024 09:32:36 +0100 Subject: [PATCH 06/23] Fixup, remove fmt --- inspector.cc | 1 - inspector.go | 3 --- 2 files changed, 4 deletions(-) diff --git a/inspector.cc b/inspector.cc index e4906cc8..00e80f3d 100644 --- a/inspector.cc +++ b/inspector.cc @@ -53,7 +53,6 @@ InspectorClientPtr NewInspectorClient(int callbackRef) { } void InspectorContextCreated(InspectorPtr inspector, ContextPtr context) { - printf("Create from C++"); LOCAL_CONTEXT(context); int groupId = 1; StringView name = StringView((const uint8_t*)"Test", 4); diff --git a/inspector.go b/inspector.go index badc11b1..ef1940c3 100644 --- a/inspector.go +++ b/inspector.go @@ -3,7 +3,6 @@ package v8go // #include "inspector.h" import "C" import ( - "fmt" "sync" "unicode/utf16" ) @@ -100,7 +99,6 @@ func (i *Inspector) Dispose() { } func (i *Inspector) ContextCreated(ctx *Context) { - fmt.Println("Created from Go") C.InspectorContextCreated(i.ptr, ctx.ptr) } @@ -143,7 +141,6 @@ func goHandleConsoleAPIMessageCallback( errorLevel C.int, message C.StringViewData, ) { - fmt.Println("Callback from Go") // Convert data to Go data client := clientRegistry.get(callbackRef) client.handler.ConsoleAPIMessage(ConsoleAPIMessage{ From b7fe7b06a938564269b7c9c98ec4b3b84f0aebbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B8iman?= Date: Thu, 5 Dec 2024 09:42:06 +0100 Subject: [PATCH 07/23] Add URL and line numbers to console message --- inspector.cc | 19 ++++++++++++++----- inspector.go | 17 ++++++++++++----- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/inspector.cc b/inspector.cc index 00e80f3d..fc00358e 100644 --- a/inspector.cc +++ b/inspector.cc @@ -21,6 +21,17 @@ class InspectorClient : public V8InspectorClient { V8StackTrace*) override; }; +StringViewData ConvertStringView(const StringView& view) { + StringViewData msg; + msg.is8bit = view.is8Bit(); + // The ? isn't necessary, the two functions return the sama pointer. But that + // has been considered an implementation detail that may change. + msg.data = + view.is8Bit() ? (void*)view.characters8() : (void*)view.characters16(); + msg.length = view.length(); + return msg; +} + void InspectorClient::consoleAPIMessage(int contextGroupId, v8::Isolate::MessageErrorLevel level, const StringView& message, @@ -28,11 +39,9 @@ void InspectorClient::consoleAPIMessage(int contextGroupId, unsigned lineNumber, unsigned columnNumber, V8StackTrace*) { - StringViewData msg; - msg.is8bit = message.is8Bit(); - msg.data = message.characters8(); - msg.length = message.length(); - goHandleConsoleAPIMessageCallback(_callbackRef, contextGroupId, level, msg); + goHandleConsoleAPIMessageCallback( + _callbackRef, contextGroupId, level, ConvertStringView(message), + ConvertStringView(url), lineNumber, columnNumber); } extern "C" { diff --git a/inspector.go b/inspector.go index ef1940c3..1ecd14cf 100644 --- a/inspector.go +++ b/inspector.go @@ -71,9 +71,9 @@ type ConsoleAPIMessage struct { contextGroupId int ErrorLevel MessageErrorLevel Message string - url string - lineNumber int - columnNumber int + Url string + LineNumber uint + ColumnNumber uint // stackTrace StackTrace } @@ -140,12 +140,19 @@ func goHandleConsoleAPIMessageCallback( contextGroupId C.int, errorLevel C.int, message C.StringViewData, + url C.StringViewData, + lineNumber C.uint, + columnNumber C.uint, ) { // Convert data to Go data client := clientRegistry.get(callbackRef) + // TODO, Stack trace client.handler.ConsoleAPIMessage(ConsoleAPIMessage{ - Message: stringViewToString(message), - ErrorLevel: MessageErrorLevel(errorLevel), + ErrorLevel: MessageErrorLevel(errorLevel), + Message: stringViewToString(message), + Url: stringViewToString(url), + LineNumber: uint(lineNumber), + ColumnNumber: uint(columnNumber), }) // client.handleConsoleAPIMessageCallback(data) } From 5afe4863b72afd70d81751791d91004a3299fc74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B8iman?= Date: Fri, 3 Jan 2025 10:28:26 +0100 Subject: [PATCH 08/23] Use handle instead of map of ints --- inspector.cc | 6 ++-- inspector.go | 79 +++++++++++++--------------------------------------- inspector.h | 2 +- 3 files changed, 24 insertions(+), 63 deletions(-) diff --git a/inspector.cc b/inspector.cc index fc00358e..ff32e2ea 100644 --- a/inspector.cc +++ b/inspector.cc @@ -8,10 +8,10 @@ using namespace v8; using namespace v8_inspector; class InspectorClient : public V8InspectorClient { - int _callbackRef; + uintptr_t _callbackRef; public: - InspectorClient(int callbackRef) { _callbackRef = callbackRef; } + InspectorClient(uintptr_t callbackRef) { _callbackRef = callbackRef; } void consoleAPIMessage(int contextGroupId, v8::Isolate::MessageErrorLevel level, const StringView& message, @@ -57,7 +57,7 @@ void DeleteInspector(InspectorPtr inspector) { /********** InspectorClient **********/ -InspectorClientPtr NewInspectorClient(int callbackRef) { +InspectorClientPtr NewInspectorClient(uintptr_t callbackRef) { return new InspectorClient(callbackRef); } diff --git a/inspector.go b/inspector.go index 1ecd14cf..40b76d97 100644 --- a/inspector.go +++ b/inspector.go @@ -3,7 +3,7 @@ package v8go // #include "inspector.h" import "C" import ( - "sync" + "runtime/cgo" "unicode/utf16" ) @@ -23,48 +23,8 @@ type Inspector struct { } type InspectorClient struct { - ptr C.InspectorClientPtr - handler ConsoleAPIMessageHandler -} - -// registry is a simple map of int->something. Allows passing an int to C-code -// that can be used in a callback; then Go code can retrieve the right object -// afterwards. -// This can be generalised, e.g. Isolate has similar functionality handling -// callback functions -type registry[T any] struct { - entries map[C.int]T - id C.int - lock sync.RWMutex -} - -func newRegistry[T any]() *registry[T] { - return ®istry[T]{ - entries: make(map[C.int]T), - } -} - -var clientRegistry = newRegistry[*InspectorClient]() - -func (r *registry[T]) register(instance T) C.int { - r.lock.Lock() - defer r.lock.Unlock() - r.id++ - r.entries[r.id] = instance - return r.id -} - -func (r *registry[T]) unregister(id C.int) { - r.lock.Lock() - defer r.lock.Unlock() - delete(r.entries, id) -} - -func (r *registry[T]) get(id C.int) T { - r.lock.RLock() - defer r.lock.RUnlock() - // What if not found? Return two parameters? Panic? (my preference) - return r.entries[id] + ptr C.InspectorClientPtr + clientHandle cgo.Handle } type ConsoleAPIMessage struct { @@ -107,12 +67,12 @@ func (i *Inspector) ContextDestroyed(ctx *Context) { } func NewInspectorClient(handler ConsoleAPIMessageHandler) *InspectorClient { - result := &InspectorClient{ - handler: handler, + clientHandle := cgo.NewHandle(handler) + ptr := C.NewInspectorClient(C.uintptr_t(clientHandle)) + return &InspectorClient{ + clientHandle: clientHandle, + ptr: ptr, } - ref := clientRegistry.register(result) - result.ptr = C.NewInspectorClient(ref) - return result } func (c *InspectorClient) Dispose() { @@ -136,7 +96,7 @@ func stringViewToString(d C.StringViewData) string { // //export goHandleConsoleAPIMessageCallback func goHandleConsoleAPIMessageCallback( - callbackRef C.int, + callbackRef C.uintptr_t, contextGroupId C.int, errorLevel C.int, message C.StringViewData, @@ -145,14 +105,15 @@ func goHandleConsoleAPIMessageCallback( columnNumber C.uint, ) { // Convert data to Go data - client := clientRegistry.get(callbackRef) - // TODO, Stack trace - client.handler.ConsoleAPIMessage(ConsoleAPIMessage{ - ErrorLevel: MessageErrorLevel(errorLevel), - Message: stringViewToString(message), - Url: stringViewToString(url), - LineNumber: uint(lineNumber), - ColumnNumber: uint(columnNumber), - }) - // client.handleConsoleAPIMessageCallback(data) + handle := cgo.Handle(callbackRef) + if client, ok := handle.Value().(ConsoleAPIMessageHandler); ok { + // TODO, Stack trace + client.ConsoleAPIMessage(ConsoleAPIMessage{ + ErrorLevel: MessageErrorLevel(errorLevel), + Message: stringViewToString(message), + Url: stringViewToString(url), + LineNumber: uint(lineNumber), + ColumnNumber: uint(columnNumber), + }) + } } diff --git a/inspector.h b/inspector.h index 6c6c521f..e1c8b7b3 100644 --- a/inspector.h +++ b/inspector.h @@ -42,7 +42,7 @@ extern void InspectorContextCreated(InspectorPtr inspector, ContextPtr context); extern void InspectorContextDestroyed(InspectorPtr inspector, ContextPtr context); -extern InspectorClientPtr NewInspectorClient(int callbackRef); +extern InspectorClientPtr NewInspectorClient(uintptr_t callbackRef); extern void DeleteInspectorClient(InspectorClientPtr client); typedef struct StringViewData { From df8b531ab54541578c2072a4c97f77dbcc2a5072 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B8iman?= Date: Fri, 3 Jan 2025 12:33:56 +0100 Subject: [PATCH 09/23] Add doc comments for public types --- inspector.go | 52 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/inspector.go b/inspector.go index 40b76d97..60e2c5da 100644 --- a/inspector.go +++ b/inspector.go @@ -7,6 +7,8 @@ import ( "unicode/utf16" ) +// Represents the level of console output from JavaScript. E.g., `console.log`, +// `console.error`, etc. type MessageErrorLevel uint8 const ( @@ -18,15 +20,30 @@ const ( ErrorLevelAll = ErrorLevelLog | ErrorLevelDebug | ErrorLevelInfo | ErrorLevelError | ErrorLevelWarning ) +// An Inspector in v8 provides access to internals of the engine, such as +// console output +// +// To receive console output, you need to first create an [InspectorClient] +// which will handle the interaction for a specific [Context]. +// +// After a Context is created, you need to register it with the Inspector using +// [Inspector.ContextCreated], and cleanup using [Inspector.ContextDestroyed]. +// +// See also: https://v8.github.io/api/head/classv8__inspector_1_1V8Inspector.html type Inspector struct { ptr C.InspectorPtr } +// An InspectorClient is the bridge from the [Inspector] to your code. type InspectorClient struct { ptr C.InspectorClientPtr clientHandle cgo.Handle } +// ConsoleAPIMessage contains the information from v8 from console function +// calls. +// +// V8 also provides a stack trace, which isn't yet supported here. type ConsoleAPIMessage struct { contextGroupId int ErrorLevel MessageErrorLevel @@ -37,16 +54,16 @@ type ConsoleAPIMessage struct { // stackTrace StackTrace } +// A ConsoleAPIMessageHandler will receive JavaScript `console` API calls. type ConsoleAPIMessageHandler interface { ConsoleAPIMessage(message ConsoleAPIMessage) } -type ConsoleAPIMessageHandlerFunc func(msg ConsoleAPIMessage) - -func (f ConsoleAPIMessageHandlerFunc) ConsoleAPIMessage(msg ConsoleAPIMessage) { - f(msg) -} - +// NewInspector creates an [Inspector] for a specific [Isolate] iso +// communicating with the [InspectorClient] client. +// +// Before disposing the iso, be sure to dispose the inspector using +// [Inspector.Dispose] func NewInspector(iso *Isolate, client *InspectorClient) *Inspector { ptr := C.CreateInspector(iso.ptr, client.ptr) return &Inspector{ @@ -54,18 +71,25 @@ func NewInspector(iso *Isolate, client *InspectorClient) *Inspector { } } +// Dispose the [Inspector]. Call this before disposing the [Isolate] and the +// [InspectorClient] that this is connected to. func (i *Inspector) Dispose() { C.DeleteInspector(i.ptr) } +// ContextCreated tells the inspector that a new [Context] has been created. +// This must be called before the [InspectorClient] can be used. func (i *Inspector) ContextCreated(ctx *Context) { C.InspectorContextCreated(i.ptr, ctx.ptr) } +// ContextDestroyed must be called before a [Context] is closed. func (i *Inspector) ContextDestroyed(ctx *Context) { C.InspectorContextDestroyed(i.ptr, ctx.ptr) } +// Create a new [InspectorClient] passing a handler that will receive the +// callbacks from v8. func NewInspectorClient(handler ConsoleAPIMessageHandler) *InspectorClient { clientHandle := cgo.NewHandle(handler) ptr := C.NewInspectorClient(C.uintptr_t(clientHandle)) @@ -75,7 +99,10 @@ func NewInspectorClient(handler ConsoleAPIMessageHandler) *InspectorClient { } } +// Dispose frees up resources taken up by the [InspectorClient]. Be sure to call +// this after calling [Inspector.Dispose] func (c *InspectorClient) Dispose() { + c.clientHandle.Delete() C.DeleteInspectorClient(c.ptr) } @@ -93,7 +120,6 @@ func stringViewToString(d C.StringViewData) string { } } -// //export goHandleConsoleAPIMessageCallback func goHandleConsoleAPIMessageCallback( callbackRef C.uintptr_t, @@ -104,16 +130,16 @@ func goHandleConsoleAPIMessageCallback( lineNumber C.uint, columnNumber C.uint, ) { - // Convert data to Go data handle := cgo.Handle(callbackRef) if client, ok := handle.Value().(ConsoleAPIMessageHandler); ok { // TODO, Stack trace client.ConsoleAPIMessage(ConsoleAPIMessage{ - ErrorLevel: MessageErrorLevel(errorLevel), - Message: stringViewToString(message), - Url: stringViewToString(url), - LineNumber: uint(lineNumber), - ColumnNumber: uint(columnNumber), + contextGroupId: int(contextGroupId), + ErrorLevel: MessageErrorLevel(errorLevel), + Message: stringViewToString(message), + Url: stringViewToString(url), + LineNumber: uint(lineNumber), + ColumnNumber: uint(columnNumber), }) } } From 50b0c4b1383dc5130d2e838fc85fb9dd0de9526e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B8iman?= Date: Fri, 3 Jan 2025 12:44:24 +0100 Subject: [PATCH 10/23] Remove unhelpful Ptr types --- inspector.cc | 14 +++++++------- inspector.go | 4 ++-- inspector.h | 19 ++++++++----------- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/inspector.cc b/inspector.cc index ff32e2ea..ac09426a 100644 --- a/inspector.cc +++ b/inspector.cc @@ -46,22 +46,22 @@ void InspectorClient::consoleAPIMessage(int contextGroupId, extern "C" { -InspectorPtr CreateInspector(v8Isolate* iso, InspectorClientPtr client) { - InspectorPtr inspector = V8Inspector::create(iso, client).release(); +v8Inspector* CreateInspector(v8Isolate* iso, v8InspectorClient* client) { + v8Inspector* inspector = V8Inspector::create(iso, client).release(); return inspector; } -void DeleteInspector(InspectorPtr inspector) { +void DeleteInspector(v8Inspector* inspector) { delete inspector; } /********** InspectorClient **********/ -InspectorClientPtr NewInspectorClient(uintptr_t callbackRef) { +v8InspectorClient* NewInspectorClient(uintptr_t callbackRef) { return new InspectorClient(callbackRef); } -void InspectorContextCreated(InspectorPtr inspector, ContextPtr context) { +void InspectorContextCreated(v8Inspector* inspector, ContextPtr context) { LOCAL_CONTEXT(context); int groupId = 1; StringView name = StringView((const uint8_t*)"Test", 4); @@ -69,12 +69,12 @@ void InspectorContextCreated(InspectorPtr inspector, ContextPtr context) { inspector->contextCreated(info); } -void InspectorContextDestroyed(InspectorPtr inspector, ContextPtr context) { +void InspectorContextDestroyed(v8Inspector* inspector, ContextPtr context) { LOCAL_CONTEXT(context); inspector->contextDestroyed(local_ctx); } -void DeleteInspectorClient(InspectorClientPtr client) { +void DeleteInspectorClient(v8InspectorClient* client) { delete client; } } diff --git a/inspector.go b/inspector.go index 60e2c5da..5adcbdff 100644 --- a/inspector.go +++ b/inspector.go @@ -31,12 +31,12 @@ const ( // // See also: https://v8.github.io/api/head/classv8__inspector_1_1V8Inspector.html type Inspector struct { - ptr C.InspectorPtr + ptr *C.v8Inspector } // An InspectorClient is the bridge from the [Inspector] to your code. type InspectorClient struct { - ptr C.InspectorClientPtr + ptr *C.v8InspectorClient clientHandle cgo.Handle } diff --git a/inspector.h b/inspector.h index e1c8b7b3..d542edff 100644 --- a/inspector.h +++ b/inspector.h @@ -13,16 +13,13 @@ class V8InspectorClient; }; // namespace v8_inspector typedef v8::Isolate v8Isolate; -typedef v8_inspector::V8Inspector* InspectorPtr; -typedef v8_inspector::V8InspectorClient* InspectorClientPtr; +typedef v8_inspector::V8Inspector v8Inspector; +typedef v8_inspector::V8InspectorClient v8InspectorClient; extern "C" { #else typedef struct v8Inspector v8Inspector; -typedef v8Inspector* InspectorPtr; - typedef struct v8InspectorClient v8InspectorClient; -typedef v8InspectorClient* InspectorClientPtr; typedef struct v8Isolate v8Isolate; @@ -36,14 +33,14 @@ typedef _Bool bool; typedef struct m_ctx m_ctx; typedef m_ctx* ContextPtr; -extern InspectorPtr CreateInspector(v8Isolate* iso, InspectorClientPtr client); -extern void DeleteInspector(InspectorPtr inspector); -extern void InspectorContextCreated(InspectorPtr inspector, ContextPtr context); -extern void InspectorContextDestroyed(InspectorPtr inspector, +extern v8Inspector* CreateInspector(v8Isolate* iso, v8InspectorClient* client); +extern void DeleteInspector(v8Inspector* inspector); +extern void InspectorContextCreated(v8Inspector* inspector, ContextPtr context); +extern void InspectorContextDestroyed(v8Inspector* inspector, ContextPtr context); -extern InspectorClientPtr NewInspectorClient(uintptr_t callbackRef); -extern void DeleteInspectorClient(InspectorClientPtr client); +extern v8InspectorClient* NewInspectorClient(uintptr_t callbackRef); +extern void DeleteInspectorClient(v8InspectorClient* client); typedef struct StringViewData { bool is8bit; From 4f4f6e75d9f2242e0d5c157e79381a596e7216a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B8iman?= Date: Sat, 4 Jan 2025 09:51:20 +0100 Subject: [PATCH 11/23] Add notes to CHANGELOG about new functionality --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3598b334..8b43e14a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- Add support to setup an `Inspector`, and `InspectorClient` to receive output from `console` messages in JS code. + ### Changed ## [v0.27.0] - 2024-12-19 From 186f8fa17cb15154fe99cb3794f1bd2560fe21f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B8iman?= Date: Sat, 4 Jan 2025 10:46:36 +0100 Subject: [PATCH 12/23] Document what undelying type this reflects --- inspector.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/inspector.go b/inspector.go index 5adcbdff..1c37453a 100644 --- a/inspector.go +++ b/inspector.go @@ -9,6 +9,8 @@ import ( // Represents the level of console output from JavaScript. E.g., `console.log`, // `console.error`, etc. +// +// The values reflect the values of v8::Isolate::MessageErrorLevel type MessageErrorLevel uint8 const ( From 873d9195b1fff4951a16da52fe603c96a728c3f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B8iman?= Date: Sat, 4 Jan 2025 11:10:14 +0100 Subject: [PATCH 13/23] Improve console test, and add string formatting --- inspector.go | 18 +++++++++++++++++ inspector_test.go | 51 +++++++++++++++++++++++++++-------------------- 2 files changed, 47 insertions(+), 22 deletions(-) diff --git a/inspector.go b/inspector.go index 1c37453a..744c9539 100644 --- a/inspector.go +++ b/inspector.go @@ -4,6 +4,7 @@ package v8go import "C" import ( "runtime/cgo" + "strconv" "unicode/utf16" ) @@ -13,6 +14,23 @@ import ( // The values reflect the values of v8::Isolate::MessageErrorLevel type MessageErrorLevel uint8 +func (lvl MessageErrorLevel) String() string { + switch lvl { + case ErrorLevelLog: + return "log" + case ErrorLevelDebug: + return "debug" + case ErrorLevelError: + return "error" + case ErrorLevelInfo: + return "info" + case ErrorLevelWarning: + return "warning" + default: + return strconv.Itoa(int(lvl)) + } +} + const ( ErrorLevelLog MessageErrorLevel = 1 << iota ErrorLevelDebug diff --git a/inspector_test.go b/inspector_test.go index fad8af5f..bc0c7811 100644 --- a/inspector_test.go +++ b/inspector_test.go @@ -1,17 +1,25 @@ package v8go_test import ( + "reflect" "testing" v8 "github.com/tommie/v8go" ) +type consoleAPIMessage struct { + Message string + ErrorLevel v8.MessageErrorLevel +} type consoleAPIMessageRecorder struct { - messages []v8.ConsoleAPIMessage + messages []consoleAPIMessage } func (r *consoleAPIMessageRecorder) ConsoleAPIMessage(msg v8.ConsoleAPIMessage) { - r.messages = append(r.messages, msg) + r.messages = append(r.messages, consoleAPIMessage{ + Message: msg.Message, + ErrorLevel: msg.ErrorLevel, + }) } type IsolateWithInspector struct { @@ -61,27 +69,26 @@ func TestMonitorCreateDispose(t *testing.T) { context := iso.NewContext() defer context.Dispose() - _, err := context.RunScript("console.log('Hello, world!'); console.error('Error, world!');", "") + _, err := context.RunScript(` + console.log("Log msg"); + console.info("Info msg"); + console.debug("Debug msg"); + console.warn("Warn msg"); + console.error("Error msg"); + `, "") if err != nil { - t.Error("Error occurred: " + err.Error()) - return + t.Fatal("Error occurred", err) + } + actual := recorder.messages + expected := []consoleAPIMessage{ + {Message: "Log msg", ErrorLevel: v8.ErrorLevelLog}, + {Message: "Info msg", ErrorLevel: v8.ErrorLevelInfo}, + {Message: "Debug msg", ErrorLevel: v8.ErrorLevelDebug}, + {Message: "Warn msg", ErrorLevel: v8.ErrorLevelWarning}, + {Message: "Error msg", ErrorLevel: v8.ErrorLevelError}, } - if len(recorder.messages) != 2 { - t.Error("Expected exactly one message") - } else { - msg1 := recorder.messages[0] - msg2 := recorder.messages[1] - if msg1.ErrorLevel != v8.ErrorLevelLog { - t.Errorf("Expected Log error level. Got %d", msg1.ErrorLevel) - } - if msg2.ErrorLevel != v8.ErrorLevelError { - t.Errorf("Expected Error error level. Got: %d", msg2.ErrorLevel) - } - if msg1.Message != "Hello, world!" { - t.Errorf("Expected Hello, World, got %s", msg1.Message) - } - if msg2.Message != "Error, world!" { - t.Errorf("Expected Error, world!, got %s", msg2.Message) - } + + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("Unexpected messages. \nExpected: %v\nGot: %v", expected, actual) } } From 46df38e2d63fbde0dffb5eb7820ac6ad3b38b2a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B8iman?= Date: Sat, 4 Jan 2025 16:28:57 +0100 Subject: [PATCH 14/23] Remove `ContextPtr` type --- inspector.h | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/inspector.h b/inspector.h index d542edff..5c6fa05e 100644 --- a/inspector.h +++ b/inspector.h @@ -31,13 +31,11 @@ typedef _Bool bool; #include typedef struct m_ctx m_ctx; -typedef m_ctx* ContextPtr; extern v8Inspector* CreateInspector(v8Isolate* iso, v8InspectorClient* client); extern void DeleteInspector(v8Inspector* inspector); -extern void InspectorContextCreated(v8Inspector* inspector, ContextPtr context); -extern void InspectorContextDestroyed(v8Inspector* inspector, - ContextPtr context); +extern void InspectorContextCreated(v8Inspector* inspector, m_ctx* context); +extern void InspectorContextDestroyed(v8Inspector* inspector, m_ctx* context); extern v8InspectorClient* NewInspectorClient(uintptr_t callbackRef); extern void DeleteInspectorClient(v8InspectorClient* client); From de2cf3db3a95c24f0d92986aae9ddc64a84caaf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B8iman?= Date: Sat, 4 Jan 2025 19:14:25 +0100 Subject: [PATCH 15/23] Rename `callbackRef` to `cgoHandle` Represents the purpose of the value --- inspector.cc | 17 ++++++++++++----- inspector.go | 5 ++--- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/inspector.cc b/inspector.cc index ac09426a..e67a33eb 100644 --- a/inspector.cc +++ b/inspector.cc @@ -7,11 +7,18 @@ using namespace v8; using namespace v8_inspector; +/** + * InspectorClient is an implementation v8_inspector::V8InspectorClient that is + * designed to be able to call back to Go code to a specific instance identified + * by a cgo handle. + * + * See also: https://pkg.go.dev/runtime/cgo#Handle + */ class InspectorClient : public V8InspectorClient { - uintptr_t _callbackRef; + uintptr_t _cgoHandle; public: - InspectorClient(uintptr_t callbackRef) { _callbackRef = callbackRef; } + InspectorClient(uintptr_t cgoHandle) { _cgoHandle = cgoHandle; } void consoleAPIMessage(int contextGroupId, v8::Isolate::MessageErrorLevel level, const StringView& message, @@ -40,7 +47,7 @@ void InspectorClient::consoleAPIMessage(int contextGroupId, unsigned columnNumber, V8StackTrace*) { goHandleConsoleAPIMessageCallback( - _callbackRef, contextGroupId, level, ConvertStringView(message), + _cgoHandle, contextGroupId, level, ConvertStringView(message), ConvertStringView(url), lineNumber, columnNumber); } @@ -57,8 +64,8 @@ void DeleteInspector(v8Inspector* inspector) { /********** InspectorClient **********/ -v8InspectorClient* NewInspectorClient(uintptr_t callbackRef) { - return new InspectorClient(callbackRef); +v8InspectorClient* NewInspectorClient(uintptr_t cgoHandle) { + return new InspectorClient(cgoHandle); } void InspectorContextCreated(v8Inspector* inspector, ContextPtr context) { diff --git a/inspector.go b/inspector.go index 744c9539..8809196c 100644 --- a/inspector.go +++ b/inspector.go @@ -142,7 +142,7 @@ func stringViewToString(d C.StringViewData) string { //export goHandleConsoleAPIMessageCallback func goHandleConsoleAPIMessageCallback( - callbackRef C.uintptr_t, + cgoHandle C.uintptr_t, contextGroupId C.int, errorLevel C.int, message C.StringViewData, @@ -150,9 +150,8 @@ func goHandleConsoleAPIMessageCallback( lineNumber C.uint, columnNumber C.uint, ) { - handle := cgo.Handle(callbackRef) + handle := cgo.Handle(cgoHandle) if client, ok := handle.Value().(ConsoleAPIMessageHandler); ok { - // TODO, Stack trace client.ConsoleAPIMessage(ConsoleAPIMessage{ contextGroupId: int(contextGroupId), ErrorLevel: MessageErrorLevel(errorLevel), From ed8be0f57a3bfda34323e5c1d5a830479577c602 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B8iman?= Date: Sat, 4 Jan 2025 19:21:56 +0100 Subject: [PATCH 16/23] Remove dummy "test" string --- inspector.cc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/inspector.cc b/inspector.cc index e67a33eb..eb4d2972 100644 --- a/inspector.cc +++ b/inspector.cc @@ -71,8 +71,7 @@ v8InspectorClient* NewInspectorClient(uintptr_t cgoHandle) { void InspectorContextCreated(v8Inspector* inspector, ContextPtr context) { LOCAL_CONTEXT(context); int groupId = 1; - StringView name = StringView((const uint8_t*)"Test", 4); - V8ContextInfo info = V8ContextInfo(local_ctx, groupId, name); + V8ContextInfo info = V8ContextInfo(local_ctx, groupId, StringView()); inspector->contextCreated(info); } From f336ba1f1890d8e20eb22283d3370ec9e36521f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B8iman?= Date: Sat, 4 Jan 2025 19:30:08 +0100 Subject: [PATCH 17/23] Remove commented line --- inspector.go | 1 - 1 file changed, 1 deletion(-) diff --git a/inspector.go b/inspector.go index 8809196c..6882ebcc 100644 --- a/inspector.go +++ b/inspector.go @@ -71,7 +71,6 @@ type ConsoleAPIMessage struct { Url string LineNumber uint ColumnNumber uint - // stackTrace StackTrace } // A ConsoleAPIMessageHandler will receive JavaScript `console` API calls. From fd8611badbc04469c08dce5a4259e148f0ff5a36 Mon Sep 17 00:00:00 2001 From: tommie Date: Sun, 12 Jan 2025 09:52:03 +0100 Subject: [PATCH 18/23] Fixes grammar in inspector.cc --- inspector.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inspector.cc b/inspector.cc index eb4d2972..d2ecec0f 100644 --- a/inspector.cc +++ b/inspector.cc @@ -8,7 +8,7 @@ using namespace v8; using namespace v8_inspector; /** - * InspectorClient is an implementation v8_inspector::V8InspectorClient that is + * InspectorClient is an implementation of v8_inspector::V8InspectorClient that is * designed to be able to call back to Go code to a specific instance identified * by a cgo handle. * From c6890af82d9c6408aaf7dac1e8f35eacab80146b Mon Sep 17 00:00:00 2001 From: tommie Date: Sun, 12 Jan 2025 09:53:05 +0100 Subject: [PATCH 19/23] Fixes format of CHANGELOG. --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b43e14a..f3911baa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + - Add support to setup an `Inspector`, and `InspectorClient` to receive output from `console` messages in JS code. ### Changed From f2317688349ad157ffdd87cc76d1d626078fc962 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B8iman?= Date: Sun, 12 Jan 2025 11:23:04 +0100 Subject: [PATCH 20/23] Create test for multi-utf16-character --- inspector_test.go | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/inspector_test.go b/inspector_test.go index bc0c7811..a61d3833 100644 --- a/inspector_test.go +++ b/inspector_test.go @@ -61,7 +61,7 @@ func (ctx *ContextWithInspector) Dispose() { ctx.Context.Close() } -func TestMonitorCreateDispose(t *testing.T) { +func TestMonitorConsoleLogLevelt(t *testing.T) { t.Parallel() recorder := consoleAPIMessageRecorder{} iso := NewIsolateWithInspectorClient(&recorder) @@ -92,3 +92,39 @@ func TestMonitorCreateDispose(t *testing.T) { t.Fatalf("Unexpected messages. \nExpected: %v\nGot: %v", expected, actual) } } + +// Verify utf-16 conversion. Internally, the strings are represented by a +// StringView, which is undocumented. Experiements shows that the values +// returned are an utf-16le encoded array, and a length. +// +// The length is assumed to be the size of the array, not the number of +// characters. This test verifies that, by writing a character that needs +// several utf-16 elements for endocing. +// +// https://v8.github.io/api/head/classv8__inspector_1_1StringView.html +func TestMonitorConsoleLogWideCharacters(t *testing.T) { + t.Parallel() + recorder := consoleAPIMessageRecorder{} + iso := NewIsolateWithInspectorClient(&recorder) + defer iso.Dispose() + context := iso.NewContext() + defer context.Dispose() + + _, err := context.RunScript(` + console.log("This character takes up multiple utf-16 values: 𐀀"); + `, "") + if err != nil { + t.Fatal("Error occurred", err) + } + actual := recorder.messages + expected := []consoleAPIMessage{ + { + Message: "This character takes up multiple utf-16 values: 𐀀", + ErrorLevel: v8.ErrorLevelLog, + }, + } + + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("Unexpected messages. \nExpected: %v\nGot: %v", expected, actual) + } +} From 0b5ff8675f23b285187190d93ecc4cd12b3bf63e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B8iman?= Date: Sun, 12 Jan 2025 11:32:15 +0100 Subject: [PATCH 21/23] Add link to Isolate documentation --- inspector.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/inspector.go b/inspector.go index 6882ebcc..db96b101 100644 --- a/inspector.go +++ b/inspector.go @@ -12,6 +12,8 @@ import ( // `console.error`, etc. // // The values reflect the values of v8::Isolate::MessageErrorLevel +// +// See also: https://v8.github.io/api/head/classv8_1_1Isolate.html type MessageErrorLevel uint8 func (lvl MessageErrorLevel) String() string { From 21dec07fa52937dda9b3a4a56afffdd9a4b0c060 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B8iman?= Date: Sun, 12 Jan 2025 11:35:19 +0100 Subject: [PATCH 22/23] Write docs for goHandleConsoleAPIMessageCallback --- inspector.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/inspector.go b/inspector.go index db96b101..9296c656 100644 --- a/inspector.go +++ b/inspector.go @@ -141,6 +141,10 @@ func stringViewToString(d C.StringViewData) string { } } +// goHandleConsoleAPIMessageCallback is called by C code when a console message +// is written. The correct [InspectorClient] is retrieved by the cgoHandle, +// which is a [cgo.Handle]. +// //export goHandleConsoleAPIMessageCallback func goHandleConsoleAPIMessageCallback( cgoHandle C.uintptr_t, From abd4f7ce4c1680498298abd85bf1b104e5ed1e83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B8iman?= Date: Sun, 12 Jan 2025 11:40:05 +0100 Subject: [PATCH 23/23] Add link to C++ API from the API message struct --- inspector.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/inspector.go b/inspector.go index 9296c656..83f44855 100644 --- a/inspector.go +++ b/inspector.go @@ -65,7 +65,12 @@ type InspectorClient struct { // ConsoleAPIMessage contains the information from v8 from console function // calls. // -// V8 also provides a stack trace, which isn't yet supported here. +// The fields correspond to the arguments for the C++ function +// v8_inspector::InspectorClient::consoleAPIMessage +// +// Note: Stack traces are not supported. +// +// See also: https://v8.github.io/api/head/classv8__inspector_1_1V8InspectorClient.html type ConsoleAPIMessage struct { contextGroupId int ErrorLevel MessageErrorLevel