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

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 18 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
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
86 changes: 86 additions & 0 deletions inspector.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#include "deps/include/v8-inspector.h"

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

using namespace v8;
using namespace v8_inspector;

/**
* 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.
*
* See also: https://pkg.go.dev/runtime/cgo#Handle
*/
class InspectorClient : public V8InspectorClient {
uintptr_t _cgoHandle;

public:
InspectorClient(uintptr_t cgoHandle) { _cgoHandle = cgoHandle; }
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(
_cgoHandle, 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 cgoHandle) {
return new InspectorClient(cgoHandle);
}

void InspectorContextCreated(v8Inspector* inspector, ContextPtr context) {
LOCAL_CONTEXT(context);
int groupId = 1;
V8ContextInfo info = V8ContextInfo(local_ctx, groupId, StringView());
inspector->contextCreated(info);
}

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

void DeleteInspectorClient(v8InspectorClient* client) {
delete client;
}
}
163 changes: 163 additions & 0 deletions inspector.go
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add some documentation for the exported functions and types? Ideally for someone who hasn't (yet) read the C++ docs.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume, you mean the "exported to C" code function? goHandleConsoleAPIMessageCallback? Done!

Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package v8go

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

// 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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great to take the values from the underlying enum, but at least document this is a copy of v8::Isolate::MessageErrorLevel.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. For the values, I adapted how e.g., PropertyAttribute is also implemented.

I think even linking to v8 documentation would make sense, but this doesn't look like a stable URL>

https://v8.github.io/api/head/classv8_1_1Isolate.html#acfc7c4d5c93913201249045e2655d3dd

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, though https://v8.github.io/api/head/classv8_1_1Isolate.html is probably useful on its own.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!


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

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

extern v8Inspector* CreateInspector(v8Isolate* iso, v8InspectorClient* client);
extern void DeleteInspector(v8Inspector* inspector);
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);

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

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

#endif
Loading
Loading