Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get console messages to Go code #77

Merged
merged 23 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions function_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down Expand Up @@ -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
Expand Down
80 changes: 80 additions & 0 deletions inspector.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#include "deps/include/v8-inspector.h"

#include "_cgo_export.h"
#include "context-macros.h"
#include "inspector.h"

using namespace v8;
using namespace v8_inspector;

class InspectorClient : public V8InspectorClient {
uintptr_t _callbackRef;

public:
InspectorClient(uintptr_t callbackRef) { _callbackRef = callbackRef; }
void consoleAPIMessage(int contextGroupId,
tommie marked this conversation as resolved.
Show resolved Hide resolved
v8::Isolate::MessageErrorLevel level,
const StringView& message,
const StringView& url,
unsigned lineNumber,
unsigned columnNumber,
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,
const StringView& url,
unsigned lineNumber,
unsigned columnNumber,
V8StackTrace*) {
goHandleConsoleAPIMessageCallback(
_callbackRef, contextGroupId, level, ConvertStringView(message),
ConvertStringView(url), lineNumber, columnNumber);
}

extern "C" {

v8Inspector* CreateInspector(v8Isolate* iso, v8InspectorClient* client) {
v8Inspector* inspector = V8Inspector::create(iso, client).release();
return inspector;
}

void DeleteInspector(v8Inspector* inspector) {
delete inspector;
}

/********** InspectorClient **********/

v8InspectorClient* NewInspectorClient(uintptr_t callbackRef) {
return new InspectorClient(callbackRef);
}

void InspectorContextCreated(v8Inspector* inspector, ContextPtr context) {
LOCAL_CONTEXT(context);
int groupId = 1;
StringView name = StringView((const uint8_t*)"Test", 4);
tommie marked this conversation as resolved.
Show resolved Hide resolved
V8ContextInfo info = V8ContextInfo(local_ctx, groupId, name);
inspector->contextCreated(info);
}

void InspectorContextDestroyed(v8Inspector* inspector, ContextPtr context) {
LOCAL_CONTEXT(context);
inspector->contextDestroyed(local_ctx);
}

void DeleteInspectorClient(v8InspectorClient* client) {
delete client;
}
}
145 changes: 145 additions & 0 deletions inspector.go
tommie marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package v8go

// #include "inspector.h"
import "C"
import (
"runtime/cgo"
"unicode/utf16"
)

// Represents the level of console output from JavaScript. E.g., `console.log`,
// `console.error`, etc.
type MessageErrorLevel uint8
tommie marked this conversation as resolved.
Show resolved Hide resolved

const (
ErrorLevelLog MessageErrorLevel = 1 << iota
ErrorLevelDebug
ErrorLevelInfo
ErrorLevelError
ErrorLevelWarning
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.v8Inspector
}

// An InspectorClient is the bridge from the [Inspector] to your code.
type InspectorClient struct {
ptr *C.v8InspectorClient
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
Message string
Url string
LineNumber uint
ColumnNumber uint
// stackTrace StackTrace
tommie marked this conversation as resolved.
Show resolved Hide resolved
}

// A ConsoleAPIMessageHandler will receive JavaScript `console` API calls.
type ConsoleAPIMessageHandler interface {
ConsoleAPIMessage(message ConsoleAPIMessage)
}

// 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{
ptr: ptr,
}
}

// 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))
return &InspectorClient{
clientHandle: clientHandle,
ptr: ptr,
}
}

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

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])
tommie marked this conversation as resolved.
Show resolved Hide resolved
}
return string(utf16.Decode(shorts))
}
}

//export goHandleConsoleAPIMessageCallback
func goHandleConsoleAPIMessageCallback(
callbackRef C.uintptr_t,
contextGroupId C.int,
errorLevel C.int,
message C.StringViewData,
url C.StringViewData,
lineNumber C.uint,
columnNumber C.uint,
) {
handle := cgo.Handle(callbackRef)
if client, ok := handle.Value().(ConsoleAPIMessageHandler); ok {
// TODO, Stack trace
client.ConsoleAPIMessage(ConsoleAPIMessage{
contextGroupId: int(contextGroupId),
ErrorLevel: MessageErrorLevel(errorLevel),
Message: stringViewToString(message),
Url: stringViewToString(url),
LineNumber: uint(lineNumber),
ColumnNumber: uint(columnNumber),
})
}
}
55 changes: 55 additions & 0 deletions inspector.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#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 v8Inspector;
typedef v8_inspector::V8InspectorClient v8InspectorClient;

extern "C" {
#else
typedef struct v8Inspector v8Inspector;
typedef struct v8InspectorClient v8InspectorClient;

typedef struct v8Isolate v8Isolate;

typedef _Bool bool;

#endif

#include <stddef.h>
#include <stdint.h>

typedef struct m_ctx m_ctx;
tommie marked this conversation as resolved.
Show resolved Hide resolved
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 v8InspectorClient* NewInspectorClient(uintptr_t callbackRef);
extern void DeleteInspectorClient(v8InspectorClient* client);

typedef struct StringViewData {
bool is8bit;
void const* data;
int length;
} StringViewData;

#ifdef __cplusplus
} // extern "C"
#endif

#endif
87 changes: 87 additions & 0 deletions inspector_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package v8go_test

import (
"testing"

v8 "github.com/tommie/v8go"
)

type consoleAPIMessageRecorder struct {
messages []v8.ConsoleAPIMessage
}

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) {
t.Parallel()
recorder := consoleAPIMessageRecorder{}
iso := NewIsolateWithInspectorClient(&recorder)
defer iso.Dispose()
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())
tommie marked this conversation as resolved.
Show resolved Hide resolved
return
}
if len(recorder.messages) != 2 {
tommie marked this conversation as resolved.
Show resolved Hide resolved
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)
}
}
}
Loading