From 6b4ccc3ce4903b8583225229dcbe6025039e7909 Mon Sep 17 00:00:00 2001 From: jviksne Date: Sat, 7 Mar 2020 15:14:43 +0200 Subject: [PATCH] Initial commit --- .gitattributes | 2 + .gitignore | 28 + .travis.yml | 26 + Dockerfile | 42 + LICENSE | 21 + README.md | 152 +++ benchmarks_test.go | 79 ++ cmd/v8-runjs/main.go | 71 ++ doc.go | 23 + docker-v8-lib/Dockerfile | 39 + docker-v8-lib/checkout_v8.sh | 12 + docker-v8-lib/compile_v8.sh | 28 + docker-v8-lib/download_v8.sh | 9 + docker-v8-lib/fatten_archives.sh | 16 + example_bind_test.go | 54 ++ examples_test.go | 203 ++++ kind.go | 191 ++++ kind_test.go | 28 + symlink.sh | 32 + travis-install-linux.sh | 21 + v8.go | 677 +++++++++++++ v8_c_bridge-old.cc.txt | 669 +++++++++++++ v8_c_bridge-old.h | 184 ++++ v8_c_bridge.cpp | 783 +++++++++++++++ v8_c_bridge.h | 216 +++++ v8_create.go | 289 ++++++ v8_go.cpp | 8 + v8_go.h | 16 + v8_test.go | 1543 ++++++++++++++++++++++++++++++ v8console/console.go | 126 +++ v8console/console_snapshot.go | 100 ++ v8console/examples_test.go | 55 ++ 32 files changed, 5743 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 benchmarks_test.go create mode 100644 cmd/v8-runjs/main.go create mode 100644 doc.go create mode 100644 docker-v8-lib/Dockerfile create mode 100644 docker-v8-lib/checkout_v8.sh create mode 100644 docker-v8-lib/compile_v8.sh create mode 100644 docker-v8-lib/download_v8.sh create mode 100644 docker-v8-lib/fatten_archives.sh create mode 100644 example_bind_test.go create mode 100644 examples_test.go create mode 100644 kind.go create mode 100644 kind_test.go create mode 100644 symlink.sh create mode 100644 travis-install-linux.sh create mode 100644 v8.go create mode 100644 v8_c_bridge-old.cc.txt create mode 100644 v8_c_bridge-old.h create mode 100644 v8_c_bridge.cpp create mode 100644 v8_c_bridge.h create mode 100644 v8_create.go create mode 100644 v8_go.cpp create mode 100644 v8_go.h create mode 100644 v8_test.go create mode 100644 v8console/console.go create mode 100644 v8console/console_snapshot.go create mode 100644 v8console/examples_test.go diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6aaa0e9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof +include +libv8 +v8/build/.gclient +v8/build/.gclient_entries diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..019fab7 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,26 @@ +# The default build environment is Ubuntu 12.04 (very old!) which doesn't +# support c++11, which we need for our bindings. Instead, use the newer trusty +# (14.04) distribution. +sudo: required +dist: trusty + +language: go +go: + - "1.10.x" + - "1.9" + +# Indicate which versions of v8 to run the test suite against. +env: + - V8_VERSION=6.3.292.48.1 + # Anything before 6.3 will break because of new ldflags definitions. + # - V8_VERSION=6.2.414.42.1 + # - V8_VERSION=6.0.286.54.3 + +go_import_path: github.com/augustoroman/v8 + +# Need to download & compile v8 libraries before running the tests. +install: ./travis-install-linux.sh + +notifications: + email: + on_failure: change diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..bce8818 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,42 @@ +ARG V8_VERSION +ARG V8_SOURCE_IMAGE=augustoroman/v8-lib + +# ------------ Import the v8 libraries -------------------------------------- +# The v8 library & include files are taken from a pre-built docker image that +# is expected to be called v8-lib. You can build that locally using: +# docker build --build-arg V8_VERSION=6.7.77 --tag augustoroman/v8-lib:6.7.77 docker-v8-lib/ +# or you can use a previously built image from: +# https://hub.docker.com/r/augustoroman/v8-lib/ +# +# Once that is available, build this docker image using: +# docker build --build-arg V8_VERSION=6.7.77 -t v8-runjs . +# and then run the interactive js using: +# docker run -it --rm v8-runjs +FROM ${V8_SOURCE_IMAGE}:${V8_VERSION} as v8 + +# ------------ Build go v8 library and run tests ---------------------------- +FROM golang as builder +# Copy the v8 code from the local disk, similar to: +# RUN go get github.com/augustoroman/v8 ||: +# but this allows using any local modifications. +ARG GO_V8_DIR=/go/src/github.com/augustoroman/v8/ +ADD *.go *.h *.cc $GO_V8_DIR +ADD cmd $GO_V8_DIR/cmd/ +ADD v8console $GO_V8_DIR/v8console/ + +# Copy the pre-compiled library & include files for the desired v8 version. +COPY --from=v8 /v8/lib $GO_V8_DIR/libv8/ +COPY --from=v8 /v8/include $GO_V8_DIR/include/ + +# Install the go code and run tests. +WORKDIR $GO_V8_DIR +RUN go get ./... +RUN go test ./... + +# ------------ Build the final container for v8-runjs ----------------------- +# TODO(aroman) find a smaller container for the executable! For some reason, +# scratch, alpine, and busybox don't work. I wonder if it has something to do +# with cgo? +FROM ubuntu:16.04 +COPY --from=builder /go/bin/v8-runjs /v8-runjs +CMD /v8-runjs diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4c6d68d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 jviksne + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c1875ae --- /dev/null +++ b/README.md @@ -0,0 +1,152 @@ +# V8 Bindings for Go [![Build Status](https://travis-ci.org/augustoroman/v8.svg?branch=master)](https://travis-ci.org/augustoroman/v8) [![Go Report Card](https://goreportcard.com/badge/github.com/augustoroman/v8)](https://goreportcard.com/report/github.com/augustoroman/v8) [![GoDoc](https://godoc.org/github.com/augustoroman/v8?status.svg)](https://godoc.org/github.com/augustoroman/v8) + +The v8 bindings allow a user to execute javascript from within a go executable. + +The bindings are tested to work with several recent v8 builds matching the +Chrome builds 54 - 60 (see the .travis.yml file for specific versions). For +example, Chrome 59 (dev branch) uses v8 5.9.211.4 when this was written. + +Note that v8 releases match the Chrome release timeline: +Chrome 48 corresponds to v8 4.8.\*, Chrome 49 matches v8 4.9.\*. You can see +the table of current chrome and the associated v8 releases at: + +http://omahaproxy.appspot.com/ + +# Using a pre-compiled v8 + +v8 is very slow to compile, it's a large project. If you want to go that route, there are building instructions below. + +Fortunately, there's a project that pre-builds v8 for various platforms. It's packaged as a ruby gem called [libv8](https://rubygems.org/gems/libv8). + +```bash +# Find the appropriate gem version for your OS, +# visit: https://rubygems.org/gems/libv8/versions + +# Download the gem +# MacOS Sierra is darwin-16, for v8 6.3.292.48.1 it looks like: +curl https://rubygems.org/downloads/libv8-6.3.292.48.1-x86_64-darwin-16.gem > libv8.gem + +# Extract the gem (it's a tarball) +tar -xf libv8.gem + +# Extract the `data.tar.gz` within +cd libv8-6.3.292.48.1-x86_64-darwin-16 +tar -xzf data.tar.gz + +# Symlink the compiled libraries and includes +ln -s $(pwd)/data/vendor/v8/include $GOPATH/src/github.com/augustoroman/v8/include +ln -s $(pwd)/data/vendor/v8/out/x64.release $GOPATH/src/github.com/augustoroman/v8/libv8 + +# Run the tests to make sure everything works +cd $GOPATH/src/github.com/augustoroman/v8 +go test +``` + +# Using docker (linux only) + +For linux builds, you can use pre-built libraries or build your own. + +## Pre-built versions + +To use a pre-built library, select the desired v8 version from https://hub.docker.com/r/augustoroman/v8-lib/tags/ and then run: + +```bash +# Select the v8 version to use: +export V8_VERSION=6.7.77 +docker pull augustoroman/v8-lib:$V8_VERSION # Download the image, updating if necessary. +docker rm v8 ||: # Cleanup from before if necessary. +docker run --name v8 augustoroman/v8-lib:$V8_VERSION # Run the image to provide access to the files. +docker cp v8:/v8/include include/ # Copy the include files. +docker cp v8:/v8/lib libv8/ # Copy the library fiels. +``` + +## Build your own via docker + +This takes a lot longer, but is still easy: + +```bash +export V8_VERSION=6.7.77 +docker build --build-arg V8_VERSION=$V8_VERSION --tag augustoroman/v8-lib:$V8_VERSION docker-v8-lib/ +``` + +and then extract the files as above: + +```bash +docker rm v8 ||: # Cleanup from before if necessary. +docker run --name v8 augustoroman/v8-lib:$V8_VERSION # Run the image to provide access to the files. +docker cp v8:/v8/include include/ # Copy the include files. +docker cp v8:/v8/lib libv8/ # Copy the library fiels. +``` + +# Building v8 + +## Prep + +You need to build v8 statically and place it in a location cgo knows about. This requires special tooling and a build directory. Using the [official instructions](https://github.com/v8/v8/wiki/Building-from-Source) as a guide, the general steps of this process are: + +1. `go get` the binding library (this library) +1. Create a v8 build directory +1. [Install depot tools](http://commondatastorage.googleapis.com/chrome-infra-docs/flat/depot_tools/docs/html/depot_tools_tutorial.html#_setting_up) +1. Configure environment +1. Download v8 +1. Build v8 +1. Copy or symlink files to the go library path +1. Build the bindings + +``` +go get github.com/augustoroman/v8 +export V8_GO=$GOPATH/src/github.com/augustoroman/v8 +export V8_BUILD=$V8_GO/v8/build #or wherever you like +mkdir -p $V8_BUILD +cd $V8_BUILD +git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git +export PATH=$PATH:$V8_BUILD/depot_tools +fetch v8 #pull down v8 (this will take some time) +cd v8 +git checkout 6.7.77 +gclient sync +``` + +## Linux + +``` +./build/install-build-deps.sh #only needed once +gn gen out.gn/golib --args="strip_debug_info=true v8_use_external_startup_data=false v8_enable_i18n_support=false v8_enable_gdbjit=false v8_static_library=true symbol_level=0 v8_experimental_extra_library_files=[] v8_extra_library_files=[]" +ninja -C out.gn/golib +# go get some coffee +``` + +## OSX + +``` +gn gen out.gn/golib --args="is_official_build=true strip_debug_info=true v8_use_external_startup_data=false v8_enable_i18n_support=false v8_enable_gdbjit=false v8_static_library=true symbol_level=0 v8_experimental_extra_library_files=[] v8_extra_library_files=[]" +ninja -C out.gn/golib +# go get some coffee +``` + +## Symlinking + +Now you can create symlinks so that cgo can associate the v8 binaries with the go library. + +``` +cd $V8_GO +./symlink.sh $V8_BUILD/v8 +``` + +## Verifying + +You should be done! Try running `go test` + +# Reference + +Also relevant is the v8 API release changes doc: + +https://docs.google.com/document/d/1g8JFi8T_oAE_7uAri7Njtig7fKaPDfotU6huOa1alds/edit + +# Credits + +This work is based off of several existing libraries: + +* https://github.com/fluxio/go-v8 +* https://github.com/kingland/go-v8 +* https://github.com/mattn/go-v8 diff --git a/benchmarks_test.go b/benchmarks_test.go new file mode 100644 index 0000000..116371e --- /dev/null +++ b/benchmarks_test.go @@ -0,0 +1,79 @@ +package v8 + +import "testing" + +func BenchmarkGetValue(b *testing.B) { + ctx := NewIsolate().NewContext() + + _, err := ctx.Eval(`var hello = "test"`, "bench.js") + if err != nil { + b.Fatal(err) + } + + glob := ctx.Global() + + b.ResetTimer() + for n := 0; n < b.N; n++ { + if _, err := glob.Get("hello"); err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkGetNumberValue(b *testing.B) { + ctx := NewIsolate().NewContext() + val, err := ctx.Eval(`(157)`, "bench.js") + if err != nil { + b.Fatal(err) + } + b.ResetTimer() + for n := 0; n < b.N; n += 2 { + if res := val.Int64(); res != 157 { + b.Fatal("Wrong value: ", res) + } + if res := val.Float64(); res != 157 { + b.Fatal("Wrong value: ", res) + } + } +} + +func BenchmarkContextCreate(b *testing.B) { + ctx := NewIsolate().NewContext() + + b.ResetTimer() + for n := 0; n < b.N; n++ { + if _, err := ctx.Create(map[string]interface{}{}); err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkEval(b *testing.B) { + iso := NewIsolate() + ctx := iso.NewContext() + + script := `"hello"` + + b.ResetTimer() + for n := 0; n < b.N; n++ { + if _, err := ctx.Eval(script, "bench-eval.js"); err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkCallback(b *testing.B) { + ctx := NewIsolate().NewContext() + ctx.Global().Set("cb", ctx.Bind("cb", func(in CallbackArgs) (*Value, error) { + return nil, nil + })) + + script := `cb()` + + b.ResetTimer() + for n := 0; n < b.N; n++ { + if _, err := ctx.Eval(script, "bench-cb.js"); err != nil { + b.Fatal(err) + } + } +} diff --git a/cmd/v8-runjs/main.go b/cmd/v8-runjs/main.go new file mode 100644 index 0000000..17afd9d --- /dev/null +++ b/cmd/v8-runjs/main.go @@ -0,0 +1,71 @@ +// v8-runjs is a command-line tool to run javascript. +// +// It's like node, but less useful. +// +// It runs the javascript files provided on the commandline in order until +// it finishes or an error occurs. If no files are provided, this will enter a +// REPL mode where you can interactively run javascript. +// +// Other than the standard javascript environment, it provides console.*: +// console.log, console.info: write args to stdout +// console.warn: write args to stderr in yellow +// console.error: write args to stderr in scary red +// +// Sooo... you can run your JS and print to the screen. +package main + +import ( + "flag" + "fmt" + "io" + "io/ioutil" + "os" + + "github.com/augustoroman/v8" + "github.com/augustoroman/v8/v8console" + "github.com/peterh/liner" +) + +const ( + kRESET = "\033[0m" + kRED = "\033[91m" +) + +func main() { + flag.Parse() + ctx := v8.NewIsolate().NewContext() + v8console.Config{"", os.Stdout, os.Stderr, true}.Inject(ctx) + + for _, filename := range flag.Args() { + data, err := ioutil.ReadFile(filename) + failOnError(err) + _, err = ctx.Eval(string(data), filename) + failOnError(err) + } + + if flag.NArg() == 0 { + s := liner.NewLiner() + s.SetMultiLineMode(true) + defer s.Close() + for { + jscode, err := s.Prompt("> ") + if err == io.EOF { + break + } + failOnError(err) + s.AppendHistory(jscode) + result, err := ctx.Eval(jscode, "") + if err != nil { + fmt.Println(kRED, err, kRESET) + } else { + fmt.Println(result) + } + } + } +} + +func failOnError(err error) { + if err != nil { + panic(err) + } +} diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..e03538f --- /dev/null +++ b/doc.go @@ -0,0 +1,23 @@ +// Package v8 provides a Go API for the the V8 javascript engine. +// +// This allows running javascript within a go executable. The bindings +// have been tested with v8 builds between 5.1.281.16 through 6.7.77. +// +// V8 provides two main concepts for managing javascript state: Isolates and +// Contexts. An isolate represents a single-threaded javascript engine that +// can manage one or more contexts. A context is a sandboxed javascript +// execution environment. +// +// Thus, if you have one isolate, you could safely execute independent code in +// many different contexts created in that isolate. The code in the various +// contexts would not interfere with each other, however no more than one +// context would ever be executing at a given time. +// +// If you have multiple isolates, they may be executing in separate threads +// simultaneously. +// +// This work is based off of several existing libraries: +// * https://github.com/fluxio/go-v8 +// * https://github.com/kingland/go-v8 +// * https://github.com/mattn/go-v8 +package v8 diff --git a/docker-v8-lib/Dockerfile b/docker-v8-lib/Dockerfile new file mode 100644 index 0000000..287eb73 --- /dev/null +++ b/docker-v8-lib/Dockerfile @@ -0,0 +1,39 @@ +# This dockerfile will build v8 as static libraries suitable for being linked +# to the github.com/augustoroman/v8 go library bindings. +# +# The V8_VERSION arg is required and specifies which v8 git version to build. +# The recommended incantation to build this is: +# +# docker build --build-arg V8_VERSION=6.7.77 --tag augustoroman/v8-lib:6.7.77 . +# + +FROM ubuntu:16.04 as builder +# Install the basics we need to compile and install stuff. +RUN apt-get update -qq \ + && apt-get install -y --no-install-recommends \ + ca-certificates build-essential pkg-config git curl python \ + && rm -rf /var/lib/apt/lists/* + +# Download the depot_tools and the basic v8 code. +ARG BUILD_DIR=/build/chromium +ADD download_v8.sh download_v8.sh +RUN ./download_v8.sh + +# Checkout the specific V8_VERSION we want to build. +ARG V8_VERSION +ADD checkout_v8.sh checkout_v8.sh +RUN ./checkout_v8.sh + +# Compile it! +ADD compile_v8.sh compile_v8.sh +RUN ./compile_v8.sh + +# Some V8 versions produce thin archives by default. +ADD fatten_archives.sh fatten_archives.sh +RUN ./fatten_archives.sh + +# Create a clean docker image with only the v8 libs. +FROM tianon/true as lib +ARG BUILD_DIR=/build/chromium +COPY --from=builder ${BUILD_DIR}/v8/out.gn/lib/obj/*.a /v8/lib/ +COPY --from=builder ${BUILD_DIR}/v8/include/ /v8/include/ diff --git a/docker-v8-lib/checkout_v8.sh b/docker-v8-lib/checkout_v8.sh new file mode 100644 index 0000000..bff9d0e --- /dev/null +++ b/docker-v8-lib/checkout_v8.sh @@ -0,0 +1,12 @@ +#!/bin/bash -ex + +: "${V8_VERSION:?V8_VERSION must be set}" +: "${BUILD_DIR:?BUILD_DIR must be set}" + +cd $BUILD_DIR +export PATH="$(pwd)/depot_tools:$PATH" +cd v8 + +git fetch origin ${V8_VERSION} +git checkout ${V8_VERSION} +gclient sync diff --git a/docker-v8-lib/compile_v8.sh b/docker-v8-lib/compile_v8.sh new file mode 100644 index 0000000..db6d336 --- /dev/null +++ b/docker-v8-lib/compile_v8.sh @@ -0,0 +1,28 @@ +#!/bin/bash -ex + +: "${BUILD_DIR:?BUILD_DIR must be set}" + +cd $BUILD_DIR +export PATH="$(pwd)/depot_tools:$PATH" +cd v8 + +gn gen out.gn/lib --args=' + target_cpu = "x64" + is_debug = false + + symbol_level = 0 + strip_debug_info = true + v8_experimental_extra_library_files = [] + v8_extra_library_files = [] + + v8_static_library = true + is_component_build = false + use_custom_libcxx = false + use_custom_libcxx_for_host = false + + icu_use_data_file = false + is_desktop_linux = false + v8_enable_i18n_support = false + v8_use_external_startup_data = false + v8_enable_gdbjit = false' +ninja -C out.gn/lib v8_libbase v8_libplatform v8_base v8_nosnapshot v8_libsampler v8_init v8_initializers diff --git a/docker-v8-lib/download_v8.sh b/docker-v8-lib/download_v8.sh new file mode 100644 index 0000000..451796f --- /dev/null +++ b/docker-v8-lib/download_v8.sh @@ -0,0 +1,9 @@ +#!/bin/bash -ex + +: "${BUILD_DIR:?BUILD_DIR must be set}" + +mkdir -p ${BUILD_DIR} +cd ${BUILD_DIR} +git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git +export PATH="$(pwd)/depot_tools:$PATH" +fetch v8 diff --git a/docker-v8-lib/fatten_archives.sh b/docker-v8-lib/fatten_archives.sh new file mode 100644 index 0000000..c22c35c --- /dev/null +++ b/docker-v8-lib/fatten_archives.sh @@ -0,0 +1,16 @@ +#!/bin/bash -x + +: "${BUILD_DIR:?BUILD_DIR must be set}" + +cd $BUILD_DIR +export PATH="$(pwd)/depot_tools:$PATH" +cd v8 + +cd out.gn/lib/obj + +# Convert any thin archives into fat ones. This will attempt to +# fatten all archives, but we ignore failures if it's already fat +# via the ||: at the end. +for lib in `find . -name '*.a'`; do + ar -t $lib | xargs ar rvs $lib.new && mv -v $lib.new $lib ||: +done diff --git a/example_bind_test.go b/example_bind_test.go new file mode 100644 index 0000000..de64c36 --- /dev/null +++ b/example_bind_test.go @@ -0,0 +1,54 @@ +package v8_test + +import ( + "fmt" + "strconv" + + "github.com/augustoroman/v8" +) + +// AddAllNumbers is the callback function that we'll make accessible the JS VM. +// It will accept 2 or more numbers and return the sum. If fewer than two args +// are passed or any of the args are not parsable as numbers, it will fail. +func AddAllNumbers(in v8.CallbackArgs) (*v8.Value, error) { + if len(in.Args) < 2 { + return nil, fmt.Errorf("add requires at least 2 numbers, but got %d args", len(in.Args)) + } + result := 0.0 + for i, arg := range in.Args { + n, err := strconv.ParseFloat(arg.String(), 64) + if err != nil { + return nil, fmt.Errorf("Arg %d [%q] cannot be parsed as a number: %v", i, arg.String(), err) + } + result += n + } + return in.Context.Create(result) +} + +func ExampleContext_Bind() { + ctx := v8.NewIsolate().NewContext() + + // First, we'll bind our callback function into a *v8.Value that we can + // use as we please. The string "my_add_function" here is the used by V8 as + // the name of the function. That is, we've defined: + // val.toString() = (function my_add_function() { [native code] }); + // However the name "my_add_function" isn't actually accessible in the V8 + // global scope anywhere yet. + val := ctx.Bind("my_add_function", AddAllNumbers) + + // Next we'll set that value into the global context to make it available to + // the JS. + if err := ctx.Global().Set("add", val); err != nil { + panic(err) + } + + // Now we'll call it! + result, err := ctx.Eval(`add(1,2,3,4,5)`, `example.js`) + if err != nil { + panic(err) + } + fmt.Println(`add(1,2,3,4,5) =`, result) + + // output: + // add(1,2,3,4,5) = 15 +} diff --git a/examples_test.go b/examples_test.go new file mode 100644 index 0000000..e6324fd --- /dev/null +++ b/examples_test.go @@ -0,0 +1,203 @@ +package v8_test + +import ( + "fmt" + + "github.com/augustoroman/v8" +) + +func Example() { + // Easy-peasy to create a new VM: + ctx := v8.NewIsolate().NewContext() + + // You can load your js from a file, create it dynamically, whatever. + ctx.Eval(` + // This is javascript code! + add = (a,b)=>{ return a + b }; // whoa, ES6 arrow functions. + `, "add.js") // <-- supply filenames for stack traces + + // State accumulates in a context. Add still exists. + // The last statements' value is returned to Go. + res, _ := ctx.Eval(`add(3,4)`, "compute.js") // don't ignore errors! + fmt.Println("add(3,4) =", res.String()) // I hope it's 7. + + // You can also bind Go functions to javascript: + product := func(in v8.CallbackArgs) (*v8.Value, error) { + var result float64 = 1 + for _, arg := range in.Args { + result *= arg.Float64() + } + return in.Context.Create(result) // ctx.Create is great for mapping Go -> JS. + } + cnt := ctx.Bind("product_function", product) + ctx.Global().Set("product", cnt) + + res, _ = ctx.Eval(` + // Now we can call that function in JS + product(1,2,3,4,5) + `, "compute2.js") + + fmt.Println("product(1,2,3,4,5) =", res.Int64()) + + _, err := ctx.Eval(` + // Sometimes there's a mistake in your js code: + functin broken(a,b) { return a+b; } + `, "ooops.js") + fmt.Println("Err:", err) // <-- get nice error messages + + // output: + // add(3,4) = 7 + // product(1,2,3,4,5) = 120 + // Err: Uncaught exception: SyntaxError: Unexpected identifier + // at ooops.js:3:20 + // functin broken(a,b) { return a+b; } + // ^^^^^^ + // Stack trace: SyntaxError: Unexpected identifier +} + +func Example_microtasks() { + // Microtasks are automatically run when the Eval'd js code has finished but + // before Eval returns. + + ctx := v8.NewIsolate().NewContext() + + // Register a simple log function in js. + ctx.Global().Set("log", ctx.Bind("log", func(in v8.CallbackArgs) (*v8.Value, error) { + fmt.Println("log>", in.Arg(0).String()) + return nil, nil + })) + + // Run some javascript that schedules microtasks, like promises. + output, err := ctx.Eval(` + log('start'); + let p = new Promise(resolve => { // this is called immediately + log('resolve:5'); + resolve(5); + }); + log('promise created'); + p.then(v => log('then:'+v)); // this is scheduled in a microtask + log('done'); // this is run before the microtask + 'xyz' // this is the output of the script + `, "microtasks.js") + + fmt.Println("output:", output) + fmt.Println("err:", err) + + // output: + // log> start + // log> resolve:5 + // log> promise created + // log> done + // log> then:5 + // output: xyz + // err: +} + +func ExampleContext_Create_basic() { + ctx := v8.NewIsolate().NewContext() + + type Info struct{ Name, Email string } + + val, _ := ctx.Create(map[string]interface{}{ + "num": 3.7, + "str": "simple string", + "bool": true, + "struct": Info{"foo", "bar"}, + "list": []int{1, 2, 3}, + }) + + // val is now a *v8.Value that is associated with ctx but not yet accessible + // from the javascript scope. + + _ = ctx.Global().Set("created_value", val) + + res, _ := ctx.Eval(` + created_value.struct.Name = 'John'; + JSON.stringify(created_value.struct) + `, `test.js`) + fmt.Println(res) + + // output: + // {"Name":"John","Email":"bar"} +} + +func ExampleContext_Create_callbacks() { + ctx := v8.NewIsolate().NewContext() + + // A typical use of Create is to return values from callbacks: + var nextId int + getNextIdCallback := func(in v8.CallbackArgs) (*v8.Value, error) { + nextId++ + return ctx.Create(nextId) // Return the created corresponding v8.Value or an error. + } + + // Because Create will use reflection to map a Go value to a JS object, it + // can also be used to easily bind a complex object into the JS VM. + resetIdsCallback := func(in v8.CallbackArgs) (*v8.Value, error) { + nextId = 0 + return nil, nil + } + myIdAPI, _ := ctx.Create(map[string]interface{}{ + "next": getNextIdCallback, + "reset": resetIdsCallback, + // Can also include other stuff: + "my_api_version": "v1.2", + }) + + // now let's use those two callbacks and the api value: + _ = ctx.Global().Set("ids", myIdAPI) + var res *v8.Value + res, _ = ctx.Eval(`ids.my_api_version`, `test.js`) + fmt.Println(`ids.my_api_version =`, res) + res, _ = ctx.Eval(`ids.next()`, `test.js`) + fmt.Println(`ids.next() =`, res) + res, _ = ctx.Eval(`ids.next()`, `test.js`) + fmt.Println(`ids.next() =`, res) + res, _ = ctx.Eval(`ids.reset(); ids.next()`, `test.js`) + fmt.Println(`ids.reset()`) + fmt.Println(`ids.next() =`, res) + + // output: + // ids.my_api_version = v1.2 + // ids.next() = 1 + // ids.next() = 2 + // ids.reset() + // ids.next() = 1 +} + +func ExampleSnapshot() { + snapshot := v8.CreateSnapshot(` + // Concantenate all the scripts you want at startup, e.g. lodash, etc. + _ = { map: function() { /* ... */ }, etc: "etc, etc..." }; + // Setup my per-context global state: + myGlobalState = { + init: function() { this.initialized = true; }, + foo: 3, + }; + // Run some functions: + myGlobalState.init(); + `) + iso := v8.NewIsolateWithSnapshot(snapshot) + + // Create a context with the state from the snapshot: + ctx1 := iso.NewContext() + fmt.Println("Context 1:") + val, _ := ctx1.Eval("myGlobalState.foo = 37; myGlobalState.initialized", "") + fmt.Println("myGlobalState.initialized:", val) + val, _ = ctx1.Eval("myGlobalState.foo", "") + fmt.Println("myGlobalState.foo:", val) + + // In the second context, the global state is reset to the state at the + // snapshot: + ctx2 := iso.NewContext() + fmt.Println("Context 2:") + val, _ = ctx2.Eval("myGlobalState.foo", "") + fmt.Println("myGlobalState.foo:", val) + + // Output: + // Context 1: + // myGlobalState.initialized: true + // myGlobalState.foo: 37 + // Context 2: + // myGlobalState.foo: 3 +} diff --git a/kind.go b/kind.go new file mode 100644 index 0000000..175cb49 --- /dev/null +++ b/kind.go @@ -0,0 +1,191 @@ +package v8 + +import ( + "fmt" + "strings" +) + +// Kind is an underlying V8 representation of a *Value. Javascript values may +// have multiple underyling kinds. For example, a function will be both +// KindObject and KindFunction. +type Kind uint8 + +const ( + KindUndefined Kind = iota + KindNull + KindName + KindString + KindSymbol + KindFunction + KindArray + KindObject + KindBoolean + KindNumber + KindExternal + KindInt32 + KindUint32 + KindDate + KindArgumentsObject + KindBooleanObject + KindNumberObject + KindStringObject + KindSymbolObject + KindNativeError + KindRegExp + KindAsyncFunction + KindGeneratorFunction + KindGeneratorObject + KindPromise + KindMap + KindSet + KindMapIterator + KindSetIterator + KindWeakMap + KindWeakSet + KindArrayBuffer + KindArrayBufferView + KindTypedArray + KindUint8Array + KindUint8ClampedArray + KindInt8Array + KindUint16Array + KindInt16Array + KindUint32Array + KindInt32Array + KindFloat32Array + KindFloat64Array + KindDataView + KindSharedArrayBuffer + KindProxy + KindWebAssemblyCompiledModule + + kNumKinds +) + +var kindStrings = [kNumKinds]string{ + "Undefined", + "Null", + "Name", + "String", + "Symbol", + "Function", + "Array", + "Object", + "Boolean", + "Number", + "External", + "Int32", + "Uint32", + "Date", + "ArgumentsObject", + "BooleanObject", + "NumberObject", + "StringObject", + "SymbolObject", + "NativeError", + "RegExp", + "AsyncFunction", + "GeneratorFunction", + "GeneratorObject", + "Promise", + "Map", + "Set", + "MapIterator", + "SetIterator", + "WeakMap", + "WeakSet", + "ArrayBuffer", + "ArrayBufferView", + "TypedArray", + "Uint8Array", + "Uint8ClampedArray", + "Int8Array", + "Uint16Array", + "Int16Array", + "Uint32Array", + "Int32Array", + "Float32Array", + "Float64Array", + "DataView", + "SharedArrayBuffer", + "Proxy", + "WebAssemblyCompiledModule", +} + +func (k Kind) String() string { + if k >= kNumKinds || k < 0 { + return fmt.Sprintf("NoSuchKind:%d", int(k)) + } + return kindStrings[int(k)] +} + +func (k Kind) mask() kindMask { return kindMask(1 << k) } + +type kindMask uint64 + +// if kNumKinds > 64, then this will fail at compile time. +const compileCheckThatNumKindsBitsFitInKindType = kindMask(1 << kNumKinds) + +func (mask kindMask) Is(k Kind) bool { + return (mask & k.mask()) != 0 +} + +func (mask kindMask) String() string { + var res []string + for k := Kind(0); k < kNumKinds; k++ { + if mask.Is(k) { + res = append(res, k.String()) + } + } + return strings.Join(res, ",") +} + +func mask(kinds ...Kind) kindMask { + var res kindMask + for _, k := range kinds { + res = res | k.mask() + } + return res +} + +// Value kind unions, most values have multiple kinds +const ( + unionKindString = (1 << KindName) | (1 << KindString) + unionKindSymbol = (1 << KindName) | (1 << KindSymbol) + unionKindFunction = (1 << KindObject) | (1 << KindFunction) + unionKindArray = (1 << KindObject) | (1 << KindArray) + unionKindDate = (1 << KindObject) | (1 << KindDate) + unionKindArgumentsObject = (1 << KindObject) | (1 << KindArgumentsObject) + + unionKindBooleanObject = (1 << KindObject) | (1 << KindBooleanObject) + unionKindNumberObject = (1 << KindObject) | (1 << KindNumberObject) + unionKindStringObject = (1 << KindObject) | (1 << KindStringObject) + unionKindSymbolObject = (1 << KindObject) | (1 << KindSymbolObject) + unionKindRegExp = (1 << KindObject) | (1 << KindRegExp) + unionKindPromise = (1 << KindObject) | (1 << KindPromise) + unionKindMap = (1 << KindObject) | (1 << KindMap) + unionKindSet = (1 << KindObject) | (1 << KindSet) + unionKindArrayBuffer = (1 << KindObject) | (1 << KindArrayBuffer) + unionKindUint8Array = (1 << KindObject) | (1 << KindArrayBufferView) | (1 << KindTypedArray) | (1 << KindUint8Array) + unionKindUint8ClampedArray = (1 << KindObject) | (1 << KindArrayBufferView) | (1 << KindTypedArray) | (1 << KindUint8ClampedArray) + unionKindInt8Array = (1 << KindObject) | (1 << KindArrayBufferView) | (1 << KindTypedArray) | (1 << KindInt8Array) + unionKindUint16Array = (1 << KindObject) | (1 << KindArrayBufferView) | (1 << KindTypedArray) | (1 << KindUint16Array) + unionKindInt16Array = (1 << KindObject) | (1 << KindArrayBufferView) | (1 << KindTypedArray) | (1 << KindInt16Array) + unionKindUint32Array = (1 << KindObject) | (1 << KindArrayBufferView) | (1 << KindTypedArray) | (1 << KindUint32Array) + unionKindInt32Array = (1 << KindObject) | (1 << KindArrayBufferView) | (1 << KindTypedArray) | (1 << KindInt32Array) + unionKindFloat32Array = (1 << KindObject) | (1 << KindArrayBufferView) | (1 << KindTypedArray) | (1 << KindFloat32Array) + unionKindFloat64Array = (1 << KindObject) | (1 << KindArrayBufferView) | (1 << KindTypedArray) | (1 << KindFloat64Array) + unionKindDataView = (1 << KindObject) | (1 << KindArrayBufferView) | (1 << KindDataView) + unionKindSharedArrayBuffer = (1 << KindObject) | (1 << KindSharedArrayBuffer) + unionKindProxy = (1 << KindObject) | (1 << KindProxy) + unionKindWeakMap = (1 << KindObject) | (1 << KindWeakMap) + unionKindWeakSet = (1 << KindObject) | (1 << KindWeakSet) + unionKindAsyncFunction = (1 << KindObject) | (1 << KindFunction) | (1 << KindAsyncFunction) + unionKindGeneratorFunction = (1 << KindObject) | (1 << KindFunction) | (1 << KindGeneratorFunction) + unionKindGeneratorObject = (1 << KindObject) | (1 << KindGeneratorObject) + unionKindMapIterator = (1 << KindObject) | (1 << KindMapIterator) + unionKindSetIterator = (1 << KindObject) | (1 << KindSetIterator) + unionKindNativeError = (1 << KindObject) | (1 << KindNativeError) + + unionKindWebAssemblyCompiledModule = (1 << KindObject) | (1 << KindWebAssemblyCompiledModule) +) diff --git a/kind_test.go b/kind_test.go new file mode 100644 index 0000000..191aa48 --- /dev/null +++ b/kind_test.go @@ -0,0 +1,28 @@ +package v8 + +import ( + "testing" +) + +// This hard-codes a handful of kind <-> string mappings to ensure that our +// kind enum and kind string array are matched up. +func TestKindString(t *testing.T) { + testcases := []struct { + kind Kind + str string + }{ + {KindUndefined, "Undefined"}, + {KindNativeError, "NativeError"}, + {KindRegExp, "RegExp"}, + {KindWebAssemblyCompiledModule, "WebAssemblyCompiledModule"}, + + // Verify that we have N kinds and they are stringified reasonably. + {kNumKinds, "NoSuchKind:47"}, + } + for _, test := range testcases { + if test.kind.String() != test.str { + t.Errorf("Expected kind %q (%d) to stringify to %q", + test.kind, test.kind, test.str) + } + } +} diff --git a/symlink.sh b/symlink.sh new file mode 100644 index 0000000..6067d13 --- /dev/null +++ b/symlink.sh @@ -0,0 +1,32 @@ +#!/bin/bash -e + +if [[ "$1" == "-h" || -z "$1" ]]; then + echo "Usage: `basename $0` /path/to/chromium/v8" + echo "" + echo "This will create symlinks for the libv8/ and include/ directories necessary" + echo "to build the v8 Go package. The path should be the v8 directory with the" + echo "compiled libraries (see build instructions)." + exit 0 +fi + +PKG_DIR=`dirname $0` +V8_DIR=${1%/} +cd ${PKG_DIR} + +# Make sure that the specified include dir exists. This could happen if you +# specify a relative directory that isn't right after cd'ing to PKG_DIR. +if [[ ! -d "${V8_DIR}/include" ]]; then + echo "ERROR: ${V8_DIR}/include does not exist." >&2 + exit 1 +fi + +V8_LIBS="out.gn/golib/obj" + +if [[ ! -d "${V8_DIR}/${V8_LIBS}" ]]; then + echo "ERROR: ${V8_DIR}/${V8_LIBS} directory does not exist." >&2 + exit 1 +fi + +set -x +e +ln -s ${V8_DIR}/${V8_LIBS} libv8 +ln -s ${V8_DIR}/include include diff --git a/travis-install-linux.sh b/travis-install-linux.sh new file mode 100644 index 0000000..34339ab --- /dev/null +++ b/travis-install-linux.sh @@ -0,0 +1,21 @@ +#!/bin/bash -ex +# +# This script is used to download and install the v8 libraries on linux for +# travis-ci. +# + +: "${V8_VERSION:?V8_VERSION must be set}" + +V8_DIR=${HOME}/libv8gem +mkdir -p ${V8_DIR} +pushd ${V8_DIR} + +curl https://rubygems.org/downloads/libv8-${V8_VERSION}-x86_64-linux.gem | tar xv +tar xzvf data.tar.gz + +popd + +ln -s ${V8_DIR}/vendor/v8/out/x64.release libv8 +ln -s ${V8_DIR}/vendor/v8/include include + +go get ./... diff --git a/v8.go b/v8.go new file mode 100644 index 0000000..e90fd15 --- /dev/null +++ b/v8.go @@ -0,0 +1,677 @@ +package v8 + +// Reference materials: +// https://developers.google.com/v8/embed#accessors +// https://developers.google.com/v8/embed#exceptions +// https://docs.google.com/document/d/1g8JFi8T_oAE_7uAri7Njtig7fKaPDfotU6huOa1alds/edit +// TODO: +// Value.Export(v) --> inverse of Context.Create() +// Proxy objects + +// BUG(aroman) Unhandled promise rejections are silently dropped +// (see https://github.com/augustoroman/v8/issues/21) + +// augustoroman/v8 - original - # cgo LDFLAGS: -pthread -L${SRCDIR}/libv8 -lv8_base -lv8_init -lv8_initializers -lv8_libbase -lv8_libplatform -lv8_libsampler -lv8_nosnapshot +// https://stackoverflow.com/questions/35856693/can-i-import-a-golang-package-based-on-the-os-im-building-for + +// #include +// #include +// #include "v8_c_bridge.h" +// #include "v8_go.h" +// +// #cgo CXXFLAGS: -I${SRCDIR} -I${SRCDIR}/include -fno-rtti -fpic -std=c++11 +// #cgo windows LDFLAGS: -pthread -L${SRCDIR}/libv8 -lv8_c_bridge +// #cgo linux,darwin LDFLAGS: -pthread -L${SRCDIR}/libv8 -lfuzzer_support -ljson_fuzzer -llib_wasm_fuzzer_common -lmulti_return_fuzzer -lparser_fuzzer -lregexp_builtins_fuzzer -lregexp_fuzzer -ltorque_base -ltorque_generated_definitions -ltorque_generated_initializers -ltorque_ls_base -lv8_base_without_compiler_0 -lv8_base_without_compiler_1 -lv8_compiler -lv8_compiler_opt -lv8_init -lv8_initializers -lv8_libbase -lv8_libplatform -lv8_libsampler -lv8_snapshot -lwasm_async_fuzzer -lwasm_code_fuzzer -lwasm_compile_fuzzer -lwasm_fuzzer -lwasm_module_runner -lwee8 -licui18n -licuuc -linspector -linspector_string_conversions +import "C" + +import ( + "errors" + "fmt" + "runtime" + "strconv" + "strings" + "sync" + "time" + "unsafe" +) + +func v8Init() { + //C.v8_Init(unsafe.Pointer(goCallbackHandler)) + C.initGoCallbackHanlder() // defined in v8_go.h, implemented in v8_go.cc, references goCallbackHandler function implemented in this file. +} + +// Callback is the signature for callback functions that are registered with a +// V8 context via Bind(). Never return a Value from a different V8 isolate. A +// return value of nil will return "undefined" to javascript. Returning an +// error will throw an exception. Panics are caught and returned as errors to +// avoid disrupting the cgo stack. +type Callback func(CallbackArgs) (*Value, error) + +// CallbackArgs provide the context for handling a javascript callback into go. +// Caller is the script location that javascript is calling from. If the +// function is called directly from Go (e.g. via Call()), then "Caller" will be +// empty. Args are the arguments provided by the JS code. Context is the V8 +// context that initiated the call. +type CallbackArgs struct { + Caller Loc + Args []*Value + Context *Context +} + +// Arg returns the specified argument or "undefined" if it doesn't exist. +func (c *CallbackArgs) Arg(n int) *Value { + if n < len(c.Args) && n >= 0 { + return c.Args[n] + } + undef, _ := c.Context.Create(nil) + return undef +} + +// Loc defines a script location. +type Loc struct { + Funcname, Filename string + Line, Column int +} + +// Version exposes the compiled-in version of the linked V8 library. This can +// be used to test for specific javascript functionality support (e.g. ES6 +// destructuring isn't supported before major version 5.). +var Version = struct{ Major, Minor, Build, Patch int }{ + Major: int(C.version.Major), + Minor: int(C.version.Minor), + Build: int(C.version.Build), + Patch: int(C.version.Patch), +} + +// PromiseState defines the state of a promise: either pending, resolved, or +// rejected. Promises that are pending have no result value yet. A promise that +// is resolved has a result value, and a promise that is rejected has a result +// value that is usually the error. +type PromiseState uint8 + +const ( + PromiseStatePending PromiseState = iota + PromiseStateResolved + PromiseStateRejected + kNumPromiseStates +) + +var promiseStateStrings = [kNumPromiseStates]string{"Pending", "Resolved", "Rejected"} + +func (s PromiseState) String() string { + if s < 0 || s >= kNumPromiseStates { + return fmt.Sprintf("InvalidPromiseState:%d", int(s)) + } + return promiseStateStrings[s] +} + +// Ensure that v8 is initialized exactly once on first use. +var v8InitOnce sync.Once + +// Snapshot contains the stored VM state that can be used to quickly recreate a +// new VM at that particular state. +type Snapshot struct{ data C.StartupData } + +func newSnapshot(data C.StartupData) *Snapshot { + s := &Snapshot{data} + runtime.SetFinalizer(s, (*Snapshot).release) + return s +} + +func (s *Snapshot) release() { + if s.data.ptr != nil { + //C.free(unsafe.Pointer(s.data.ptr)) + C.v8_Free(unsafe.Pointer(s.data.ptr)) + } + s.data.ptr = nil + s.data.len = 0 + runtime.SetFinalizer(s, nil) +} + +// Export returns the VM state data as a byte slice. +func (s *Snapshot) Export() []byte { + return []byte(C.GoStringN(s.data.ptr, s.data.len)) +} + +// RestoreSnapshotFromExport creates a Snapshot from a byte slice that should +// have previous come from Snapshot.Export(). +func RestoreSnapshotFromExport(data []byte) *Snapshot { + str := C.String{ + ptr: (*C.char)(C.malloc(C.size_t(len(data)))), + len: C.int(len(data)), + } + C.memcpy(unsafe.Pointer(str.ptr), unsafe.Pointer(&data[0]), C.size_t(len(data))) + return newSnapshot(str) +} + +/* +// CreateSnapshot creates a new Snapshot after running the supplied JS code. +// Because Snapshots cannot have refences to external code (no Go callbacks), +// all of the initialization code must be pure JS and supplied at once as the +// arg to this function. +func CreateSnapshot(js string) *Snapshot { + v8InitOnce.Do(func() { v8Init() }) + js_ptr := C.CString(js) + defer C.free(unsafe.Pointer(js_ptr)) + return newSnapshot(C.v8_CreateSnapshotDataBlob(js_ptr)) +} +*/ + +// Isolate represents a single-threaded V8 engine instance. It can run multiple +// independent Contexts and V8 values can be freely shared between the Contexts, +// however only one context will ever execute at a time. +type Isolate struct { + ptr C.IsolatePtr + s *Snapshot // make sure not to be advanced GC +} + +// NewIsolate creates a new V8 Isolate. +func NewIsolate() *Isolate { + v8InitOnce.Do(func() { v8Init() }) + iso := &Isolate{ptr: C.v8_Isolate_New(C.StartupData{ptr: nil, len: 0})} + runtime.SetFinalizer(iso, (*Isolate).release) + return iso +} + +// NewIsolateWithSnapshot creates a new V8 Isolate using the supplied Snapshot +// to initialize all Contexts created from this Isolate. +func NewIsolateWithSnapshot(s *Snapshot) *Isolate { + v8InitOnce.Do(func() { v8Init() }) + iso := &Isolate{ptr: C.v8_Isolate_New(s.data), s: s} + runtime.SetFinalizer(iso, (*Isolate).release) + return iso +} + +// NewContext creates a new, clean V8 Context within this Isolate. +func (i *Isolate) NewContext() *Context { + ctx := &Context{ + iso: i, + ptr: C.v8_Isolate_NewContext(i.ptr), + callbacks: map[int]callbackInfo{}, + } + + contextsMutex.Lock() + nextContextId++ + ctx.id = nextContextId + contextsMutex.Unlock() + + runtime.SetFinalizer(ctx, (*Context).release) + + return ctx +} + +// Terminate will interrupt all operation in this Isolate, interrupting any +// Contexts that are executing. This may be called from any goroutine at any +// time. +func (i *Isolate) Terminate() { C.v8_Isolate_Terminate(i.ptr) } +func (i *Isolate) release() { + C.v8_Isolate_Release(i.ptr) + i.ptr = nil + runtime.SetFinalizer(i, nil) +} + +func (i *Isolate) convertErrorMsg(error_msg C.Error) error { + if error_msg.ptr == nil { + return nil + } + err := errors.New(C.GoStringN(error_msg.ptr, error_msg.len)) + //C.free(unsafe.Pointer(error_msg.ptr)) + C.v8_Free(unsafe.Pointer(error_msg.ptr)) + return err +} + +// Context is a sandboxed js environment with its own set of built-in objects +// and functions. Values and javascript operations within a context are visible +// only within that context unless the Go code explicitly moves values from one +// context to another. +type Context struct { + id int + iso *Isolate + ptr C.ContextPtr + + callbacks map[int]callbackInfo + nextCallbackId int +} +type callbackInfo struct { + Callback + name string +} + +func (ctx *Context) split(ret C.ValueTuple) (*Value, error) { + return ctx.newValue(ret.Value, ret.Kinds), ctx.iso.convertErrorMsg(ret.error_msg) +} + +// Eval runs the javascript code in the VM. The filename parameter is +// informational only -- it is shown in javascript stack traces. +func (ctx *Context) Eval(jsCode, filename string) (*Value, error) { + js_code_cstr := C.CString(jsCode) + filename_cstr := C.CString(filename) + addRef(ctx) + ret := C.v8_Context_Run(ctx.ptr, js_code_cstr, filename_cstr) + decRef(ctx) + C.free(unsafe.Pointer(js_code_cstr)) + C.free(unsafe.Pointer(filename_cstr)) + return ctx.split(ret) +} + +// Bind creates a V8 function value that calls a Go function when invoked. This +// value is created but NOT visible in the Context until it is explicitly passed +// to the Context (either via a .Set() call or as a callback return value). +// +// The name that is provided is the name of the defined javascript function, and +// generally doesn't affect anything. That is, for a call such as: +// +// val, _ = ctx.Bind("my_func_name", callback) +// +// then val is a function object in javascript, so calling val.String() (or +// calling .toString() on the object within the JS VM) would result in: +// +// function my_func_name() { [native code] } +// +// NOTE: Once registered, a callback function will be stored in the Context +// until it is GC'd, so each Bind for a given context will take up a little +// more memory each time. Normally this isn't a problem, but many many Bind's +// on a Context can gradually consume memory. +func (ctx *Context) Bind(name string, cb Callback) *Value { + ctx.nextCallbackId++ + id := ctx.nextCallbackId + ctx.callbacks[id] = callbackInfo{cb, name} + cbIdStr := C.CString(fmt.Sprintf("%d:%d", ctx.id, id)) + defer C.free(unsafe.Pointer(cbIdStr)) + nameStr := C.CString(name) + defer C.free(unsafe.Pointer(nameStr)) + return ctx.newValue( + C.v8_Context_RegisterCallback(ctx.ptr, nameStr, cbIdStr), + unionKindFunction, + ) +} + +// Global returns the JS global object for this context, with properties like +// Object, Array, JSON, etc. +func (ctx *Context) Global() *Value { + return ctx.newValue(C.v8_Context_Global(ctx.ptr), C.KindMask(KindObject)) +} +func (ctx *Context) release() { + if ctx.ptr != nil { + C.v8_Context_Release(ctx.ptr) + } + ctx.ptr = nil + + contextsMutex.Lock() + delete(contexts, ctx.id) + contextsMutex.Unlock() + + runtime.SetFinalizer(ctx, nil) + ctx.iso = nil // Allow the isolate to be GC'd if we're the last ptr to it. +} + +// Terminate will interrupt any processing going on in the context. This may +// be called from any goroutine. +func (ctx *Context) Terminate() { ctx.iso.Terminate() } +func (ctx *Context) newValue(ptr C.PersistentValuePtr, kinds C.KindMask) *Value { + if ptr == nil { + return nil + } + + val := &Value{ctx, ptr, kindMask(kinds)} + runtime.SetFinalizer(val, (*Value).release) + return val +} + +// ParseJson uses V8's JSON.parse to parse the string and return the parsed +// object. +func (ctx *Context) ParseJson(json string) (*Value, error) { + var json_parse *Value + if json, err := ctx.Global().Get("JSON"); err != nil { + return nil, fmt.Errorf("Cannot get JSON: %v", err) + } else if json_parse, err = json.Get("parse"); err != nil { + return nil, fmt.Errorf("Cannot get JSON.parse: %v", err) + } + str, err := ctx.Create(json) + if err != nil { + return nil, err + } + return json_parse.Call(json_parse, str) +} + +// Value represents a handle to a value within the javascript VM. Values are +// associated with a particular Context, but may be passed freely between +// Contexts within an Isolate. +type Value struct { + ctx *Context + ptr C.PersistentValuePtr + kindMask kindMask +} + +// Bytes returns a byte slice extracted from this value when the value +// is of type ArrayBuffer. The returned byte slice is copied from the underlying +// buffer, so modifying it will not be reflected in the VM. +// Values of other types return nil. +func (v *Value) Bytes() []byte { + mem := C.v8_Value_Bytes(v.ctx.ptr, v.ptr) + if mem.ptr == nil { + return nil + } + ret := make([]byte, mem.len) + copy(ret, ((*[1 << 30]byte)(unsafe.Pointer(mem.ptr)))[:mem.len:mem.len]) + // NOTE: We don't free the memory here: It's owned by V8. + return ret +} + +// Float64 returns this Value as a float64. If this value is not a number, +// then NaN will be returned. +func (v *Value) Float64() float64 { + return float64(C.v8_Value_Float64(v.ctx.ptr, v.ptr)) +} + +// Int64 returns this Value as an int64. If this value is not a number, +// then 0 will be returned. +func (v *Value) Int64() int64 { + return int64(C.v8_Value_Int64(v.ctx.ptr, v.ptr)) +} + +// Bool returns this Value as a boolean. If the underlying value is not a +// boolean, it will be coerced to a boolean using Javascript's coercion rules. +func (v *Value) Bool() bool { + return C.v8_Value_Bool(v.ctx.ptr, v.ptr) == 1 +} + +// Date returns this Value as a time.Time. If the underlying value is not a +// KindDate, this will return an error. +func (v *Value) Date() (time.Time, error) { + if !v.IsKind(KindDate) { + return time.Time{}, errors.New("Not a date") + } + msec := v.Int64() + sec := msec / 1000 + nsec := (msec % 1000) * 1e6 + return time.Unix(sec, nsec), nil +} + +// PromiseInfo will return information about the promise if this value's +// underlying kind is KindPromise, otherwise it will return an error. If there +// is no error, then the returned value will depend on the promise state: +// pending: nil +// fulfilled: the value of the promise +// rejected: the rejected result, usually a JS error +func (v *Value) PromiseInfo() (PromiseState, *Value, error) { + if !v.IsKind(KindPromise) { + return 0, nil, errors.New("Not a promise") + } + var state C.int + val, err := v.ctx.split(C.v8_Value_PromiseInfo(v.ctx.ptr, v.ptr, &state)) + return PromiseState(state), val, err +} + +// String returns the string representation of the value using the ToString() +// method. For primitive types this is just the printable value. For objects, +// this is "[object Object]". Functions print the function definition. +func (v *Value) String() string { + cstr := C.v8_Value_String(v.ctx.ptr, v.ptr) + str := C.GoStringN(cstr.ptr, cstr.len) + //C.free(unsafe.Pointer(cstr.ptr)) + C.v8_Free(unsafe.Pointer(cstr.ptr)) + return str +} + +// Get a field from the object. If this value is not an object, this will fail. +func (v *Value) Get(name string) (*Value, error) { + name_cstr := C.CString(name) + ret := C.v8_Value_Get(v.ctx.ptr, v.ptr, name_cstr) + C.free(unsafe.Pointer(name_cstr)) + return v.ctx.split(ret) +} + +// Get the value at the specified index. If this value is not an object or an +// array, this will fail. +func (v *Value) GetIndex(idx int) (*Value, error) { + return v.ctx.split(C.v8_Value_GetIdx(v.ctx.ptr, v.ptr, C.int(idx))) +} + +// Set a field on the object. If this value is not an object, this +// will fail. +func (v *Value) Set(name string, value *Value) error { + name_cstr := C.CString(name) + errmsg := C.v8_Value_Set(v.ctx.ptr, v.ptr, name_cstr, value.ptr) + C.free(unsafe.Pointer(name_cstr)) + return v.ctx.iso.convertErrorMsg(errmsg) +} + +// SetIndex sets the object's value at the specified index. If this value is +// not an object or an array, this will fail. +func (v *Value) SetIndex(idx int, value *Value) error { + return v.ctx.iso.convertErrorMsg( + C.v8_Value_SetIdx(v.ctx.ptr, v.ptr, C.int(idx), value.ptr)) +} + +// Call this value as a function. If this value is not a function, this will +// fail. +func (v *Value) Call(this *Value, args ...*Value) (*Value, error) { + // always allocate at least one so &argPtrs[0] works. + argPtrs := make([]C.PersistentValuePtr, len(args)+1) + for i := range args { + argPtrs[i] = args[i].ptr + } + var thisPtr C.PersistentValuePtr + if this != nil { + thisPtr = this.ptr + } + addRef(v.ctx) + result := C.v8_Value_Call(v.ctx.ptr, v.ptr, thisPtr, C.int(len(args)), &argPtrs[0]) + decRef(v.ctx) + return v.ctx.split(result) +} + +// IsKind will test whether the underlying value is the specified JS kind. +// The kind of a value is set when the value is created and will not change. +func (v *Value) IsKind(k Kind) bool { + return v.kindMask.Is(k) +} + +// New creates a new instance of an object using this value as its constructor. +// If this value is not a function, this will fail. +func (v *Value) New(args ...*Value) (*Value, error) { + // always allocate at least one so &argPtrs[0] works. + argPtrs := make([]C.PersistentValuePtr, len(args)+1) + for i := range args { + argPtrs[i] = args[i].ptr + } + addRef(v.ctx) + result := C.v8_Value_New(v.ctx.ptr, v.ptr, C.int(len(args)), &argPtrs[0]) + decRef(v.ctx) + return v.ctx.split(result) +} + +func (v *Value) release() { + if v.ptr != nil { + C.v8_Value_Release(v.ctx.ptr, v.ptr) + } + v.ctx = nil + v.ptr = nil + runtime.SetFinalizer(v, nil) +} + +// MarshalJSON implements the json.Marshaler interface using the JSON.stringify +// function from the VM to serialize the value and fails if that cannot be +// found. +// +// Note that JSON.stringify will ignore function values. For example, this JS +// object: +// { foo: function() { return "x" }, bar: 3 } +// will serialize to this: +// {"bar":3} +func (v *Value) MarshalJSON() ([]byte, error) { + var json_stringify *Value + if json, err := v.ctx.Global().Get("JSON"); err != nil { + return nil, fmt.Errorf("Cannot get JSON object: %v", err) + } else if json_stringify, err = json.Get("stringify"); err != nil { + return nil, fmt.Errorf("Cannot get JSON.stringify: %v", err) + } + res, err := json_stringify.Call(json_stringify, v) + if err != nil { + return nil, fmt.Errorf("Failed to stringify val: %v", err) + } + return []byte(res.String()), nil +} + +// +// callback magic +// + +// Because of the rules of Go <--> C pointer interchange +// (https://golang.org/cmd/cgo/#hdr-Passing_pointers), we can't pass a *Context +// pointer into the C code. That means that when V8 wants to execute a callback +// back into the Go code, we have to find some other way of determining which +// context the callback was associated with. +// +// One way (as described at https://github.com/golang/go/wiki/cgo) is to create +// a registry that keeps the pointers all in Go and uses an arbitrary numeric +// handle to pass to C instead. +// +// One tricky side-affect is that this holds a pointer to our Context. Well, +// that's obvious, right? But that means our Context can't be GC'd. Oops. +// +// To work around this, we'll dynamically create the registered entry each time +// we call into V8 and remove when we're done. Specifically, we'll use a ref +// count just in case somebody gets cute and calls back into V8 from a callback. +// +var contexts = map[int]*refCount{} +var contextsMutex sync.RWMutex +var nextContextId int + +type refCount struct { + ptr *Context + count int +} + +func addRef(ctx *Context) { + contextsMutex.Lock() + ref := contexts[ctx.id] + if ref == nil { + ref = &refCount{ctx, 0} + contexts[ctx.id] = ref + } + ref.count++ + contextsMutex.Unlock() +} +func decRef(ctx *Context) { + contextsMutex.Lock() + ref := contexts[ctx.id] + if ref == nil || ref.count <= 1 { + delete(contexts, ctx.id) + } else { + ref.count-- + } + contextsMutex.Unlock() +} + +//export goCallbackHandler +func goCallbackHandler( + cbIdStr C.String, + caller C.CallerInfo, + argc C.int, + argvptr *C.ValueTuple, +) (ret C.ValueTuple) { + caller_loc := Loc{ + Funcname: C.GoStringN(caller.Funcname.ptr, caller.Funcname.len), + Filename: C.GoStringN(caller.Filename.ptr, caller.Filename.len), + Line: int(caller.Line), + Column: int(caller.Column), + } + + cbId := C.GoStringN(cbIdStr.ptr, cbIdStr.len) + parts := strings.SplitN(cbId, ":", 2) + ctxId, _ := strconv.Atoi(parts[0]) + callbackId, _ := strconv.Atoi(parts[1]) + + contextsMutex.RLock() + ref := contexts[ctxId] + if ref == nil { + panic(fmt.Errorf( + "Missing context pointer during callback for context #%d", ctxId)) + } + ctx := ref.ptr + contextsMutex.RUnlock() + + info := ctx.callbacks[int(callbackId)] + if info.Callback == nil { + // Everything is bad -- this should never happen. + panic(fmt.Errorf("No such registered callback: %s", info.name)) + } + + // Convert array of args into a slice. See: + // https://github.com/golang/go/wiki/cgo + // and + // http://play.golang.org/p/XuC0xqtAIC + argv := (*[1 << 30]C.ValueTuple)(unsafe.Pointer(argvptr))[:argc:argc] + + args := make([]*Value, argc) + for i := 0; i < int(argc); i++ { + args[i] = ctx.newValue(argv[i].Value, argv[i].Kinds) + } + + // Catch panics -- if they are uncaught, they skip past the C stack and + // continue straight through to the go call, wreaking havoc with the C + // state. + defer func() { + if v := recover(); v != nil { + errmsg := fmt.Sprintf("Panic during callback %q: %v", info.name, v) + ret.error_msg = C.Error{ptr: C.CString(errmsg), len: C.int(len(errmsg))} + } + }() + + res, err := info.Callback(CallbackArgs{caller_loc, args, ctx}) + + if err != nil { + errmsg := err.Error() + e := C.Error{ptr: C.CString(errmsg), len: C.int(len(errmsg))} + return C.ValueTuple{nil, 0, e} + } + + if res == nil { + return C.ValueTuple{} + } else if res.ctx.iso.ptr != ctx.iso.ptr { + errmsg := fmt.Sprintf("Callback %s returned a value from another isolate.", info.name) + e := C.Error{ptr: C.CString(errmsg), len: C.int(len(errmsg))} + return C.ValueTuple{nil, 0, e} + } + + return C.ValueTuple{Value: res.ptr} +} + +// HeapStatistics represent v8::HeapStatistics which are statistics +// about the heap memory usage. +type HeapStatistics struct { + TotalHeapSize uint64 + TotalHeapSizeExecutable uint64 + TotalPhysicalSize uint64 + TotalAvailableSize uint64 + UsedHeapSize uint64 + HeapSizeLimit uint64 + MallocedMemory uint64 + PeakMallocedMemory uint64 + DoesZapGarbage bool +} + +// GetHeapStatistics gets statistics about the heap memory usage. +func (i *Isolate) GetHeapStatistics() HeapStatistics { + hs := C.v8_Isolate_GetHeapStatistics(i.ptr) + return HeapStatistics{ + TotalHeapSize: uint64(hs.total_heap_size), + TotalHeapSizeExecutable: uint64(hs.total_heap_size_executable), + TotalPhysicalSize: uint64(hs.total_physical_size), + TotalAvailableSize: uint64(hs.total_available_size), + UsedHeapSize: uint64(hs.used_heap_size), + HeapSizeLimit: uint64(hs.heap_size_limit), + MallocedMemory: uint64(hs.malloced_memory), + PeakMallocedMemory: uint64(hs.peak_malloced_memory), + DoesZapGarbage: hs.does_zap_garbage == 1, + } +} + +// SendLowMemoryNotification sends an optional notification that the +// system is running low on memory. V8 uses these notifications to +// attempt to free memory. +func (i *Isolate) SendLowMemoryNotification() { + C.v8_Isolate_LowMemoryNotification(i.ptr) +} diff --git a/v8_c_bridge-old.cc.txt b/v8_c_bridge-old.cc.txt new file mode 100644 index 0000000..69f93f3 --- /dev/null +++ b/v8_c_bridge-old.cc.txt @@ -0,0 +1,669 @@ +#include "v8_c_bridge.h" + +#include "libplatform/libplatform.h" +#include "v8.h" + +#include +#include +#include +#include +#include + +#define ISOLATE_SCOPE(iso) \ + v8::Isolate* isolate = (iso); \ + v8::Locker locker(isolate); /* Lock to current thread. */ \ + v8::Isolate::Scope isolate_scope(isolate); /* Assign isolate to this thread. */ + + +#define VALUE_SCOPE(ctxptr) \ + ISOLATE_SCOPE(static_cast(ctxptr)->isolate) \ + v8::HandleScope handle_scope(isolate); /* Create a scope for handles. */ \ + v8::Local ctx(static_cast(ctxptr)->ptr.Get(isolate)); \ + v8::Context::Scope context_scope(ctx); /* Scope to this context. */ + +extern "C" ValueTuple go_callback_handler( + String id, CallerInfo info, int argc, ValueTuple* argv); + +// We only need one, it's stateless. +auto allocator = v8::ArrayBuffer::Allocator::NewDefaultAllocator(); + +typedef struct { + v8::Persistent ptr; + v8::Isolate* isolate; +} Context; + +typedef v8::Persistent Value; + +String DupString(const v8::String::Utf8Value& src) { + char* data = static_cast(malloc(src.length())); + memcpy(data, *src, src.length()); + return (String){data, src.length()}; +} +String DupString(const v8::Local& val) { + return DupString(v8::String::Utf8Value(val)); +} +String DupString(const char* msg) { + const char* data = strdup(msg); + return (String){data, int(strlen(msg))}; +} +String DupString(const std::string& src) { + char* data = static_cast(malloc(src.length())); + memcpy(data, src.data(), src.length()); + return (String){data, int(src.length())}; +} + +KindMask v8_Value_KindsFromLocal(v8::Local value) { + KindMask kinds = 0; + + if (value->IsUndefined()) kinds |= (1ULL << Kind::kUndefined ); + if (value->IsNull()) kinds |= (1ULL << Kind::kNull ); + if (value->IsName()) kinds |= (1ULL << Kind::kName ); + if (value->IsString()) kinds |= (1ULL << Kind::kString ); + if (value->IsSymbol()) kinds |= (1ULL << Kind::kSymbol ); + if (value->IsObject()) kinds |= (1ULL << Kind::kObject ); + if (value->IsArray()) kinds |= (1ULL << Kind::kArray ); + if (value->IsBoolean()) kinds |= (1ULL << Kind::kBoolean ); + if (value->IsNumber()) kinds |= (1ULL << Kind::kNumber ); + if (value->IsExternal()) kinds |= (1ULL << Kind::kExternal ); + if (value->IsInt32()) kinds |= (1ULL << Kind::kInt32 ); + if (value->IsUint32()) kinds |= (1ULL << Kind::kUint32 ); + if (value->IsDate()) kinds |= (1ULL << Kind::kDate ); + if (value->IsArgumentsObject()) kinds |= (1ULL << Kind::kArgumentsObject ); + if (value->IsBooleanObject()) kinds |= (1ULL << Kind::kBooleanObject ); + if (value->IsNumberObject()) kinds |= (1ULL << Kind::kNumberObject ); + if (value->IsStringObject()) kinds |= (1ULL << Kind::kStringObject ); + if (value->IsSymbolObject()) kinds |= (1ULL << Kind::kSymbolObject ); + if (value->IsNativeError()) kinds |= (1ULL << Kind::kNativeError ); + if (value->IsRegExp()) kinds |= (1ULL << Kind::kRegExp ); + if (value->IsFunction()) kinds |= (1ULL << Kind::kFunction ); + if (value->IsAsyncFunction()) kinds |= (1ULL << Kind::kAsyncFunction ); + if (value->IsGeneratorFunction()) kinds |= (1ULL << Kind::kGeneratorFunction); + if (value->IsGeneratorObject()) kinds |= (1ULL << Kind::kGeneratorObject ); + if (value->IsPromise()) kinds |= (1ULL << Kind::kPromise ); + if (value->IsMap()) kinds |= (1ULL << Kind::kMap ); + if (value->IsSet()) kinds |= (1ULL << Kind::kSet ); + if (value->IsMapIterator()) kinds |= (1ULL << Kind::kMapIterator ); + if (value->IsSetIterator()) kinds |= (1ULL << Kind::kSetIterator ); + if (value->IsWeakMap()) kinds |= (1ULL << Kind::kWeakMap ); + if (value->IsWeakSet()) kinds |= (1ULL << Kind::kWeakSet ); + if (value->IsArrayBuffer()) kinds |= (1ULL << Kind::kArrayBuffer ); + if (value->IsArrayBufferView()) kinds |= (1ULL << Kind::kArrayBufferView ); + if (value->IsTypedArray()) kinds |= (1ULL << Kind::kTypedArray ); + if (value->IsUint8Array()) kinds |= (1ULL << Kind::kUint8Array ); + if (value->IsUint8ClampedArray()) kinds |= (1ULL << Kind::kUint8ClampedArray); + if (value->IsInt8Array()) kinds |= (1ULL << Kind::kInt8Array ); + if (value->IsUint16Array()) kinds |= (1ULL << Kind::kUint16Array ); + if (value->IsInt16Array()) kinds |= (1ULL << Kind::kInt16Array ); + if (value->IsUint32Array()) kinds |= (1ULL << Kind::kUint32Array ); + if (value->IsInt32Array()) kinds |= (1ULL << Kind::kInt32Array ); + if (value->IsFloat32Array()) kinds |= (1ULL << Kind::kFloat32Array ); + if (value->IsFloat64Array()) kinds |= (1ULL << Kind::kFloat64Array ); + if (value->IsDataView()) kinds |= (1ULL << Kind::kDataView ); + if (value->IsSharedArrayBuffer()) kinds |= (1ULL << Kind::kSharedArrayBuffer); + if (value->IsProxy()) kinds |= (1ULL << Kind::kProxy ); + if (value->IsWebAssemblyCompiledModule()) + kinds |= (1ULL << Kind::kWebAssemblyCompiledModule); + + return kinds; +} + +std::string str(v8::Local value) { + v8::String::Utf8Value s(value); + if (s.length() == 0) { + return ""; + } + return *s; +} + +std::string report_exception(v8::Isolate* isolate, v8::Local ctx, v8::TryCatch& try_catch) { + std::stringstream ss; + ss << "Uncaught exception: "; + + std::string exceptionStr = str(try_catch.Exception()); + ss << exceptionStr; // TODO(aroman) JSON-ify objects? + + if (!try_catch.Message().IsEmpty()) { + if (!try_catch.Message()->GetScriptResourceName()->IsUndefined()) { + ss << std::endl + << "at " << str(try_catch.Message()->GetScriptResourceName()); + + v8::Maybe line_no = try_catch.Message()->GetLineNumber(ctx); + v8::Maybe start = try_catch.Message()->GetStartColumn(ctx); + v8::Maybe end = try_catch.Message()->GetEndColumn(ctx); + v8::MaybeLocal sourceLine = try_catch.Message()->GetSourceLine(ctx); + + if (line_no.IsJust()) { + ss << ":" << line_no.ToChecked(); + } + if (start.IsJust()) { + ss << ":" << start.ToChecked(); + } + if (!sourceLine.IsEmpty()) { + ss << std::endl + << " " << str(sourceLine.ToLocalChecked()); + } + if (start.IsJust() && end.IsJust()) { + ss << std::endl + << " "; + for (int i = 0; i < start.ToChecked(); i++) { + ss << " "; + } + for (int i = start.ToChecked(); i < end.ToChecked(); i++) { + ss << "^"; + } + } + } + } + + if (!try_catch.StackTrace().IsEmpty()) { + ss << std::endl << "Stack trace: " << str(try_catch.StackTrace()); + } + + return ss.str(); +} + + +extern "C" { + +Version version = {V8_MAJOR_VERSION, V8_MINOR_VERSION, V8_BUILD_NUMBER, V8_PATCH_LEVEL}; + +void v8_init() { + v8::Platform *platform = v8::platform::CreateDefaultPlatform( + 0, // thread_pool_size + v8::platform::IdleTaskSupport::kDisabled, + v8::platform::InProcessStackDumping::kDisabled); + v8::V8::InitializePlatform(platform); + v8::V8::Initialize(); + return; +} + +StartupData v8_CreateSnapshotDataBlob(const char* js) { + v8::StartupData data = v8::V8::CreateSnapshotDataBlob(js); + return StartupData{data.data, data.raw_size}; +} + +IsolatePtr v8_Isolate_New(StartupData startup_data) { + v8::Isolate::CreateParams create_params; + create_params.array_buffer_allocator = allocator; + if (startup_data.len > 0 && startup_data.ptr != nullptr) { + v8::StartupData* data = new v8::StartupData; + data->data = startup_data.ptr; + data->raw_size = startup_data.len; + create_params.snapshot_blob = data; + } + return static_cast(v8::Isolate::New(create_params)); +} +ContextPtr v8_Isolate_NewContext(IsolatePtr isolate_ptr) { + ISOLATE_SCOPE(static_cast(isolate_ptr)); + v8::HandleScope handle_scope(isolate); + + isolate->SetCaptureStackTraceForUncaughtExceptions(true); + + v8::Local globals = v8::ObjectTemplate::New(isolate); + + Context* ctx = new Context; + ctx->ptr.Reset(isolate, v8::Context::New(isolate, nullptr, globals)); + ctx->isolate = isolate; + return static_cast(ctx); +} +void v8_Isolate_Terminate(IsolatePtr isolate_ptr) { + v8::Isolate* isolate = static_cast(isolate_ptr); + isolate->TerminateExecution(); +} +void v8_Isolate_Release(IsolatePtr isolate_ptr) { + if (isolate_ptr == nullptr) { + return; + } + v8::Isolate* isolate = static_cast(isolate_ptr); + isolate->Dispose(); +} + +ValueTuple v8_Context_Run(ContextPtr ctxptr, const char* code, const char* filename) { + Context* ctx = static_cast(ctxptr); + v8::Isolate* isolate = ctx->isolate; + v8::Locker locker(isolate); + v8::Isolate::Scope isolate_scope(isolate); + v8::HandleScope handle_scope(isolate); + v8::Context::Scope context_scope(ctx->ptr.Get(isolate)); + v8::TryCatch try_catch(isolate); + try_catch.SetVerbose(false); + + filename = filename ? filename : "(no file)"; + + ValueTuple res = { nullptr, 0, nullptr }; + + v8::Local script = v8::Script::Compile( + v8::String::NewFromUtf8(isolate, code), + v8::String::NewFromUtf8(isolate, filename)); + + if (script.IsEmpty()) { + res.error_msg = DupString(report_exception(isolate, ctx->ptr.Get(isolate), try_catch)); + return res; + } + + v8::Local result = script->Run(); + + if (result.IsEmpty()) { + res.error_msg = DupString(report_exception(isolate, ctx->ptr.Get(isolate), try_catch)); + } else { + res.Value = static_cast(new Value(isolate, result)); + res.Kinds = v8_Value_KindsFromLocal(result); + } + + return res; +} + +void go_callback(const v8::FunctionCallbackInfo& args); + +PersistentValuePtr v8_Context_RegisterCallback( + ContextPtr ctxptr, + const char* name, + const char* id +) { + VALUE_SCOPE(ctxptr); + + v8::Local cb = + v8::FunctionTemplate::New(isolate, go_callback, + v8::String::NewFromUtf8(isolate, id)); + cb->SetClassName(v8::String::NewFromUtf8(isolate, name)); + return new Value(isolate, cb->GetFunction()); +} + +void go_callback(const v8::FunctionCallbackInfo& args) { + v8::Isolate* iso = args.GetIsolate(); + v8::HandleScope scope(iso); + + std::string id = str(args.Data()); + + std::string src_file, src_func; + int line_number = 0, column = 0; + v8::Local trace(v8::StackTrace::CurrentStackTrace(iso, 1)); + if (trace->GetFrameCount() == 1) { + v8::Local frame(trace->GetFrame(0)); + src_file = str(frame->GetScriptName()); + src_func = str(frame->GetFunctionName()); + line_number = frame->GetLineNumber(); + column = frame->GetColumn(); + } + + int argc = args.Length(); + ValueTuple argv[argc]; + for (int i = 0; i < argc; i++) { + argv[i] = (ValueTuple){new Value(iso, args[i]), v8_Value_KindsFromLocal(args[i])}; + } + + ValueTuple result = + go_callback_handler( + (String){id.data(), int(id.length())}, + (CallerInfo){ + (String){src_func.data(), int(src_func.length())}, + (String){src_file.data(), int(src_file.length())}, + line_number, + column + }, + argc, argv); + + if (result.error_msg.ptr != nullptr) { + v8::Local err = v8::Exception::Error( + v8::String::NewFromUtf8(iso, result.error_msg.ptr, v8::NewStringType::kNormal, result.error_msg.len).ToLocalChecked()); + iso->ThrowException(err); + } else if (result.Value == NULL) { + args.GetReturnValue().Set(v8::Undefined(iso)); + } else { + args.GetReturnValue().Set(*static_cast(result.Value)); + } +} + +PersistentValuePtr v8_Context_Global(ContextPtr ctxptr) { + VALUE_SCOPE(ctxptr); + return new Value(isolate, ctx->Global()); +} + +void v8_Context_Release(ContextPtr ctxptr) { + if (ctxptr == nullptr) { + return; + } + Context* ctx = static_cast(ctxptr); + ISOLATE_SCOPE(ctx->isolate); + ctx->ptr.Reset(); +} + +PersistentValuePtr v8_Context_Create(ContextPtr ctxptr, ImmediateValue val) { + VALUE_SCOPE(ctxptr); + + switch (val.Type) { + case tARRAY: return new Value(isolate, v8::Array::New(isolate, val.Mem.len)); break; + case tARRAYBUFFER: { + v8::Local buf = v8::ArrayBuffer::New(isolate, val.Mem.len); + memcpy(buf->GetContents().Data(), val.Mem.ptr, val.Mem.len); + return new Value(isolate, buf); + break; + } + case tBOOL: return new Value(isolate, v8::Boolean::New(isolate, val.Bool == 1)); break; + case tDATE: return new Value(isolate, v8::Date::New(isolate, val.Float64)); break; + case tFLOAT64: return new Value(isolate, v8::Number::New(isolate, val.Float64)); break; + // For now, this is converted to a double on entry. + // TODO(aroman) Consider using BigInt, but only if the V8 version supports + // it. Check to see what V8 versions support BigInt. + case tINT64: return new Value(isolate, v8::Number::New(isolate, double(val.Int64))); break; + case tOBJECT: return new Value(isolate, v8::Object::New(isolate)); break; + case tSTRING: { + return new Value(isolate, v8::String::NewFromUtf8( + isolate, val.Mem.ptr, v8::NewStringType::kNormal, val.Mem.len).ToLocalChecked()); + break; + } + case tUNDEFINED: return new Value(isolate, v8::Undefined(isolate)); break; + } + return nullptr; +} + +ValueTuple v8_Value_Get(ContextPtr ctxptr, PersistentValuePtr valueptr, const char* field) { + VALUE_SCOPE(ctxptr); + + Value* value = static_cast(valueptr); + v8::Local maybeObject = value->Get(isolate); + if (!maybeObject->IsObject()) { + return (ValueTuple){nullptr, 0, DupString("Not an object")}; + } + + // We can safely call `ToLocalChecked`, because + // we've just created the local object above. + v8::Local object = maybeObject->ToObject(ctx).ToLocalChecked(); + + v8::Local localValue = object->Get(ctx, v8::String::NewFromUtf8(isolate, field)).ToLocalChecked(); + + return (ValueTuple){ + new Value(isolate, localValue), + v8_Value_KindsFromLocal(localValue), + nullptr, + }; +} + +ValueTuple v8_Value_GetIdx(ContextPtr ctxptr, PersistentValuePtr valueptr, int idx) { + VALUE_SCOPE(ctxptr); + + Value* value = static_cast(valueptr); + v8::Local maybeObject = value->Get(isolate); + if (!maybeObject->IsObject()) { + return (ValueTuple){nullptr, 0, DupString("Not an object")}; + } + + v8::Local obj; + if (maybeObject->IsArrayBuffer()) { + v8::ArrayBuffer* bufPtr = v8::ArrayBuffer::Cast(*maybeObject); + if (idx < bufPtr->GetContents().ByteLength()) { + obj = v8::Number::New(isolate, ((unsigned char*)bufPtr->GetContents().Data())[idx]); + } else { + obj = v8::Undefined(isolate); + } + } else { + // We can safely call `ToLocalChecked`, because + // we've just created the local object above. + v8::Local object = maybeObject->ToObject(ctx).ToLocalChecked(); + obj = object->Get(ctx, uint32_t(idx)).ToLocalChecked(); + } + return (ValueTuple){new Value(isolate, obj), v8_Value_KindsFromLocal(obj), nullptr}; +} + +Error v8_Value_Set(ContextPtr ctxptr, PersistentValuePtr valueptr, + const char* field, PersistentValuePtr new_valueptr) { + VALUE_SCOPE(ctxptr); + + Value* value = static_cast(valueptr); + v8::Local maybeObject = value->Get(isolate); + if (!maybeObject->IsObject()) { + return DupString("Not an object"); + } + + // We can safely call `ToLocalChecked`, because + // we've just created the local object above. + v8::Local object = + maybeObject->ToObject(ctx).ToLocalChecked(); + + Value* new_value = static_cast(new_valueptr); + v8::Local new_value_local = new_value->Get(isolate); + v8::Maybe res = + object->Set(ctx, v8::String::NewFromUtf8(isolate, field), new_value_local); + + if (res.IsNothing()) { + return DupString("Something went wrong -- set returned nothing."); + } else if (!res.FromJust()) { + return DupString("Something went wrong -- set failed."); + } + return (Error){nullptr, 0}; +} + +Error v8_Value_SetIdx(ContextPtr ctxptr, PersistentValuePtr valueptr, + int idx, PersistentValuePtr new_valueptr) { + VALUE_SCOPE(ctxptr); + + Value* value = static_cast(valueptr); + v8::Local maybeObject = value->Get(isolate); + if (!maybeObject->IsObject()) { + return DupString("Not an object"); + } + + Value* new_value = static_cast(new_valueptr); + v8::Local new_value_local = new_value->Get(isolate); + if (maybeObject->IsArrayBuffer()) { + v8::ArrayBuffer* bufPtr = v8::ArrayBuffer::Cast(*maybeObject); + if (!new_value_local->IsNumber()) { + return DupString("Cannot assign non-number into array buffer"); + } else if (idx >= bufPtr->GetContents().ByteLength()) { + return DupString("Cannot assign to an index beyond the size of an array buffer"); + } else { + ((unsigned char*)bufPtr->GetContents().Data())[idx] = new_value_local->ToNumber(ctx).ToLocalChecked()->Value(); + } + } else { + // We can safely call `ToLocalChecked`, because + // we've just created the local object above. + v8::Local object = maybeObject->ToObject(ctx).ToLocalChecked(); + + v8::Maybe res = object->Set(ctx, uint32_t(idx), new_value_local); + + if (res.IsNothing()) { + return DupString("Something went wrong -- set returned nothing."); + } else if (!res.FromJust()) { + return DupString("Something went wrong -- set failed."); + } + } + + return (Error){nullptr, 0}; +} + +ValueTuple v8_Value_Call(ContextPtr ctxptr, + PersistentValuePtr funcptr, + PersistentValuePtr selfptr, + int argc, PersistentValuePtr* argvptr) { + VALUE_SCOPE(ctxptr); + + v8::TryCatch try_catch(isolate); + try_catch.SetVerbose(false); + + v8::Local func_val = static_cast(funcptr)->Get(isolate); + if (!func_val->IsFunction()) { + return (ValueTuple){nullptr, 0, DupString("Not a function")}; + } + v8::Local func = v8::Local::Cast(func_val); + + v8::Local self; + if (selfptr == nullptr) { + self = ctx->Global(); + } else { + self = static_cast(selfptr)->Get(isolate); + } + + v8::Local* argv = new v8::Local[argc]; + for (int i = 0; i < argc; i++) { + argv[i] = static_cast(argvptr[i])->Get(isolate); + } + + v8::MaybeLocal result = func->Call(ctx, self, argc, argv); + + delete[] argv; + + if (result.IsEmpty()) { + return (ValueTuple){nullptr, 0, DupString(report_exception(isolate, ctx, try_catch))}; + } + + v8::Local value = result.ToLocalChecked(); + return (ValueTuple){ + static_cast(new Value(isolate, value)), + v8_Value_KindsFromLocal(value), + nullptr + }; +} + +ValueTuple v8_Value_New(ContextPtr ctxptr, + PersistentValuePtr funcptr, + int argc, PersistentValuePtr* argvptr) { + VALUE_SCOPE(ctxptr); + + v8::TryCatch try_catch(isolate); + try_catch.SetVerbose(false); + + v8::Local func_val = static_cast(funcptr)->Get(isolate); + if (!func_val->IsFunction()) { + return (ValueTuple){nullptr, 0, DupString("Not a function")}; + } + v8::Local func = v8::Local::Cast(func_val); + + v8::Local* argv = new v8::Local[argc]; + for (int i = 0; i < argc; i++) { + argv[i] = static_cast(argvptr[i])->Get(isolate); + } + + v8::MaybeLocal result = func->NewInstance(ctx, argc, argv); + + delete[] argv; + + if (result.IsEmpty()) { + return (ValueTuple){nullptr, 0, DupString(report_exception(isolate, ctx, try_catch))}; + } + + v8::Local value = result.ToLocalChecked(); + return (ValueTuple){ + static_cast(new Value(isolate, value)), + v8_Value_KindsFromLocal(value), + nullptr + }; +} + +void v8_Value_Release(ContextPtr ctxptr, PersistentValuePtr valueptr) { + if (valueptr == nullptr || ctxptr == nullptr) { + return; + } + + ISOLATE_SCOPE(static_cast(ctxptr)->isolate); + + Value* value = static_cast(valueptr); + value->Reset(); + delete value; +} + +String v8_Value_String(ContextPtr ctxptr, PersistentValuePtr valueptr) { + VALUE_SCOPE(ctxptr); + + v8::Local value = static_cast(valueptr)->Get(isolate); + return DupString(value->ToString()); +} + +double v8_Value_Float64(ContextPtr ctxptr, PersistentValuePtr valueptr) { + VALUE_SCOPE(ctxptr); + v8::Local value = static_cast(valueptr)->Get(isolate); + v8::Maybe val = value->NumberValue(ctx); + if (val.IsNothing()) { + return 0; + } + return val.ToChecked(); +} +int64_t v8_Value_Int64(ContextPtr ctxptr, PersistentValuePtr valueptr) { + VALUE_SCOPE(ctxptr); + v8::Local value = static_cast(valueptr)->Get(isolate); + v8::Maybe val = value->IntegerValue(ctx); + if (val.IsNothing()) { + return 0; + } + return val.ToChecked(); +} +int v8_Value_Bool(ContextPtr ctxptr, PersistentValuePtr valueptr) { + VALUE_SCOPE(ctxptr); + v8::Local value = static_cast(valueptr)->Get(isolate); + v8::Maybe val = value->BooleanValue(ctx); + if (val.IsNothing()) { + return 0; + } + return val.ToChecked() ? 1 : 0; +} + +ByteArray v8_Value_Bytes(ContextPtr ctxptr, PersistentValuePtr valueptr) { + VALUE_SCOPE(ctxptr); + + v8::Local value = static_cast(valueptr)->Get(isolate); + + v8::ArrayBuffer* bufPtr; + + if (value->IsTypedArray()) { + bufPtr = *v8::TypedArray::Cast(*value)->Buffer(); + } else if (value->IsArrayBuffer()) { + bufPtr = v8::ArrayBuffer::Cast(*value); + } else { + return (ByteArray){ nullptr, 0 }; + } + + if (bufPtr == NULL) { + return (ByteArray){ nullptr, 0 }; + } + + return (ByteArray){ + static_cast(bufPtr->GetContents().Data()), + static_cast(bufPtr->GetContents().ByteLength()), + }; +} + +HeapStatistics v8_Isolate_GetHeapStatistics(IsolatePtr isolate_ptr) { + if (isolate_ptr == nullptr) { + return HeapStatistics{0}; + } + ISOLATE_SCOPE(static_cast(isolate_ptr)); + v8::HeapStatistics hs; + isolate->GetHeapStatistics(&hs); + return HeapStatistics{ + hs.total_heap_size(), + hs.total_heap_size_executable(), + hs.total_physical_size(), + hs.total_available_size(), + hs.used_heap_size(), + hs.heap_size_limit(), + hs.malloced_memory(), + hs.peak_malloced_memory(), + hs.does_zap_garbage() + }; +} + +void v8_Isolate_LowMemoryNotification(IsolatePtr isolate_ptr) { + if (isolate_ptr == nullptr) { + return; + } + ISOLATE_SCOPE(static_cast(isolate_ptr)); + isolate->LowMemoryNotification(); +} + +ValueTuple v8_Value_PromiseInfo(ContextPtr ctxptr, PersistentValuePtr valueptr, + int* promise_state) { + VALUE_SCOPE(ctxptr); + v8::Local value = static_cast(valueptr)->Get(isolate); + if (!value->IsPromise()) { // just in case + return (ValueTuple){nullptr, 0, DupString("Not a promise")}; + } + + v8::Promise* prom = v8::Promise::Cast(*value); + *promise_state = prom->State(); + if (prom->State() == v8::Promise::PromiseState::kPending) { + return (ValueTuple){nullptr, 0, nullptr}; + } + v8::Local res = prom->Result(); + return (ValueTuple){new Value(isolate, res), v8_Value_KindsFromLocal(res), nullptr}; +} + +} // extern "C" diff --git a/v8_c_bridge-old.h b/v8_c_bridge-old.h new file mode 100644 index 0000000..ab91aca --- /dev/null +++ b/v8_c_bridge-old.h @@ -0,0 +1,184 @@ +#include +#include + +#ifndef V8_C_BRIDGE_H +#define V8_C_BRIDGE_H + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void* IsolatePtr; +typedef void* ContextPtr; +typedef void* PersistentValuePtr; + +typedef struct { + const char* ptr; + int len; +} String; + +typedef String Error; +typedef String StartupData; +typedef String ByteArray; + +typedef struct { + size_t total_heap_size; + size_t total_heap_size_executable; + size_t total_physical_size; + size_t total_available_size; + size_t used_heap_size; + size_t heap_size_limit; + size_t malloced_memory; + size_t peak_malloced_memory; + size_t does_zap_garbage; +} HeapStatistics; + +// NOTE! These values must exactly match the values in kinds.go. Any mismatch +// will cause kinds to be misreported. +typedef enum { + kUndefined = 0, + kNull, + kName, + kString, + kSymbol, + kFunction, + kArray, + kObject, + kBoolean, + kNumber, + kExternal, + kInt32, + kUint32, + kDate, + kArgumentsObject, + kBooleanObject, + kNumberObject, + kStringObject, + kSymbolObject, + kNativeError, + kRegExp, + kAsyncFunction, + kGeneratorFunction, + kGeneratorObject, + kPromise, + kMap, + kSet, + kMapIterator, + kSetIterator, + kWeakMap, + kWeakSet, + kArrayBuffer, + kArrayBufferView, + kTypedArray, + kUint8Array, + kUint8ClampedArray, + kInt8Array, + kUint16Array, + kInt16Array, + kUint32Array, + kInt32Array, + kFloat32Array, + kFloat64Array, + kDataView, + kSharedArrayBuffer, + kProxy, + kWebAssemblyCompiledModule, + kNumKinds, +} Kind; + +// Each kind can be represent using only single 64 bit bitmask since there +// are less than 64 kinds so far. If this grows beyond 64 kinds, we can switch +// to multiple bitmasks or a dynamically-allocated array. +typedef uint64_t KindMask; + +typedef struct { + PersistentValuePtr Value; + KindMask Kinds; + Error error_msg; +} ValueTuple; + +typedef struct { + String Funcname; + String Filename; + int Line; + int Column; +} CallerInfo; + +typedef struct { int Major, Minor, Build, Patch; } Version; +extern Version version; + +// typedef unsigned int uint32_t; + +// v8_init must be called once before anything else. +extern void v8_init(); + +extern StartupData v8_CreateSnapshotDataBlob(const char* js); + +extern IsolatePtr v8_Isolate_New(StartupData data); +extern ContextPtr v8_Isolate_NewContext(IsolatePtr isolate); +extern void v8_Isolate_Terminate(IsolatePtr isolate); +extern void v8_Isolate_Release(IsolatePtr isolate); + +extern HeapStatistics v8_Isolate_GetHeapStatistics(IsolatePtr isolate); +extern void v8_Isolate_LowMemoryNotification(IsolatePtr isolate); + +extern ValueTuple v8_Context_Run(ContextPtr ctx, + const char* code, const char* filename); +extern PersistentValuePtr v8_Context_RegisterCallback(ContextPtr ctx, + const char* name, const char* id); +extern PersistentValuePtr v8_Context_Global(ContextPtr ctx); +extern void v8_Context_Release(ContextPtr ctx); + +typedef enum { + tSTRING, + tBOOL, + tFLOAT64, + tINT64, + tOBJECT, + tARRAY, + tARRAYBUFFER, + tUNDEFINED, + tDATE, // uses Float64 for msec since Unix epoch +} ImmediateValueType; + +typedef struct { + ImmediateValueType Type; + // Mem is used for String, ArrayBuffer, or Array. For Array, only len is + // used -- ptr is ignored. + ByteArray Mem; + int Bool; + double Float64; + int64_t Int64; +} ImmediateValue; + +extern PersistentValuePtr v8_Context_Create(ContextPtr ctx, ImmediateValue val); + +extern ValueTuple v8_Value_Get(ContextPtr ctx, PersistentValuePtr value, const char* field); +extern Error v8_Value_Set(ContextPtr ctx, PersistentValuePtr value, + const char* field, PersistentValuePtr new_value); +extern ValueTuple v8_Value_GetIdx(ContextPtr ctx, PersistentValuePtr value, int idx); +extern Error v8_Value_SetIdx(ContextPtr ctx, PersistentValuePtr value, + int idx, PersistentValuePtr new_value); +extern ValueTuple v8_Value_Call(ContextPtr ctx, + PersistentValuePtr func, + PersistentValuePtr self, + int argc, PersistentValuePtr* argv); +extern ValueTuple v8_Value_New(ContextPtr ctx, + PersistentValuePtr func, + int argc, PersistentValuePtr* argv); +extern void v8_Value_Release(ContextPtr ctx, PersistentValuePtr value); +extern String v8_Value_String(ContextPtr ctx, PersistentValuePtr value); + +extern double v8_Value_Float64(ContextPtr ctx, PersistentValuePtr value); +extern int64_t v8_Value_Int64(ContextPtr ctx, PersistentValuePtr value); +extern int v8_Value_Bool(ContextPtr ctx, PersistentValuePtr value); +extern ByteArray v8_Value_Bytes(ContextPtr ctx, PersistentValuePtr value); + +extern ValueTuple v8_Value_PromiseInfo(ContextPtr ctx, PersistentValuePtr value, + int* promise_state); + +#ifdef __cplusplus +} +#endif + +#endif // !defined(V8_C_BRIDGE_H) diff --git a/v8_c_bridge.cpp b/v8_c_bridge.cpp new file mode 100644 index 0000000..95cc5f7 --- /dev/null +++ b/v8_c_bridge.cpp @@ -0,0 +1,783 @@ +// v8_c_bridge.cpp : Defines the exported functions for the DLL. +// +// The following line has been added for Go projects to be able to include the CPP +// file along Go files for static library compilation in Linux/Darwin, but +// exclude the file for Windows which needs a dynamic library: +// +build !windows +// +//TODO: check all places where ToLocalChecked() is present without real checks + +#include "pch.h" +#include "framework.h" +#include "v8_c_bridge.h" + +#include "libplatform/libplatform.h" +#include "v8.h" + +#include +#include +#include +#include +#include +#include + +#define ISOLATE_SCOPE(iso) \ + v8::Isolate* isolate = (iso); \ + v8::Locker locker(isolate); /* Lock to current thread. */ \ + v8::Isolate::Scope isolate_scope(isolate); /* Assign isolate to this thread. */ + + +#define VALUE_SCOPE(ctxptr) \ + ISOLATE_SCOPE(static_cast(ctxptr)->isolate) \ + v8::HandleScope handle_scope(isolate); /* Create a scope for handles. */ \ + v8::Local ctx(static_cast(ctxptr)->ptr.Get(isolate)); \ + v8::Context::Scope context_scope(ctx); /* Scope to this context. */ + +// We only need one, it's stateless. +auto allocator = v8::ArrayBuffer::Allocator::NewDefaultAllocator(); + +typedef struct { + v8::Persistent ptr; + v8::Isolate* isolate; +} Context; + +typedef v8::Persistent Value; + +String DupString(const v8::String::Utf8Value& src) { + char* data = static_cast(malloc(src.length())); + memcpy(data, *src, src.length()); + return String{ data, src.length() }; +} +String DupString(v8::Isolate* isolate, const v8::Local& val) { + return DupString(v8::String::Utf8Value(isolate, val)); +} +String DupString(const char* msg) { + const char* data = _strdup(msg); + return String{ data, int(strlen(msg)) }; +} +String DupString(const std::string& src) { + char* data = static_cast(malloc(src.length())); + memcpy(data, src.data(), src.length()); + return String{ data, int(src.length()) }; +} + +KindMask v8_Value_KindsFromLocal(v8::Local value) { + KindMask kinds = 0; + + if (value->IsUndefined()) kinds |= (1ULL << Kind::kUndefined); + if (value->IsNull()) kinds |= (1ULL << Kind::kNull); + if (value->IsName()) kinds |= (1ULL << Kind::kName); + if (value->IsString()) kinds |= (1ULL << Kind::kString); + if (value->IsSymbol()) kinds |= (1ULL << Kind::kSymbol); + if (value->IsObject()) kinds |= (1ULL << Kind::kObject); + if (value->IsArray()) kinds |= (1ULL << Kind::kArray); + if (value->IsBoolean()) kinds |= (1ULL << Kind::kBoolean); + if (value->IsNumber()) kinds |= (1ULL << Kind::kNumber); + if (value->IsExternal()) kinds |= (1ULL << Kind::kExternal); + if (value->IsInt32()) kinds |= (1ULL << Kind::kInt32); + if (value->IsUint32()) kinds |= (1ULL << Kind::kUint32); + if (value->IsDate()) kinds |= (1ULL << Kind::kDate); + if (value->IsArgumentsObject()) kinds |= (1ULL << Kind::kArgumentsObject); + if (value->IsBooleanObject()) kinds |= (1ULL << Kind::kBooleanObject); + if (value->IsNumberObject()) kinds |= (1ULL << Kind::kNumberObject); + if (value->IsStringObject()) kinds |= (1ULL << Kind::kStringObject); + if (value->IsSymbolObject()) kinds |= (1ULL << Kind::kSymbolObject); + if (value->IsNativeError()) kinds |= (1ULL << Kind::kNativeError); + if (value->IsRegExp()) kinds |= (1ULL << Kind::kRegExp); + if (value->IsFunction()) kinds |= (1ULL << Kind::kFunction); + if (value->IsAsyncFunction()) kinds |= (1ULL << Kind::kAsyncFunction); + if (value->IsGeneratorFunction()) kinds |= (1ULL << Kind::kGeneratorFunction); + if (value->IsGeneratorObject()) kinds |= (1ULL << Kind::kGeneratorObject); + if (value->IsPromise()) kinds |= (1ULL << Kind::kPromise); + if (value->IsMap()) kinds |= (1ULL << Kind::kMap); + if (value->IsSet()) kinds |= (1ULL << Kind::kSet); + if (value->IsMapIterator()) kinds |= (1ULL << Kind::kMapIterator); + if (value->IsSetIterator()) kinds |= (1ULL << Kind::kSetIterator); + if (value->IsWeakMap()) kinds |= (1ULL << Kind::kWeakMap); + if (value->IsWeakSet()) kinds |= (1ULL << Kind::kWeakSet); + if (value->IsArrayBuffer()) kinds |= (1ULL << Kind::kArrayBuffer); + if (value->IsArrayBufferView()) kinds |= (1ULL << Kind::kArrayBufferView); + if (value->IsTypedArray()) kinds |= (1ULL << Kind::kTypedArray); + if (value->IsUint8Array()) kinds |= (1ULL << Kind::kUint8Array); + if (value->IsUint8ClampedArray()) kinds |= (1ULL << Kind::kUint8ClampedArray); + if (value->IsInt8Array()) kinds |= (1ULL << Kind::kInt8Array); + if (value->IsUint16Array()) kinds |= (1ULL << Kind::kUint16Array); + if (value->IsInt16Array()) kinds |= (1ULL << Kind::kInt16Array); + if (value->IsUint32Array()) kinds |= (1ULL << Kind::kUint32Array); + if (value->IsInt32Array()) kinds |= (1ULL << Kind::kInt32Array); + if (value->IsFloat32Array()) kinds |= (1ULL << Kind::kFloat32Array); + if (value->IsFloat64Array()) kinds |= (1ULL << Kind::kFloat64Array); + if (value->IsDataView()) kinds |= (1ULL << Kind::kDataView); + if (value->IsSharedArrayBuffer()) kinds |= (1ULL << Kind::kSharedArrayBuffer); + if (value->IsProxy()) kinds |= (1ULL << Kind::kProxy); + if (value->IsWebAssemblyCompiledModule()) + kinds |= (1ULL << Kind::kWebAssemblyCompiledModule); + + return kinds; +} + +std::string str(v8::Isolate* isolate, v8::Local value) { + v8::String::Utf8Value s(isolate, value); + if (s.length() == 0) { + return ""; + } + return *s; +} + +std::string report_exception(v8::Isolate* isolate, v8::Local ctx, v8::TryCatch& try_catch) { + std::stringstream ss; + ss << "Uncaught exception: "; + + std::string exceptionStr = str(isolate, try_catch.Exception()); + ss << exceptionStr; // TODO(aroman) JSON-ify objects? + + if (!try_catch.Message().IsEmpty()) { + if (!try_catch.Message()->GetScriptResourceName()->IsUndefined()) { + ss << std::endl + << "at " << str(isolate, try_catch.Message()->GetScriptResourceName()); + + v8::Maybe line_no = try_catch.Message()->GetLineNumber(ctx); + v8::Maybe start = try_catch.Message()->GetStartColumn(ctx); + v8::Maybe end = try_catch.Message()->GetEndColumn(ctx); + v8::MaybeLocal sourceLine = try_catch.Message()->GetSourceLine(ctx); + + if (line_no.IsJust()) { + ss << ":" << line_no.ToChecked(); + } + if (start.IsJust()) { + ss << ":" << start.ToChecked(); + } + if (!sourceLine.IsEmpty()) { + ss << std::endl + << " " << str(isolate, sourceLine.ToLocalChecked()); + } + if (start.IsJust() && end.IsJust()) { + ss << std::endl + << " "; + for (int i = 0; i < start.ToChecked(); i++) { + ss << " "; + } + for (int i = start.ToChecked(); i < end.ToChecked(); i++) { + ss << "^"; + } + } + } + } + + if (!try_catch.StackTrace(ctx).IsEmpty()) { + ss << std::endl << "Stack trace: " << str(isolate, try_catch.StackTrace(ctx).ToLocalChecked()); + } + + return ss.str(); +} + +extern "C" { + + V8CBRIDGE_API GoCallbackHandlerPtr go_callback_handler = nullptr; + + V8CBRIDGE_API Version version = { V8_MAJOR_VERSION, V8_MINOR_VERSION, V8_BUILD_NUMBER, V8_PATCH_LEVEL }; + + V8CBRIDGE_API void v8_Init(GoCallbackHandlerPtr callback_handler) { + + //const char* path + //v8::V8::InitializeICUDefaultLocation(path); + //v8::V8::InitializeExternalStartupData(path); + + if (!v8::V8::InitializeICU()) { + std::cout << "Warning: V8 ICU not initialized - not bundled with library.\n"; + } + + std::unique_ptr platform = v8::platform::NewDefaultPlatform( + 0, // thread_pool_size + v8::platform::IdleTaskSupport::kDisabled, + v8::platform::InProcessStackDumping::kDisabled); + v8::V8::InitializePlatform(platform.get()); + v8::V8::Initialize(); + + go_callback_handler = callback_handler; + + return; + } + + V8CBRIDGE_API void v8_Free(void* ptr) { + free(ptr); + } + + /* + TODO: probably to be ported to use v8::SnapshotCreator + V8CBRIDGE_API StartupData v8_CreateSnapshotDataBlob(const char* js) { + v8::StartupData data = v8::V8::CreateSnapshotDataBlob(js); + return StartupData{ data.data, data.raw_size }; + } + */ + + V8CBRIDGE_API IsolatePtr v8_Isolate_New(StartupData startup_data) { + std::cout << "Hello from v8_Isolate_New\n"; + v8::Isolate::CreateParams create_params; + std::cout << "v8_Isolate_New 2\n"; + create_params.array_buffer_allocator = allocator; + //create_params.array_buffer_allocator = v8::ArrayBuffer::Allocator::NewDefaultAllocator(); + std::cout << "v8_Isolate_New 3\n"; + if (startup_data.len > 0 && startup_data.ptr != nullptr) { + std::cout << "v8_Isolate_New 4\n"; + v8::StartupData* data = new v8::StartupData; + data->data = startup_data.ptr; + data->raw_size = startup_data.len; + create_params.snapshot_blob = data; + } + std::cout << "v8_Isolate_New 5 with outPtr\n"; + v8::Isolate* x = v8::Isolate::New(create_params); + std::cout << "v8_Isolate_New x is init\n"; + IsolatePtr outPtr = static_cast(x); + std::cout << "v8_Isolate_New outPtr is init\n"; + std::cout << outPtr; + std::cout << "returning\n"; + return outPtr; + } + V8CBRIDGE_API ContextPtr v8_Isolate_NewContext(IsolatePtr isolate_ptr) { + ISOLATE_SCOPE(static_cast(isolate_ptr)); + v8::HandleScope handle_scope(isolate); + + isolate->SetCaptureStackTraceForUncaughtExceptions(true); + + v8::Local globals = v8::ObjectTemplate::New(isolate); + + Context* ctx = new Context; + ctx->ptr.Reset(isolate, v8::Context::New(isolate, nullptr, globals)); + ctx->isolate = isolate; + return static_cast(ctx); + } + V8CBRIDGE_API void v8_Isolate_Terminate(IsolatePtr isolate_ptr) { + v8::Isolate* isolate = static_cast(isolate_ptr); + isolate->TerminateExecution(); + } + V8CBRIDGE_API void v8_Isolate_Release(IsolatePtr isolate_ptr) { + if (isolate_ptr == nullptr) { + return; + } + v8::Isolate* isolate = static_cast(isolate_ptr); + isolate->Dispose(); + } + + V8CBRIDGE_API ValueTuple v8_Context_Run(ContextPtr ctxptr, const char* code, const char* filename) { + Context* ctx = static_cast(ctxptr); + v8::Isolate* isolate = ctx->isolate; + v8::Locker locker(isolate); + v8::Isolate::Scope isolate_scope(isolate); + v8::HandleScope handle_scope(isolate); + v8::Local localCtx = ctx->ptr.Get(isolate); + v8::Context::Scope context_scope(localCtx); + v8::TryCatch try_catch(isolate); + try_catch.SetVerbose(false); + + filename = filename ? filename : "(no file)"; + + ValueTuple res = { nullptr, 0, nullptr }; + + v8::MaybeLocal filenameLocal = v8::String::NewFromUtf8(isolate, filename); + if (filenameLocal.IsEmpty()) { + res.error_msg = DupString("Error initing filename."); //TODO: is this check needed and what is appropriate message? + return res; + } + + v8::ScriptOrigin origin(filenameLocal.ToLocalChecked()); + + v8::MaybeLocal script = v8::Script::Compile( + localCtx, + v8::String::NewFromUtf8(isolate, code).ToLocalChecked(), + &origin); + + if (script.IsEmpty()) { + res.error_msg = DupString(report_exception(isolate, ctx->ptr.Get(isolate), try_catch)); + return res; + } + + v8::MaybeLocal result = script.ToLocalChecked()->Run(localCtx); + + if (result.IsEmpty()) { + res.error_msg = DupString(report_exception(isolate, ctx->ptr.Get(isolate), try_catch)); + } + else { + v8::Local resultChecked = result.ToLocalChecked(); + res.Value = static_cast(new Value(isolate, resultChecked)); + res.Kinds = v8_Value_KindsFromLocal(resultChecked); + } + + return res; + } + + V8CBRIDGE_API void go_callback(const v8::FunctionCallbackInfo& args); + + V8CBRIDGE_API PersistentValuePtr v8_Context_RegisterCallback( + ContextPtr ctxptr, + const char* name, + const char* id + ) { + VALUE_SCOPE(ctxptr); + + v8::MaybeLocal idLocal = v8::String::NewFromUtf8(isolate, id); + if (idLocal.IsEmpty()) { + return nullptr; + } + + v8::MaybeLocal nameLocal = v8::String::NewFromUtf8(isolate, name); + if (nameLocal.IsEmpty()) { + return nullptr; + } + + v8::Local cb = + v8::FunctionTemplate::New(isolate, go_callback, + idLocal.ToLocalChecked()); + cb->SetClassName(nameLocal.ToLocalChecked()); + + v8::MaybeLocal fn = cb->GetFunction(ctx); + + if (fn.IsEmpty()) { + return nullptr; + } + + return new Value(isolate, fn.ToLocalChecked()); + } + + V8CBRIDGE_API void go_callback(const v8::FunctionCallbackInfo& args) { + v8::Isolate* iso = args.GetIsolate(); + v8::HandleScope scope(iso); + + if (go_callback_handler == nullptr) { + const char* err_msg = "Callback handler not init"; + v8::Local err = v8::Exception::Error( + v8::String::NewFromUtf8(iso, err_msg).ToLocalChecked()); + iso->ThrowException(err); + } + + std::string id = str(iso, args.Data()); + + std::string src_file, src_func; + int line_number = 0, column = 0; + v8::Local trace(v8::StackTrace::CurrentStackTrace(iso, 1)); + if (trace->GetFrameCount() == 1) { + v8::Local frame(trace->GetFrame(iso, 0)); + src_file = str(iso, frame->GetScriptName()); + src_func = str(iso, frame->GetFunctionName()); + line_number = frame->GetLineNumber(); + column = frame->GetColumn(); + } + + int argc = args.Length(); + ValueTuple* argv = new ValueTuple[argc]; + for (int i = 0; i < argc; i++) { + argv[i] = ValueTuple{ new Value(iso, args[i]), v8_Value_KindsFromLocal(args[i]) }; + } + + ValueTuple result = + go_callback_handler( + String{ id.data(), int(id.length()) }, + CallerInfo{ + String{src_func.data(), int(src_func.length())}, + String{src_file.data(), int(src_file.length())}, + line_number, + column + }, + argc, argv); + + if (result.error_msg.ptr != nullptr) { + v8::Local err = v8::Exception::Error( + v8::String::NewFromUtf8(iso, result.error_msg.ptr, v8::NewStringType::kNormal, result.error_msg.len).ToLocalChecked()); + iso->ThrowException(err); + } + else if (result.Value == NULL) { + args.GetReturnValue().Set(v8::Undefined(iso)); + } + else { + v8::Persistent* persVal = static_cast(result.Value); + args.GetReturnValue().Set(persVal->Get(iso)); + } + + } + + V8CBRIDGE_API PersistentValuePtr v8_Context_Global(ContextPtr ctxptr) { + VALUE_SCOPE(ctxptr); + return new Value(isolate, ctx->Global()); + } + + V8CBRIDGE_API void v8_Context_Release(ContextPtr ctxptr) { + if (ctxptr == nullptr) { + return; + } + Context* ctx = static_cast(ctxptr); + ISOLATE_SCOPE(ctx->isolate); + ctx->ptr.Reset(); + } + + V8CBRIDGE_API PersistentValuePtr v8_Context_Create(ContextPtr ctxptr, ImmediateValue val) { + VALUE_SCOPE(ctxptr); + + switch (val.Type) { + case tARRAY: return new Value(isolate, v8::Array::New(isolate, val.Mem.len)); break; + case tARRAYBUFFER: { + v8::Local buf = v8::ArrayBuffer::New(isolate, val.Mem.len); + memcpy(buf->GetContents().Data(), val.Mem.ptr, val.Mem.len); + return new Value(isolate, buf); + break; + } + case tBOOL: return new Value(isolate, v8::Boolean::New(isolate, val.Bool == 1)); break; + case tDATE: { + v8::MaybeLocal maybeDate = v8::Date::New(ctx, val.Float64); + if (maybeDate.IsEmpty()) { + return nullptr; + } + return new Value(isolate, maybeDate.ToLocalChecked()); + break; + } + case tFLOAT64: return new Value(isolate, v8::Number::New(isolate, val.Float64)); break; + // For now, this is converted to a double on entry. + // TODO(aroman) Consider using BigInt, but only if the V8 version supports + // it. Check to see what V8 versions support BigInt. + case tINT64: return new Value(isolate, v8::Number::New(isolate, double(val.Int64))); break; + case tOBJECT: return new Value(isolate, v8::Object::New(isolate)); break; + case tSTRING: { + return new Value(isolate, v8::String::NewFromUtf8( + isolate, val.Mem.ptr, v8::NewStringType::kNormal, val.Mem.len).ToLocalChecked()); + break; + } + case tUNDEFINED: return new Value(isolate, v8::Undefined(isolate)); break; + } + return nullptr; + } + + V8CBRIDGE_API ValueTuple v8_Value_Get(ContextPtr ctxptr, PersistentValuePtr valueptr, const char* field) { + VALUE_SCOPE(ctxptr); + + Value* value = static_cast(valueptr); + v8::Local maybeObject = value->Get(isolate); + if (!maybeObject->IsObject()) { + return ValueTuple{ nullptr, 0, DupString("Not an object") }; + } + + // We can safely call `ToLocalChecked`, because + // we've just created the local object above. + v8::Local object = maybeObject->ToObject(ctx).ToLocalChecked(); + + v8::MaybeLocal maybeFieldName = v8::String::NewFromUtf8(isolate, field); + + v8::Local localValue; + + if (maybeFieldName.IsEmpty()) { + localValue = v8::Undefined(isolate); + } + else { + v8::MaybeLocal maybeLocalValue = object->Get(ctx, maybeFieldName.ToLocalChecked()); + + if (maybeLocalValue.IsEmpty()) { + localValue = v8::Undefined(isolate); + } + else { + localValue = maybeLocalValue.ToLocalChecked(); + } + } + + return ValueTuple{ + new Value(isolate, localValue), + v8_Value_KindsFromLocal(localValue), + nullptr, + }; + } + + V8CBRIDGE_API ValueTuple v8_Value_GetIdx(ContextPtr ctxptr, PersistentValuePtr valueptr, int idx) { + VALUE_SCOPE(ctxptr); + + Value* value = static_cast(valueptr); + v8::Local maybeObject = value->Get(isolate); + if (!maybeObject->IsObject()) { + return ValueTuple{ nullptr, 0, DupString("Not an object") }; + } + + v8::Local obj; + if (maybeObject->IsArrayBuffer()) { + v8::ArrayBuffer* bufPtr = v8::ArrayBuffer::Cast(*maybeObject); + if (idx < bufPtr->GetContents().ByteLength()) { + obj = v8::Number::New(isolate, ((unsigned char*)bufPtr->GetContents().Data())[idx]); + } + else { + obj = v8::Undefined(isolate); + } + } + else { + // We can safely call `ToLocalChecked`, because + // we've just created the local object above. + v8::Local object = maybeObject->ToObject(ctx).ToLocalChecked(); + obj = object->Get(ctx, uint32_t(idx)).ToLocalChecked(); + } + return ValueTuple{ new Value(isolate, obj), v8_Value_KindsFromLocal(obj), nullptr }; + } + + V8CBRIDGE_API Error v8_Value_Set(ContextPtr ctxptr, PersistentValuePtr valueptr, + const char* field, PersistentValuePtr new_valueptr) { + VALUE_SCOPE(ctxptr); + + Value* value = static_cast(valueptr); + v8::Local maybeObject = value->Get(isolate); + if (!maybeObject->IsObject()) { + return DupString("Not an object"); + } + + // We can safely call `ToLocalChecked`, because + // we've just created the local object above. + v8::Local object = + maybeObject->ToObject(ctx).ToLocalChecked(); + + Value* newValue = static_cast(new_valueptr); + v8::Local newValueLocal = newValue->Get(isolate); + v8::MaybeLocal maybeField = v8::String::NewFromUtf8(isolate, field); + if (maybeField.IsEmpty()) { + return DupString("Something went wrong -- local value for field name could not be constructed."); + } + v8::Maybe res = + object->Set(ctx, maybeField.ToLocalChecked(), newValueLocal); + + if (res.IsNothing()) { + return DupString("Something went wrong -- set returned nothing."); + } + else if (!res.FromJust()) { + return DupString("Something went wrong -- set failed."); + } + return Error{ nullptr, 0 }; + } + + V8CBRIDGE_API Error v8_Value_SetIdx(ContextPtr ctxptr, PersistentValuePtr valueptr, + int idx, PersistentValuePtr new_valueptr) { + VALUE_SCOPE(ctxptr); + + Value* value = static_cast(valueptr); + v8::Local maybeObject = value->Get(isolate); + if (!maybeObject->IsObject()) { + return DupString("Not an object"); + } + + Value* new_value = static_cast(new_valueptr); + v8::Local new_value_local = new_value->Get(isolate); + if (maybeObject->IsArrayBuffer()) { + v8::ArrayBuffer* bufPtr = v8::ArrayBuffer::Cast(*maybeObject); + if (!new_value_local->IsNumber()) { + return DupString("Cannot assign non-number into array buffer"); + } + else if (idx >= bufPtr->GetContents().ByteLength()) { + return DupString("Cannot assign to an index beyond the size of an array buffer"); + } + else { + ((unsigned char*)bufPtr->GetContents().Data())[idx] = new_value_local->ToNumber(ctx).ToLocalChecked()->Value(); + } + } + else { + // We can safely call `ToLocalChecked`, because + // we've just created the local object above. + v8::Local object = maybeObject->ToObject(ctx).ToLocalChecked(); + + v8::Maybe res = object->Set(ctx, uint32_t(idx), new_value_local); + + if (res.IsNothing()) { + return DupString("Something went wrong -- set returned nothing."); + } + else if (!res.FromJust()) { + return DupString("Something went wrong -- set failed."); + } + } + + return Error{ nullptr, 0 }; + } + + V8CBRIDGE_API ValueTuple v8_Value_Call(ContextPtr ctxptr, + PersistentValuePtr funcptr, + PersistentValuePtr selfptr, + int argc, PersistentValuePtr* argvptr) { + VALUE_SCOPE(ctxptr); + + v8::TryCatch try_catch(isolate); + try_catch.SetVerbose(false); + + v8::Local func_val = static_cast(funcptr)->Get(isolate); + if (!func_val->IsFunction()) { + return ValueTuple{ nullptr, 0, DupString("Not a function") }; + } + v8::Local func = v8::Local::Cast(func_val); + + v8::Local self; + if (selfptr == nullptr) { + self = ctx->Global(); + } + else { + self = static_cast(selfptr)->Get(isolate); + } + + v8::Local* argv = new v8::Local[argc]; + for (int i = 0; i < argc; i++) { + argv[i] = static_cast(argvptr[i])->Get(isolate); + } + + v8::MaybeLocal result = func->Call(ctx, self, argc, argv); + + delete[] argv; + + if (result.IsEmpty()) { + return ValueTuple{ nullptr, 0, DupString(report_exception(isolate, ctx, try_catch)) }; + } + + v8::Local value = result.ToLocalChecked(); + return ValueTuple{ + static_cast(new Value(isolate, value)), + v8_Value_KindsFromLocal(value), + nullptr + }; + } + + V8CBRIDGE_API ValueTuple v8_Value_New(ContextPtr ctxptr, + PersistentValuePtr funcptr, + int argc, PersistentValuePtr* argvptr) { + VALUE_SCOPE(ctxptr); + + v8::TryCatch try_catch(isolate); + try_catch.SetVerbose(false); + + v8::Local func_val = static_cast(funcptr)->Get(isolate); + if (!func_val->IsFunction()) { + return ValueTuple{ nullptr, 0, DupString("Not a function") }; + } + v8::Local func = v8::Local::Cast(func_val); + + v8::Local* argv = new v8::Local[argc]; + for (int i = 0; i < argc; i++) { + argv[i] = static_cast(argvptr[i])->Get(isolate); + } + + v8::MaybeLocal result = func->NewInstance(ctx, argc, argv); + + delete[] argv; + + if (result.IsEmpty()) { + return ValueTuple{ nullptr, 0, DupString(report_exception(isolate, ctx, try_catch)) }; + } + + v8::Local value = result.ToLocalChecked(); + return ValueTuple{ + static_cast(new Value(isolate, value)), + v8_Value_KindsFromLocal(value), + nullptr + }; + } + + V8CBRIDGE_API void v8_Value_Release(ContextPtr ctxptr, PersistentValuePtr valueptr) { + if (valueptr == nullptr || ctxptr == nullptr) { + return; + } + + ISOLATE_SCOPE(static_cast(ctxptr)->isolate); + + Value* value = static_cast(valueptr); + value->Reset(); + delete value; + } + + V8CBRIDGE_API String v8_Value_String(ContextPtr ctxptr, PersistentValuePtr valueptr) { + VALUE_SCOPE(ctxptr); + + v8::Local value = static_cast(valueptr)->Get(isolate); + return DupString(isolate, value); + } + + V8CBRIDGE_API double v8_Value_Float64(ContextPtr ctxptr, PersistentValuePtr valueptr) { + VALUE_SCOPE(ctxptr); + v8::Local value = static_cast(valueptr)->Get(isolate); + v8::Maybe val = value->NumberValue(ctx); + if (val.IsNothing()) { + return 0; + } + return val.ToChecked(); + } + V8CBRIDGE_API int64_t v8_Value_Int64(ContextPtr ctxptr, PersistentValuePtr valueptr) { + VALUE_SCOPE(ctxptr); + v8::Local value = static_cast(valueptr)->Get(isolate); + v8::Maybe val = value->IntegerValue(ctx); + if (val.IsNothing()) { + return 0; + } + return val.ToChecked(); + } + V8CBRIDGE_API int v8_Value_Bool(ContextPtr ctxptr, PersistentValuePtr valueptr) { + VALUE_SCOPE(ctxptr); + v8::Local value = static_cast(valueptr)->Get(isolate); + return value->BooleanValue(isolate) ? 1 : 0; + } + + V8CBRIDGE_API ByteArray v8_Value_Bytes(ContextPtr ctxptr, PersistentValuePtr valueptr) { + VALUE_SCOPE(ctxptr); + + v8::Local value = static_cast(valueptr)->Get(isolate); + + v8::ArrayBuffer* bufPtr; + + if (value->IsTypedArray()) { + bufPtr = *v8::TypedArray::Cast(*value)->Buffer(); + } + else if (value->IsArrayBuffer()) { + bufPtr = v8::ArrayBuffer::Cast(*value); + } + else { + return ByteArray{ nullptr, 0 }; + } + + if (bufPtr == NULL) { + return ByteArray{ nullptr, 0 }; + } + + return ByteArray{ + static_cast(bufPtr->GetContents().Data()), + static_cast(bufPtr->GetContents().ByteLength()), + }; + } + + V8CBRIDGE_API HeapStatistics v8_Isolate_GetHeapStatistics(IsolatePtr isolate_ptr) { + if (isolate_ptr == nullptr) { + return HeapStatistics{ 0 }; + } + ISOLATE_SCOPE(static_cast(isolate_ptr)); + v8::HeapStatistics hs; + isolate->GetHeapStatistics(&hs); + return HeapStatistics{ + hs.total_heap_size(), + hs.total_heap_size_executable(), + hs.total_physical_size(), + hs.total_available_size(), + hs.used_heap_size(), + hs.heap_size_limit(), + hs.malloced_memory(), + hs.peak_malloced_memory(), + hs.does_zap_garbage() + }; + } + + V8CBRIDGE_API void v8_Isolate_LowMemoryNotification(IsolatePtr isolate_ptr) { + if (isolate_ptr == nullptr) { + return; + } + ISOLATE_SCOPE(static_cast(isolate_ptr)); + isolate->LowMemoryNotification(); + } + + V8CBRIDGE_API ValueTuple v8_Value_PromiseInfo(ContextPtr ctxptr, PersistentValuePtr valueptr, + int* promise_state) { + VALUE_SCOPE(ctxptr); + v8::Local value = static_cast(valueptr)->Get(isolate); + if (!value->IsPromise()) { // just in case + return ValueTuple{ nullptr, 0, DupString("Not a promise") }; + } + + v8::Promise* prom = v8::Promise::Cast(*value); + *promise_state = prom->State(); + if (prom->State() == v8::Promise::PromiseState::kPending) { + return ValueTuple{ nullptr, 0, nullptr }; + } + v8::Local res = prom->Result(); + return ValueTuple{ new Value(isolate, res), v8_Value_KindsFromLocal(res), nullptr }; + } + +} // extern "C" \ No newline at end of file diff --git a/v8_c_bridge.h b/v8_c_bridge.h new file mode 100644 index 0000000..e3fe5d0 --- /dev/null +++ b/v8_c_bridge.h @@ -0,0 +1,216 @@ +// The following ifdef block is the standard way of creating macros which make exporting +// from a DLL simpler. All files within this DLL are compiled with the V8CBRIDGE_EXPORTS +// symbol defined on the command line. This symbol should not be defined on any project +// that uses this DLL. This way any other project whose source files include this file see +// V8CBRIDGE_API functions as being imported from a DLL, whereas this DLL sees symbols +// defined with this macro as being exported. +#ifdef V8CBRIDGE_EXPORTS +#define V8CBRIDGE_API __declspec(dllexport) +#elif defined(_MSC_VER) +#define V8CBRIDGE_API __declspec(dllimport) +#else // for it to be possible to use v8_c_bridge.h in cgo define it as blank +#define V8CBRIDGE_API +#endif + +/* +// This class is exported from the dll +class V8CBRIDGE_API Cv8cbridge { +public: + Cv8cbridge(void); + // TODO: add your methods here. +}; + +extern V8CBRIDGE_API int nv8cbridge; + +V8CBRIDGE_API int fnv8cbridge(void); +*/ + +#include +#include + +#ifndef V8_C_BRIDGE_H +#define V8_C_BRIDGE_H + +#ifdef __cplusplus +extern "C" { +#endif + + V8CBRIDGE_API typedef void* IsolatePtr; + V8CBRIDGE_API typedef void* ContextPtr; + V8CBRIDGE_API typedef void* PersistentValuePtr; + + V8CBRIDGE_API void v8_Free(void* ptr); + + V8CBRIDGE_API typedef struct { + const char* ptr; + int len; + } String; + + V8CBRIDGE_API typedef String Error; + V8CBRIDGE_API typedef String StartupData; + V8CBRIDGE_API typedef String ByteArray; + + V8CBRIDGE_API typedef struct { + size_t total_heap_size; + size_t total_heap_size_executable; + size_t total_physical_size; + size_t total_available_size; + size_t used_heap_size; + size_t heap_size_limit; + size_t malloced_memory; + size_t peak_malloced_memory; + size_t does_zap_garbage; + } HeapStatistics; + + // NOTE! These values must exactly match the values in kinds.go. Any mismatch + // will cause kinds to be misreported. + V8CBRIDGE_API typedef enum { + kUndefined = 0, + kNull, + kName, + kString, + kSymbol, + kFunction, + kArray, + kObject, + kBoolean, + kNumber, + kExternal, + kInt32, + kUint32, + kDate, + kArgumentsObject, + kBooleanObject, + kNumberObject, + kStringObject, + kSymbolObject, + kNativeError, + kRegExp, + kAsyncFunction, + kGeneratorFunction, + kGeneratorObject, + kPromise, + kMap, + kSet, + kMapIterator, + kSetIterator, + kWeakMap, + kWeakSet, + kArrayBuffer, + kArrayBufferView, + kTypedArray, + kUint8Array, + kUint8ClampedArray, + kInt8Array, + kUint16Array, + kInt16Array, + kUint32Array, + kInt32Array, + kFloat32Array, + kFloat64Array, + kDataView, + kSharedArrayBuffer, + kProxy, + kWebAssemblyCompiledModule, + kNumKinds, + } Kind; + + // Each kind can be represent using only single 64 bit bitmask since there + // are less than 64 kinds so far. If this grows beyond 64 kinds, we can switch + // to multiple bitmasks or a dynamically-allocated array. + V8CBRIDGE_API typedef uint64_t KindMask; + + V8CBRIDGE_API typedef struct { + PersistentValuePtr Value; + KindMask Kinds; + Error error_msg; + } ValueTuple; + + V8CBRIDGE_API typedef struct { + String Funcname; + String Filename; + int Line; + int Column; + } CallerInfo; + + V8CBRIDGE_API typedef struct { int Major, Minor, Build, Patch; } Version; + V8CBRIDGE_API extern Version version; + + // pointer to callback function + V8CBRIDGE_API typedef ValueTuple(*GoCallbackHandlerPtr)(String id, CallerInfo info, int argc, ValueTuple* argv); + + // v8_Init must be called once before anything else. + V8CBRIDGE_API void v8_Init(GoCallbackHandlerPtr callback_handler); + + // typedef unsigned int uint32_t; + + /* V8CBRIDGE_API extern StartupData v8_CreateSnapshotDataBlob(const char* js); */ + + V8CBRIDGE_API extern IsolatePtr v8_Isolate_New(StartupData data); + V8CBRIDGE_API extern ContextPtr v8_Isolate_NewContext(IsolatePtr isolate); + V8CBRIDGE_API extern void v8_Isolate_Terminate(IsolatePtr isolate); + V8CBRIDGE_API extern void v8_Isolate_Release(IsolatePtr isolate); + + V8CBRIDGE_API extern HeapStatistics v8_Isolate_GetHeapStatistics(IsolatePtr isolate); + V8CBRIDGE_API extern void v8_Isolate_LowMemoryNotification(IsolatePtr isolate); + + V8CBRIDGE_API extern ValueTuple v8_Context_Run(ContextPtr ctx, + const char* code, const char* filename); + V8CBRIDGE_API extern PersistentValuePtr v8_Context_RegisterCallback(ContextPtr ctx, + const char* name, const char* id); + V8CBRIDGE_API extern PersistentValuePtr v8_Context_Global(ContextPtr ctx); + V8CBRIDGE_API extern void v8_Context_Release(ContextPtr ctx); + + V8CBRIDGE_API typedef enum { + tSTRING, + tBOOL, + tFLOAT64, + tINT64, + tOBJECT, + tARRAY, + tARRAYBUFFER, + tUNDEFINED, + tDATE, // uses Float64 for msec since Unix epoch + } ImmediateValueType; + + V8CBRIDGE_API typedef struct { + ImmediateValueType Type; + // Mem is used for String, ArrayBuffer, or Array. For Array, only len is + // used -- ptr is ignored. + ByteArray Mem; + int Bool; + double Float64; + int64_t Int64; + } ImmediateValue; + + V8CBRIDGE_API extern PersistentValuePtr v8_Context_Create(ContextPtr ctx, ImmediateValue val); + + V8CBRIDGE_API extern ValueTuple v8_Value_Get(ContextPtr ctx, PersistentValuePtr value, const char* field); + V8CBRIDGE_API extern Error v8_Value_Set(ContextPtr ctx, PersistentValuePtr value, + const char* field, PersistentValuePtr new_value); + V8CBRIDGE_API extern ValueTuple v8_Value_GetIdx(ContextPtr ctx, PersistentValuePtr value, int idx); + V8CBRIDGE_API extern Error v8_Value_SetIdx(ContextPtr ctx, PersistentValuePtr value, + int idx, PersistentValuePtr new_value); + V8CBRIDGE_API extern ValueTuple v8_Value_Call(ContextPtr ctx, + PersistentValuePtr func, + PersistentValuePtr self, + int argc, PersistentValuePtr* argv); + V8CBRIDGE_API extern ValueTuple v8_Value_New(ContextPtr ctx, + PersistentValuePtr func, + int argc, PersistentValuePtr* argv); + V8CBRIDGE_API extern void v8_Value_Release(ContextPtr ctx, PersistentValuePtr value); + V8CBRIDGE_API extern String v8_Value_String(ContextPtr ctx, PersistentValuePtr value); + + V8CBRIDGE_API extern double v8_Value_Float64(ContextPtr ctx, PersistentValuePtr value); + V8CBRIDGE_API extern int64_t v8_Value_Int64(ContextPtr ctx, PersistentValuePtr value); + V8CBRIDGE_API extern int v8_Value_Bool(ContextPtr ctx, PersistentValuePtr value); + V8CBRIDGE_API extern ByteArray v8_Value_Bytes(ContextPtr ctx, PersistentValuePtr value); + + V8CBRIDGE_API extern ValueTuple v8_Value_PromiseInfo(ContextPtr ctx, PersistentValuePtr value, + int* promise_state); + +#ifdef __cplusplus +} +#endif + +#endif // !defined(V8_C_BRIDGE_H) diff --git a/v8_create.go b/v8_create.go new file mode 100644 index 0000000..a4cabbc --- /dev/null +++ b/v8_create.go @@ -0,0 +1,289 @@ +package v8 + +import ( + "fmt" + "path" + "reflect" + "runtime" + "sort" + "strings" + "time" + "unicode" + "unsafe" +) + +// original: # cgo LDFLAGS: -pthread -L${SRCDIR}/libv8 -lv8_base -lv8_init -lv8_initializers -lv8_libbase -lv8_libplatform -lv8_libsampler -lv8_nosnapshot + +// #include +// #include +// #include "v8_c_bridge.h" +// #cgo CXXFLAGS: -I${SRCDIR} -I${SRCDIR}/include -fno-rtti -fpic -std=c++11 +// #cgo windows LDFLAGS: -pthread -L${SRCDIR}/libv8 -lv8_c_bridge +// #cgo linux,darwin LDFLAGS: -pthread -L${SRCDIR}/libv8 -lfuzzer_support -ljson_fuzzer -llib_wasm_fuzzer_common -lmulti_return_fuzzer -lparser_fuzzer -lregexp_builtins_fuzzer -lregexp_fuzzer -ltorque_base -ltorque_generated_definitions -ltorque_generated_initializers -ltorque_ls_base -lv8_base_without_compiler_0 -lv8_base_without_compiler_1 -lv8_compiler -lv8_compiler_opt -lv8_init -lv8_initializers -lv8_libbase -lv8_libplatform -lv8_libsampler -lv8_snapshot -lwasm_async_fuzzer -lwasm_code_fuzzer -lwasm_compile_fuzzer -lwasm_fuzzer -lwasm_module_runner -lwee8 -licui18n -licuuc -linspector -linspector_string_conversions +import "C" + +var float64Type = reflect.TypeOf(float64(0)) +var callbackType = reflect.TypeOf(Callback(nil)) +var stringType = reflect.TypeOf(string("")) +var valuePtrType = reflect.TypeOf((*Value)(nil)) +var timeType = reflect.TypeOf(time.Time{}) + +// Create maps Go values into corresponding JavaScript values. This value is +// created but NOT visible in the Context until it is explicitly passed to the +// Context (either via a .Set() call or as a callback return value). +// +// Create can automatically map the following types of values: +// * bool +// * all integers and floats are mapped to JS numbers (float64) +// * strings +// * maps (keys must be strings, values must be convertible) +// * time.Time values (converted to js Date object) +// * structs (exported field values must be convertible) +// * slices of convertible types +// * pointers to any convertible field +// * v8.Callback function (automatically bind'd) +// * *v8.Value (returned as-is) +// +// Any nil pointers are converted to undefined in JS. +// +// Values for elements in maps, structs, and slices may be any of the above +// types. +// +// When structs are being converted, any fields with json struct tags will +// respect the json naming entry. For example: +// var x = struct { +// Ignored string `json:"-"` +// Renamed string `json:"foo"` +// DefaultName string `json:",omitempty"` +// Bar string +// }{"a", "b", "c", "d"} +// will be converted as: +// { +// foo: "a", +// DefaultName: "b", +// Bar: "c", +// } +// Also, embedded structs (or pointers-to-structs) will get inlined. +// +// Byte slices tagged as 'v8:"arraybuffer"' will be converted into a javascript +// ArrayBuffer object for more efficient conversion. For example: +// var y = struct { +// Buf []byte `v8:"arraybuffer"` +// }{[]byte{1,2,3}} +// will be converted as +// { +// Buf: new Uint8Array([1,2,3]).buffer +// } +func (ctx *Context) Create(val interface{}) (*Value, error) { + v, _, err := ctx.create(reflect.ValueOf(val)) + return v, err +} + +func (ctx *Context) createVal(v C.ImmediateValue, kinds kindMask) *Value { + return ctx.newValue(C.v8_Context_Create(ctx.ptr, v), C.KindMask(kinds)) +} + +func getJsName(fieldName, jsonTag string) string { + jsonName := strings.TrimSpace(strings.Split(jsonTag, ",")[0]) + if jsonName == "-" { + return "" // skip this field + } + if jsonName == "" { + return fieldName // use the default name + } + return jsonName // explict name specified +} + +func (ctx *Context) create(val reflect.Value) (v *Value, allocated bool, err error) { + return ctx.createWithTags(val, []string{}) +} + +func (ctx *Context) createWithTags(val reflect.Value, tags []string) (v *Value, allocated bool, err error) { + if !val.IsValid() { + return ctx.createVal(C.ImmediateValue{Type: C.tUNDEFINED}, mask(KindUndefined)), true, nil + } + + if val.Type() == valuePtrType { + // This is the only time that we return an already-allocated Value, so + // allocated is false. + return val.Interface().(*Value), false, nil + } else if val.Type() == timeType { + msec := C.double(val.Interface().(time.Time).UnixNano()) / 1e6 + return ctx.createVal(C.ImmediateValue{Type: C.tDATE, Float64: msec}, unionKindDate), true, nil + } + + switch val.Kind() { + case reflect.Bool: + bval := C.int(0) + if val.Bool() { + bval = 1 + } + return ctx.createVal(C.ImmediateValue{Type: C.tBOOL, Bool: bval}, mask(KindBoolean)), true, nil + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Float32, reflect.Float64: + num := C.double(val.Convert(float64Type).Float()) + return ctx.createVal(C.ImmediateValue{Type: C.tFLOAT64, Float64: num}, mask(KindNumber)), true, nil + case reflect.String: + gostr := val.String() + str := C.ByteArray{ptr: C.CString(gostr), len: C.int(len(gostr))} + defer C.free(unsafe.Pointer(str.ptr)) + return ctx.createVal(C.ImmediateValue{Type: C.tSTRING, Mem: str}, unionKindString), true, nil + case reflect.UnsafePointer, reflect.Uintptr: + return nil, false, fmt.Errorf("Uintptr not supported: %#v", val.Interface()) + case reflect.Complex64, reflect.Complex128: + return nil, false, fmt.Errorf("Complex not supported: %#v", val.Interface()) + case reflect.Chan: + return nil, false, fmt.Errorf("Chan not supported: %#v", val.Interface()) + case reflect.Func: + if val.Type().ConvertibleTo(callbackType) { + name := path.Base(runtime.FuncForPC(val.Pointer()).Name()) + return ctx.Bind(name, val.Convert(callbackType).Interface().(Callback)), true, nil + } + return nil, false, fmt.Errorf("Func not supported: %#v", val.Interface()) + case reflect.Interface, reflect.Ptr: + return ctx.create(val.Elem()) + case reflect.Map: + if val.Type().Key() != stringType { + return nil, false, fmt.Errorf("Map keys must be strings, %s not allowed", val.Type().Key()) + } + ob := ctx.createVal(C.ImmediateValue{Type: C.tOBJECT}, mask(KindObject)) + keys := val.MapKeys() + sort.Sort(stringKeys(keys)) + for _, key := range keys { + v, wasAllocated, err := ctx.create(val.MapIndex(key)) + if err != nil { + return nil, false, fmt.Errorf("map key %q: %v", key.String(), err) + } + if err := ob.Set(key.String(), v); err != nil { + return nil, false, err + } + if wasAllocated { + v.release() + } + } + return ob, true, nil + case reflect.Struct: + ob := ctx.createVal(C.ImmediateValue{Type: C.tOBJECT}, mask(KindObject)) + return ob, true, ctx.writeStructFields(ob, val) + case reflect.Array, reflect.Slice: + arrayBuffer := false + for _, tag := range tags { + if strings.TrimSpace(tag) == "arraybuffer" { + arrayBuffer = true + } + } + + if arrayBuffer && val.Kind() == reflect.Slice && val.Type().Elem().Kind() == reflect.Uint8 { + // Special case for byte array -> arraybuffer + bytes := val.Bytes() + var ptr *C.char + if bytes != nil && len(bytes) > 0 { + ptr = (*C.char)(unsafe.Pointer(&val.Bytes()[0])) + } + ob := ctx.createVal( + C.ImmediateValue{ + Type: C.tARRAYBUFFER, + Mem: C.ByteArray{ptr: ptr, len: C.int(val.Len())}, + }, + unionKindArrayBuffer, + ) + return ob, true, nil + } else { + ob := ctx.createVal( + C.ImmediateValue{ + Type: C.tARRAY, + Mem: C.ByteArray{ptr: nil, len: C.int(val.Len())}, + }, + unionKindArray, + ) + for i := 0; i < val.Len(); i++ { + v, wasAllocated, err := ctx.create(val.Index(i)) + if err != nil { + return nil, false, fmt.Errorf("index %d: %v", i, err) + } + if err := ob.SetIndex(i, v); err != nil { + return nil, false, err + } + if wasAllocated { + v.release() + } + } + return ob, true, nil + } + } + panic("Unknown kind!") +} + +func (ctx *Context) writeStructFields(ob *Value, val reflect.Value) error { + t := val.Type() + + for i := 0; i < t.NumField(); i++ { + f := t.Field(i) + name := getJsName(f.Name, f.Tag.Get("json")) + if name == "" { + continue // skip field with tag `json:"-"` + } + + // Inline embedded fields. + if f.Anonymous { + sub := val.Field(i) + for sub.Kind() == reflect.Ptr && !sub.IsNil() { + sub = sub.Elem() + } + + if sub.Kind() == reflect.Struct { + err := ctx.writeStructFields(ob, sub) + if err != nil { + return fmt.Errorf("Writing embedded field %q: %v", f.Name, err) + } + continue + } + } + + if !unicode.IsUpper(rune(f.Name[0])) { + continue // skip unexported fields + } + + v8Tags := strings.Split(f.Tag.Get("v8"), ",") + v, wasAllocated, err := ctx.createWithTags(val.Field(i), v8Tags) + if err != nil { + return fmt.Errorf("field %q: %v", f.Name, err) + } + if err := ob.Set(name, v); err != nil { + return err + } + if wasAllocated { + v.release() + } + } + + // Also export any methods of the struct that match the callback type. + for i := 0; i < t.NumMethod(); i++ { + name := t.Method(i).Name + if !unicode.IsUpper(rune(name[0])) { + continue // skip unexported values + } + + m := val.Method(i) + if m.Type().ConvertibleTo(callbackType) { + v, wasAllocated, err := ctx.create(m) + if err != nil { + return fmt.Errorf("method %q: %v", name, err) + } + if err := ob.Set(name, v); err != nil { + return err + } + if wasAllocated { + v.release() + } + } + } + return nil +} + +type stringKeys []reflect.Value + +func (s stringKeys) Len() int { return len(s) } +func (s stringKeys) Swap(a, b int) { s[a], s[b] = s[b], s[a] } +func (s stringKeys) Less(a, b int) bool { return s[a].String() < s[b].String() } diff --git a/v8_go.cpp b/v8_go.cpp new file mode 100644 index 0000000..f3bcdeb --- /dev/null +++ b/v8_go.cpp @@ -0,0 +1,8 @@ +#include "v8_c_bridge.h" +#include "v8_go.h" + +extern "C" ValueTuple goCallbackHandler(String id, CallerInfo info, int argc, ValueTuple* argv); + +extern "C" void initGoCallbackHanlder() { + v8_Init(goCallbackHandler); +} \ No newline at end of file diff --git a/v8_go.h b/v8_go.h new file mode 100644 index 0000000..ffcbd91 --- /dev/null +++ b/v8_go.h @@ -0,0 +1,16 @@ +#include "v8_c_bridge.h" + +#ifndef V8_GO_H +#define V8_GO_H + +#ifdef __cplusplus +extern "C" { +#endif + +extern void initGoCallbackHanlder(); + +#ifdef __cplusplus +} +#endif + +#endif \ No newline at end of file diff --git a/v8_test.go b/v8_test.go new file mode 100644 index 0000000..03412e3 --- /dev/null +++ b/v8_test.go @@ -0,0 +1,1543 @@ +package v8 + +import ( + "encoding/json" + "errors" + "fmt" + "math" + "math/big" + "reflect" + "regexp" + "runtime" + "strings" + "sync" + "testing" + "time" +) + +func TestRunSimpleJS(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + res, err := ctx.Eval(` + var a = 10; + var b = 20; + var c = a+b; + c; + `, "test.js") + if err != nil { + t.Fatalf("Error evaluating javascript, err: %v", err) + } + if num := res.Int64(); num != 30 { + t.Errorf("Expected 30, got %v", res) + } +} + +func TestBoolConversion(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + + testcases := []struct { + js string + expected bool + isBool bool + }{ + // These are the only values that are KindBoolean. Everything else below is + // implicitly converted. + {`true`, true, true}, + {`false`, false, true}, + + // Super confusing in JS: + // !!(new Boolean(false)) == true + // !!(new Boolean(true)) == true + // That's because a non-undefined non-null Object in JS is 'true'. + // Also, neither of these are actually Boolean kinds -- they are + // BooleanObject, though. + {`new Boolean(true)`, true, false}, + {`new Boolean(false)`, true, false}, + {`undefined`, false, false}, + {`null`, false, false}, + {`[]`, true, false}, + {`[1]`, true, false}, + {`7`, true, false}, + {`"xyz"`, true, false}, + {`(() => 3)`, true, false}, + } + + for i, test := range testcases { + res, err := ctx.Eval(test.js, "test.js") + if err != nil { + t.Errorf("%d %#q: Failed to run js: %v", i, test.js, err) + } else if b := res.Bool(); b != test.expected { + t.Errorf("%d %#q: Expected bool of %v, but got %v", i, test.js, test.expected, b) + } else if res.IsKind(KindBoolean) != test.isBool { + t.Errorf("%d %#q: Expected this to be a bool kind, but it's %v", i, test.js, res.kindMask) + } + } +} + +func TestJsRegex(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + + re, err := ctx.Eval(`/foo.*bar/`, "test.js") + if err != nil { + t.Fatal(err) + } + if re.String() != `/foo.*bar/` { + t.Errorf("Bad stringification of regex: %#q", re) + } + if !re.IsKind(KindRegExp) { + t.Errorf("Wrong kind for regex: %v", re.kindMask) + } +} + +func TestNumberConversions(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + + res, err := ctx.Eval(`13`, "test.js") + if err != nil { + t.Fatal(err) + } + + if !res.IsKind(KindNumber) { + t.Errorf("Expected %q to be a number kind, but it's not: %q", res, res.kindMask) + } + if res.IsKind(KindFunction) { + t.Errorf("Expected %q to NOT be a function kind, but it is: %q", res, res.kindMask) + } + + if f64 := res.Float64(); f64 != 13.0 { + t.Errorf("Expected %q to eq 13.0, but got %f", res, f64) + } + + if i64 := res.Int64(); i64 != 13 { + t.Errorf("Expected %q to eq 13.0, but got %d", res, i64) + } +} + +func TestNumberConversionsFailForNonNumbers(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + + res, err := ctx.Eval(`undefined`, "test.js") + if err != nil { + t.Fatal(err) + } + + if res.IsKind(KindNumber) { + t.Errorf("Expected %q to NOT be a number kind, but it is: %q", res, res.kindMask) + } + + if f64 := res.Float64(); !math.IsNaN(f64) { + t.Errorf("Expected %q to be NaN, but got %f", res, f64) + } + + if i64 := res.Int64(); i64 != 0 { + t.Errorf("Expected %q to eq 0, but got %d", res, i64) + } +} + +func TestErrorRunningInvalidJs(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + + res, err := ctx.Eval(`kajsdfa91j23e`, "junk.js") + if err == nil { + t.Errorf("Expected error, but got result: %v", res) + } +} + +func TestValueString(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + + testcases := []struct{ jsCode, toString string }{ + // primitives: + {`"some string"`, `some string`}, + {`5`, `5`}, + {`5.123`, `5.123`}, + {`true`, `true`}, + {`false`, `false`}, + {`null`, `null`}, + {`undefined`, `undefined`}, + // more complicated objects: + {`(function x() { return 1 + 2; })`, `function x() { return 1 + 2; }`}, + {`([1,2,3])`, `1,2,3`}, + {`({x: 5})`, `[object Object]`}, + // basically a primitive, but an interesting case still: + {`JSON.stringify({x: 5})`, `{"x":5}`}, + } + + for i, test := range testcases { + res, err := ctx.Eval(test.jsCode, "test.js") + if err != nil { + t.Fatalf("Case %d: Error evaluating javascript %#q, err: %v", + i, test.jsCode, err) + } + if res.String() != test.toString { + t.Errorf("Case %d: Got %#q, expected %#q from running js %#q", + i, res.String(), test.toString, test.jsCode) + } + } +} + +func TestJsReturnStringWithEmbeddedNulls(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + res, err := ctx.Eval(`"foo\000bar"`, "test.js") + if err != nil { + t.Fatalf("Error evaluating javascript, err: %v", err) + } + if str := res.String(); str != "foo\000bar" { + t.Errorf("Expected 'foo\\000bar', got %q", str) + } +} + +func TestJsReturnUndefined(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + res, err := ctx.Eval(``, "undefined.js") + if err != nil { + t.Fatalf("Error evaluating javascript, err: %v", err) + } + if str := res.String(); str != "undefined" { + t.Errorf("Expected 'undefined', got %q", str) + } + if b := res.Bytes(); b != nil { + t.Errorf("Expected failure to map to bytes but got byte array of length %d", len(b)) + } +} + +func TestJsReturnArrayBuffer(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + res, err := ctx.Eval(`new ArrayBuffer(5)`, "undefined.js") + if err != nil { + t.Fatalf("Error evaluating javascript, err: %v", err) + } + b := res.Bytes() + if b == nil { + t.Errorf("Expected non-nil byte array but got nil buffer") + } + if len(b) != 5 { + t.Errorf("Expected byte array of length 5 but got %d", len(b)) + } +} + +func TestJsThrowString(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + res, err := ctx.Eval(`throw 'badness'`, "my_file.js") + if err == nil { + t.Fatalf("It worked but it wasn't supposed to: %v", res.String()) + } + match, _ := regexp.MatchString("Uncaught exception: badness", err.Error()) + if !match { + t.Error("Expected 'Uncaught exception: badness', got: ", err.Error()) + } +} + +func TestJsThrowError(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + res, err := ctx.Eval(`throw new Error('ooopsie')`, "my_file.js") + if err == nil { + t.Fatalf("It worked but it wasn't supposed to: %v", res.String()) + } + match, _ := regexp.MatchString("Uncaught exception: Error: ooopsie", err.Error()) + if !match { + t.Error("Expected 'Uncaught exception: Error: ooopsie', got: ", err.Error()) + } +} + +func TestReadFieldFromObject(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + res, err := ctx.Eval(`({foo:"bar"})`, "my_file.js") + if err != nil { + t.Fatalf("Error evaluating javascript, err: %v", err) + } + val, err := res.Get("foo") + if err != nil { + t.Fatalf("Error trying to get field: %v", err) + } + if str := val.String(); str != "bar" { + t.Errorf("Expected 'bar', got %q", str) + } +} + +func TestReadAndWriteIndexFromArrayBuffer(t *testing.T) { + t.Parallel() + + ctx := NewIsolate().NewContext() + val, err := ctx.Create(struct { + Data []byte `v8:"arraybuffer"` + }{[]byte{1, 2, 3}}) + if err != nil { + t.Fatal(err) + } + + data, err := val.Get("Data") + if err != nil { + t.Fatal(err) + } + + v, err := data.GetIndex(1) + if err != nil { + t.Fatal(err) + } else if num := v.Int64(); num != 2 { + t.Errorf("Wrong value, expected 2, got %v (%v)", num, v) + } + + v2, err := data.GetIndex(17) + if err != nil { + t.Fatal(err) + } else if str := v2.String(); str != "undefined" { + t.Errorf("Expected undefined, got %s", str) + } + + v3, err := data.GetIndex(2) + if err != nil { + t.Fatal(err) + } else if num := v3.Int64(); num != 3 { + t.Errorf("Expected undefined, got %v (%v)", num, v3) + } + + data.SetIndex(2, v) + v2, err = data.GetIndex(2) + if err != nil { + t.Fatal(err) + } else if num := v2.Int64(); num != 2 { + t.Errorf("Expected 2, got %v (%v)", num, v2) + } + + largeValue, err := ctx.Create(int(500)) + if err != nil { + t.Fatal(err) + } + + // 500 truncates to 500 % 256 == 244 + data.SetIndex(2, largeValue) + v4, err := data.GetIndex(2) + if err != nil { + t.Fatal(err) + } else if num := v4.Int64(); num != 244 { + t.Errorf("Expected 244, got %v (%v)", num, v4) + } + + negativeValue, err := ctx.Create(int(-55)) + if err != nil { + t.Fatal(err) + } + + // -55 "truncates" to -55 % 256 == 201 + data.SetIndex(2, negativeValue) + v5, err := data.GetIndex(2) + if err != nil { + t.Fatal(err) + } else if num := v5.Int64(); num != 201 { + t.Errorf("Expected 201, got %v (%v)", num, v5) + } +} + +func TestReadAndWriteIndexFromArray(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + val, err := ctx.Create([]int{1, 2, 3}) + if err != nil { + t.Fatal(err) + } + + v, err := val.GetIndex(1) + if err != nil { + t.Fatal(err) + } else if num := v.Int64(); num != 2 { + t.Errorf("Wrong value, expected 2, got %v (%v)", num, v) + } + + v2, err := val.GetIndex(17) + if err != nil { + t.Fatal(err) + } else if str := v2.String(); str != "undefined" { + t.Errorf("Expected undefined, got %s", str) + } + + val.SetIndex(17, v) + v2, err = val.GetIndex(17) + if err != nil { + t.Fatal(err) + } else if num := v2.Int64(); num != 2 { + t.Errorf("Expected 2, got %v (%v)", num, v2) + } +} + +func TestReadFieldFromNonObjectFails(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + res, err := ctx.Eval(`17`, "my_file.js") + if err != nil { + t.Fatalf("Error evaluating javascript, err: %v", err) + } + val, err := res.Get("foo") + if err == nil { + t.Fatalf("Missing error trying to get field, got %v", val) + } + val, err = res.GetIndex(3) + if err == nil { + t.Fatalf("Missing error trying to get field, got %v", val) + } +} + +func TestReadFieldFromGlobal(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + _, err := ctx.Eval(`foo = "bar";`, "my_file.js") + if err != nil { + t.Fatalf("Error evaluating javascript, err: %v", err) + } + val, err := ctx.Global().Get("foo") + if err != nil { + t.Fatalf("Error trying to get field: %v", err) + } + if str := val.String(); str != "bar" { + t.Errorf("Expected 'bar', got %q", str) + } +} + +func TestSetField(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + + three, err := ctx.Eval(`(3)`, "") + if err != nil { + t.Fatal(err) + } + + if err := ctx.Global().Set("foo", three); err != nil { + t.Fatal(err) + } + + res, err := ctx.Eval(`foo`, "") + if err != nil { + t.Fatal(err) + } + if num := res.Int64(); num != 3 { + t.Errorf("Expected 3, got %v (%v)", num, res) + } +} + +func TestRunningCodeInContextAfterThrowingError(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + _, err := ctx.Eval(` + function fail(a,b) { + this.c = a+b; + throw "some failure"; + } + function work(a,b) { + this.c = a+b+2; + } + x = new fail(3,5);`, "file1.js") + if err == nil { + t.Fatal("Expected an exception.") + } + + res, err := ctx.Eval(`y = new work(3,6); y.c`, "file2.js") + if err != nil { + t.Fatal("Expected it to work, but got:", err) + } + + if num := res.Int64(); num != 11 { + t.Errorf("Expected 11, got: %v (%v)", num, res) + } +} + +func TestManyContextsThrowingErrors(t *testing.T) { + t.Parallel() + + prog := ` + function work(N, depth, fail) { + if (depth == 0) { return 1; } + var sum = 0; + for (i = 0; i < N; i++) { sum *= work(N, depth-1); } + if (fail) { + throw "Failed"; + } + return sum; + }` + + const N = 100 // num parallel contexts + runtime.GOMAXPROCS(N) + + var done sync.WaitGroup + + iso := NewIsolate() + + done.Add(N) + for i := 0; i < N; i++ { + ctx := iso.NewContext() + + ctx.Eval(prog, "prog.js") + go func(ctx *Context, i int) { + cmd := fmt.Sprintf(`work(10000,100,%v)`, i%5 == 0) + ctx.Eval(cmd, "") + ctx.Eval(cmd, "") + ctx.Eval(cmd, "") + done.Done() + }(ctx, i) + } + done.Wait() +} + +func TestErrorsInNativeCode(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + _, err := ctx.Eval(`[].map(undefined);`, "map_undef.js") + if err == nil { + t.Fatal("Expected error.") + } + t.Log("Got expected error: ", err) +} + +func TestCallFunctionWithExplicitThis(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + this, _ := ctx.Eval(`(function(){ this.z = 3; return this; })()`, "") + add, _ := ctx.Eval(`((x,y)=>(x+y+this.z))`, "") + one, _ := ctx.Eval(`1`, "") + two, _ := ctx.Eval(`2`, "") + res, err := add.Call(this, one, two) + if err != nil { + t.Fatal(err) + } else if num := res.Int64(); num != 6 { + t.Errorf("Expected 6, got %v (%v)", num, res) + } +} + +func TestCallFunctionWithGlobalScope(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + ctx.Eval(`z = 4`, "") + add, _ := ctx.Eval(`((x,y)=>(x+y+this.z))`, "") + one, _ := ctx.Eval(`1`, "") + two, _ := ctx.Eval(`2`, "") + res, err := add.Call(nil, one, two) + if err != nil { + t.Fatal(err) + } else if num := res.Int64(); num != 7 { + t.Errorf("Expected 7, got %v (%v)", num, res) + } +} + +func TestCallFunctionFailsOnNonFunction(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + ob, _ := ctx.Eval(`({x:3})`, "") + res, err := ob.Call(nil) + if err == nil { + t.Fatalf("Expected err, but got %v", res) + } else if err.Error() != "Not a function" { + t.Errorf("Wrong error message: %q", err) + } +} + +func TestNewFunction(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + cons, _ := ctx.Eval(`(function(){ this.x = 1; })`, "") + obj, err := cons.New() + if err != nil { + t.Fatal(err) + } + res, err := obj.Get("x") + if err != nil { + t.Fatal(err) + } else if num := res.Int64(); num != 1 { + t.Errorf("Expected 1, got %v (%v)", num, res) + } +} + +func TestNewFunctionThrows(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + cons, _ := ctx.Eval(`(function(){ throw "oops"; })`, "") + obj, err := cons.New() + if err == nil { + t.Fatalf("Expected err, but got %v", obj) + } else if !strings.HasPrefix(err.Error(), "Uncaught exception: oops") { + t.Errorf("Wrong error message: %q", err) + } +} + +func TestNewFunctionWithArgs(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + cons, _ := ctx.Eval(`(function(x, y){ this.x = x + y; })`, "") + one, _ := ctx.Eval(`1`, "") + two, _ := ctx.Eval(`2`, "") + obj, err := cons.New(one, two) + if err != nil { + t.Fatal(err) + } + res, err := obj.Get("x") + if err != nil { + t.Fatal(err) + } else if num := res.Int64(); num != 3 { + t.Errorf("Expected 3, got %v (%v)", num, res) + } +} + +func TestNewFunctionFailsOnNonFunction(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + ob, _ := ctx.Eval(`({x:3})`, "") + res, err := ob.New() + if err == nil { + t.Fatalf("Expected err, but got %v", res) + } else if err.Error() != "Not a function" { + t.Errorf("Wrong error message: %q", err) + } +} + +func TestBind(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + + var expectedLoc Loc + + getLastCb := func(in CallbackArgs) (*Value, error) { + if in.Caller != expectedLoc { + t.Errorf("Wrong source location: %#v", in.Caller) + } + t.Logf("Args: %s", in.Args) + return in.Args[len(in.Args)-1], nil + } + + getLast := ctx.Bind("foo", getLastCb) + ctx.Global().Set("last", getLast) + + expectedLoc = Loc{"doit", "somefile.js", 3, 11} + res, err := ctx.Eval(` + function doit() { + return last(1,2,3); + } + doit() + `, "somefile.js") + if err != nil { + t.Fatal(err) + } else if num := res.Int64(); num != 3 { + t.Errorf("Expected 3, got %v (%v)", num, res) + } + + expectedLoc = Loc{"", "", 0, 0} // empty when called directly from Go + abc, _ := ctx.Eval("'abc'", "unused_filename.js") + xyz, _ := ctx.Eval("'xyz'", "unused_filename.js") + res, err = getLast.Call(nil, res, abc, xyz) + if err != nil { + t.Fatal(err) + } else if str := res.String(); str != "xyz" { + t.Errorf("Expected xyz, got %q", str) + } +} + +func TestBindReturnsError(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + + fails := ctx.Bind("fails", func(CallbackArgs) (*Value, error) { + return nil, errors.New("borked") + }) + res, err := fails.Call(nil) + if err == nil { + t.Fatalf("Expected error, but got %q instead", res) + } else if !strings.HasPrefix(err.Error(), "Uncaught exception: Error: borked") { + t.Errorf("Wrong error message: %q", err) + } +} + +func TestBindPanics(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + + panic := ctx.Bind("panic", func(CallbackArgs) (*Value, error) { panic("aaaah!!") }) + ctx.Global().Set("panic", panic) + res, err := ctx.Eval(`panic();`, "esplode.js") + if err == nil { + t.Error("Expected error, got ", res) + } else if matched, _ := regexp.MatchString("panic.*aaaah!!", err.Error()); !matched { + t.Errorf("Error should mention a panic and 'aaaah!!', but doesn't: %v", err) + } +} + +func TestBindName(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + + xyz := ctx.Bind("xyz", func(CallbackArgs) (*Value, error) { return nil, nil }) + if str := xyz.String(); str != "function xyz() { [native code] }" { + t.Errorf("Wrong function signature: %q", str) + } +} + +func TestBindNilReturn(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + + xyz := ctx.Bind("xyz", func(CallbackArgs) (*Value, error) { return nil, nil }) + res, err := xyz.Call(nil) + if err != nil { + t.Error(err) + } + if str := res.String(); str != "undefined" { + t.Errorf("Expected undefined, got %q", res) + } +} + +func TestTerminate(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + + waitUntilRunning := make(chan bool) + notify := ctx.Bind("notify", func(CallbackArgs) (*Value, error) { + waitUntilRunning <- true + return nil, nil + }) + ctx.Global().Set("notify", notify) + + go func() { + <-waitUntilRunning + ctx.Terminate() + }() + + done := make(chan bool) + + go func() { + res, err := ctx.Eval(` + notify(); + while(1) {} + `, "test.js") + + if err == nil { + t.Error("Expected an error, but got result: ", res) + } + + done <- true + }() + + select { + case <-done: + // yay, it worked! + case <-time.After(time.Second): + t.Fatal("Terminate didn't terminate :/") + } +} + +func TestSnapshot(t *testing.T) { + t.Parallel() + snapshot := CreateSnapshot("zzz='hi there!';") + ctx := NewIsolateWithSnapshot(snapshot).NewContext() + + res, err := ctx.Eval(`zzz`, "script.js") + if err != nil { + t.Fatal(err) + } + if str := res.String(); str != "hi there!" { + t.Errorf("Expected 'hi there!' got %s", str) + } +} + +func TestSnapshotBadJs(t *testing.T) { + t.Parallel() + snapshot := CreateSnapshot("This isn't yo mama's snapshot!") + + if snapshot.data.ptr != nil { + t.Error("Expected nil ptr") + } + + ctx := NewIsolateWithSnapshot(snapshot).NewContext() + + _, err := ctx.Eval(`zzz`, "script.js") + if err == nil { + t.Fatal("Expected error because zzz should be undefined.") + } +} + +func TestEs6Destructuring(t *testing.T) { + if Version.Major < 5 { + t.Skip("V8 versions before 5.* don't support destructuring.") + } + + t.Parallel() + ctx := NewIsolate().NewContext() + + bar, err := ctx.Eval(` + const f = (n) => ({foo:n, bar:n+1}); + var {foo, bar} = f(5); + bar + `, "test.js") + if err != nil { + t.Fatal(err) + } + if num := bar.Int64(); num != 6 { + t.Errorf("Expected 6, got %v (%v)", num, bar) + } +} + +func TestJsonExport(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + + var json_stringify *Value + if json, err := ctx.Global().Get("JSON"); err != nil { + t.Fatal(err) + } else if json_stringify, err = json.Get("stringify"); err != nil { + t.Fatal(err) + } + + some_result, err := ctx.Eval("(() => ({a:3,b:'xyz',c:true}))()", "test.js") + if err != nil { + t.Fatal(err) + } + + res, err := json_stringify.Call(json_stringify, some_result) + if err != nil { + t.Fatal(err) + } + + if str := res.String(); str != `{"a":3,"b":"xyz","c":true}` { + t.Errorf("Wrong JSON result, got: %s", str) + } +} + +func TestValueReleaseMoreThanOnceIsOk(t *testing.T) { + t.Parallel() + iso := NewIsolate() + ctx := iso.NewContext() + + res, err := ctx.Eval("5", "test.js") + if err != nil { + t.Fatal(err) + } + res.release() + res.release() + res.release() + res.release() + + ctx.release() + iso.release() + ctx.release() + ctx.release() + iso.release() + iso.release() +} + +func TestSharingValuesAmongContextsInAnIsolate(t *testing.T) { + t.Parallel() + iso := NewIsolate() + ctx1, ctx2 := iso.NewContext(), iso.NewContext() + + // Create a value in ctx1 + foo, err := ctx1.Eval(`foo = {x:6,y:true,z:"asdf"}; foo`, "ctx1.js") + if err != nil { + t.Fatal(err) + } + + // Set that value into ctx2 + err = ctx2.Global().Set("bar", foo) + if err != nil { + t.Fatal(err) + } + // ...and verify that it has the same value. + res, err := ctx2.Eval(`bar.z`, "ctx2.js") + if err != nil { + t.Fatal(err) + } + if str := res.String(); str != "asdf" { + t.Errorf("Expected 'asdf', got %q", str) + } + + // Now modify that value in ctx2 + _, err = ctx2.Eval("bar.z = 'xyz';", "ctx2b.js") + if err != nil { + t.Fatal(err) + } + + // ...and verify that it got changed in ctx1 as well! + res, err = ctx1.Eval("foo.z", "ctx1b.js") + if err != nil { + t.Fatal(err) + } + if str := res.String(); str != "xyz" { + t.Errorf("Expected 'xyz', got %q", str) + } +} + +func TestCreateSimple(t *testing.T) { + t.Parallel() + iso := NewIsolate() + ctx := iso.NewContext() + + callback := func(CallbackArgs) (*Value, error) { return nil, nil } + + tm := time.Date(2018, 5, 8, 3, 4, 5, 17, time.Local) + + var testcases = []struct { + val interface{} + str string + }{ + {nil, "undefined"}, + {3, "3"}, + {3.7, "3.7"}, + {true, "true"}, + {"asdf", "asdf"}, + {callback, "function v8.TestCreateSimple.func1() { [native code] }"}, + {map[string]int{"foo": 1, "bar": 2}, "[object Object]"}, + {struct { + Foo int + Bar bool + }{3, true}, "[object Object]"}, + {[]interface{}{1, true, "three"}, "1,true,three"}, + {tm, tm.Format("Mon Jan 02 2006 15:04:05 GMT-0700 (MST)")}, + {&tm, tm.Format("Mon Jan 02 2006 15:04:05 GMT-0700 (MST)")}, + } + + for i, test := range testcases { + val, err := ctx.Create(test.val) + if err != nil { + t.Errorf("%d: Failed to create %#v: %v", i, test, err) + continue + } + if str := val.String(); str != test.str { + t.Errorf("Expected %q, got %q", test.str, str) + } + } +} + +func TestCreateComplex(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + + fn := func(CallbackArgs) (*Value, error) { return ctx.Create("abc") } + type Struct struct { + Val string + secret bool + Sub interface{} + } + + zzz1 := &Struct{Val: "BOOM!"} + zzz2 := &zzz1 + zzz3 := &zzz2 + zzz4 := &zzz3 + zzz5 := &zzz4 // zzz5 is a *****Struct. Make sure pointers work! + + fn2 := ctx.Bind("fn2", fn) + + val, err := ctx.Create([]Struct{ + {"asdf", false, nil}, + {"foo", true, map[string]interface{}{ + "num": 123.123, + "fn": fn, + "fn2": fn2, + "list": []float64{1, 2, 3}, + "valArr": []*Value{fn2}, + }}, + {"*****Struct", false, zzz5}, + {"bufbuf", false, struct { + Data []byte `v8:"arraybuffer"` + }{[]byte{1, 2, 3, 4}}}, + {"emptybuf", false, struct { + Data []byte `v8:"arraybuffer"` + }{[]byte{}}}, + {"structWithValue", false, struct{ *Value }{fn2}}, + }) + if err != nil { + t.Fatal(err) + } + + if fn2.ptr == nil { + t.Error("Create should not release *Values allocated prior to the call.") + } + + ctx.Global().Set("mega", val) + + if res, err := ctx.Eval(`mega[1].Sub.fn2()`, "test.js"); err != nil { + t.Fatal(err) + } else if str := res.String(); str != "abc" { + t.Errorf("Expected abc, got %q", str) + } + + if res, err := ctx.Eval(`mega[1].Sub.fn()`, "test.js"); err != nil { + t.Fatal(err) + } else if str := res.String(); str != "abc" { + t.Errorf("Expected abc, got %q", str) + } + + if res, err := ctx.Eval(`mega[1].secret`, "test.js"); err != nil { + t.Fatal(err) + } else if str := res.String(); str != "undefined" { + t.Errorf("Expected undefined trying to access non-existent field 'secret', but got %q", str) + } + + if res, err := ctx.Eval(`mega[2].Sub.Val`, "test.js"); err != nil { + t.Fatal(err) + } else if str := res.String(); str != "BOOM!" { + t.Errorf("Expected 'BOOM1', but got %q", str) + } + + if res, err := ctx.Eval(`mega[3].Sub.Data.byteLength`, "test.js"); err != nil { + t.Fatal(err) + } else if str := res.String(); str != "4" { + t.Errorf("Expected array buffer length of '4', but got %q", str) + } + + if res, err := ctx.Eval(`new Uint8Array(mega[3].Sub.Data)[2]`, "test.js"); err != nil { + t.Fatal(err) + } else if str := res.String(); str != "3" { + t.Errorf("Expected array buffer value at index 2 of '3', but got %q", str) + } + + if res, err := ctx.Eval(`mega[4].Sub.Data.byteLength`, "test.js"); err != nil { + t.Fatal(err) + } else if str := res.String(); str != "0" { + t.Errorf("Expected empty array buffer length of '0', but got %q", str) + } +} + +func TestJsCreateArrayBufferRoundtrip(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + val, err := ctx.Create(struct { + Data []byte `v8:"arraybuffer"` + }{[]byte{1, 2, 3, 4}}) + if err != nil { + t.Fatal(err) + } + + ctx.Global().Set("buf", val) + + if _, err := ctx.Eval(` + var view = new Uint8Array(buf.Data) + view[3] = view[0] + view[1] + view[2] + `, "test.js"); err != nil { + t.Fatal(err) + } + + data, err := val.Get("Data") + if err != nil { + t.Fatal(err) + } + + v1, err := data.GetIndex(0) + if err != nil { + t.Fatal(err) + } else if str := v1.String(); str != "1" { + t.Errorf("Expected first value of '1' but got %q", str) + } + + v2, err := data.GetIndex(3) + if err != nil { + t.Fatal(err) + } else if str := v2.String(); str != "6" { + t.Errorf("Expected fourth value of '6' but got %q", str) + } + + err = data.SetIndex(2, v1) + if err != nil { + t.Fatal(err) + } + + v3, err := data.GetIndex(2) + if err != nil { + t.Fatal(err) + } else if str := v3.String(); str != "1" { + t.Errorf("Expected third value of '1' but got %q", str) + } + + bytes := data.Bytes() + if !reflect.DeepEqual(bytes, []byte{1, 2, 1, 6}) { + t.Errorf("Expected byte array [1,2,1,6] but got %q", bytes) + } + + // Out of range + err = data.SetIndex(7, v1) + if err == nil { + t.Errorf("Expected error assigning out of range of array buffer") + } +} + +func TestTypedArrayBuffers(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + + uint8Array, err := ctx.Eval(` + new Uint8Array(4).fill(4, 1, 3) // taken from a MDN example + `, "test.js") + if err != nil { + t.Fatal(err) + } + + bytes := uint8Array.Bytes() + if !reflect.DeepEqual(bytes, []byte{0, 4, 4, 0}) { + t.Errorf("Expected byte array [0,4,4,0] but got %q", bytes) + } +} + +func TestCreateJsonTags(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + + type A struct { + Embedded string `json:"embedded"` + AlsoIngored string `json:"-"` + } + type Nil struct{ Missing string } + type B struct { + *A + *Nil + Ignored string `json:"-"` + Renamed string `json:"foo"` + DefaultName string `json:",omitempty"` + Bar string + } + + var x = B{&A{"a", "x"}, nil, "y", "b", "c", "d"} + val, err := ctx.Create(x) + if err != nil { + t.Fatal(err) + } + + const expected = `{"embedded":"a","foo":"b","DefaultName":"c","Bar":"d"}` + if data, err := json.Marshal(val); err != nil { + t.Fatal(err) + } else if string(data) != expected { + t.Errorf("Incorrect object:\nExp: %s\nGot: %s", expected, data) + } +} + +func TestParseJson(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + val, err := ctx.ParseJson(`{"foo":"bar","bar":3}`) + if err != nil { + t.Fatal(err) + } + if res, err := val.Get("foo"); err != nil { + t.Fatal(err) + } else if str := res.String(); str != "bar" { + t.Errorf("Expected 'bar', got %q", str) + } + + // Make sure it fails if the data is not actually json. + val, err = ctx.ParseJson(`this is not json`) + if err == nil { + t.Errorf("Expected an error, but got %s", val) + } +} + +func TestJsonMarshal(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + val, err := ctx.Eval(`(()=>({ + blah: 3, + muck: true, + label: "lala", + missing: () => ( "functions get dropped" ) + }))()`, "test.js") + if err != nil { + t.Fatal(err) + } + + data, err := json.Marshal(val) + if err != nil { + t.Fatal(err) + } + const expected = `{"blah":3,"muck":true,"label":"lala"}` + if string(data) != expected { + t.Errorf("Expected: %q\nGot : %q", expected, string(data)) + } +} + +func TestCallbackProvideCorrectContext(t *testing.T) { + t.Parallel() + + // greet is a generate callback handler that is not associated with a + // particular context -- it uses the provided context to create a value + // to return, even when used from different isolates. + greet := func(in CallbackArgs) (*Value, error) { + return in.Context.Create("Hello " + in.Arg(0).String()) + } + + ctx1, ctx2 := NewIsolate().NewContext(), NewIsolate().NewContext() + ctx1.Global().Set("greet", ctx1.Bind("greet", greet)) + ctx2.Global().Set("greet", ctx2.Bind("greet", greet)) + + alice, err1 := ctx1.Eval("greet('Alice')", "ctx1.js") + if err1 != nil { + t.Errorf("Context 1 failed: %v", err1) + } else if str := alice.String(); str != "Hello Alice" { + t.Errorf("Bad result: %q", str) + } + + bob, err2 := ctx2.Eval("greet('Bob')", "ctx2.js") + if err2 != nil { + t.Errorf("Context 2 failed: %v", err2) + } else if str := bob.String(); str != "Hello Bob" { + t.Errorf("Bad result: %q", str) + } +} + +func TestCircularReferenceJsonMarshalling(t *testing.T) { + t.Parallel() + + ctx := NewIsolate().NewContext() + circ, err := ctx.Eval("var test = {}; test.blah = test", "circular.js") + if err != nil { + t.Fatalf("Failed to create object with circular ref: %v", err) + } + data, err := circ.MarshalJSON() + if err == nil { + t.Fatalf("Expected error marshalling circular ref, but got: `%s`", data) + } else if !strings.Contains(err.Error(), "circular") { + t.Errorf("Expected a circular reference error, but got: %v", err) + } +} + +func TestIsolateFinalizer(t *testing.T) { + t.Parallel() + iso := NewIsolate() + + fin := make(chan bool) + // Reset the finalizer so we test if it is working + runtime.SetFinalizer(iso, nil) + runtime.SetFinalizer(iso, func(iso *Isolate) { + close(fin) + iso.release() + }) + iso = nil + + if !runGcUntilReceivedOrTimedOut(fin, 4*time.Second) { + t.Fatal("finalizer of iso didn't run, no context is associated with the iso.") + } + + iso = NewIsolate() + iso.NewContext() + + fin = make(chan bool) + // Reset the finalizer so we test if it is working + runtime.SetFinalizer(iso, nil) + runtime.SetFinalizer(iso, func(iso *Isolate) { + close(fin) + iso.release() + }) + iso = nil + + if !runGcUntilReceivedOrTimedOut(fin, 4*time.Second) { + t.Fatal("finalizer of iso didn't run, iso created one context.") + } +} + +func TestContextFinalizer(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + + fin := make(chan bool) + // Reset the finalizer so we test if it is working + runtime.SetFinalizer(ctx, nil) + runtime.SetFinalizer(ctx, func(ctx *Context) { + close(fin) + ctx.release() + }) + ctx = nil + + if !runGcUntilReceivedOrTimedOut(fin, 4*time.Second) { + t.Fatal("finalizer of ctx didn't run") + } +} + +func TestContextFinalizerWithValues(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + + greet := func(in CallbackArgs) (*Value, error) { + return in.Context.Create("Hello " + in.Arg(0).String()) + } + ctx.Global().Set("greet", ctx.Bind("greet", greet)) + val, err := ctx.Eval("greet('bob')", "") + if err != nil { + t.Fatal(err) + } + t.Log(val.String()) + + fin := make(chan bool) + // Reset the finalizer so we test if it is working + runtime.SetFinalizer(ctx, nil) + runtime.SetFinalizer(ctx, func(ctx *Context) { + close(fin) + ctx.release() + }) + ctx = nil + + if !runGcUntilReceivedOrTimedOut(fin, 4*time.Second) { + t.Fatal("finalizer of ctx didn't run after creating a value") + } +} + +func TestIsolateGetHeapStatistics(t *testing.T) { + iso := NewIsolate() + initHeap := iso.GetHeapStatistics() + if initHeap.TotalHeapSize <= 0 { + t.Fatalf("expected heap to be more than zero, got: %d\n", initHeap.TotalHeapSize) + } + + ctx := iso.NewContext() + for i := 0; i < 10000; i++ { + ctx.Create(map[string]interface{}{ + "hello": map[string]interface{}{ + "world": []string{"foo", "bar"}, + }, + }) + } + + midHeap := iso.GetHeapStatistics() + if midHeap.TotalHeapSize <= initHeap.TotalHeapSize { + t.Fatalf("expected heap to grow after creating context, got: %d\n", midHeap.TotalHeapSize) + } + + beforeNotifyHeap := iso.GetHeapStatistics() + + iso.SendLowMemoryNotification() + + finalHeap := iso.GetHeapStatistics() + if finalHeap.TotalHeapSize >= beforeNotifyHeap.TotalHeapSize { + t.Fatalf("expected heap to reduce after terminating context, got: %d\n", finalHeap.TotalHeapSize) + } + +} + +func runGcUntilReceivedOrTimedOut(signal <-chan bool, timeout time.Duration) bool { + expired := time.After(timeout) + for { + select { + case <-signal: + return true + case <-expired: + return false + case <-time.After(10 * time.Millisecond): + runtime.GC() + } + } +} + +// This is bad, and should be fixed! See https://github.com/augustoroman/v8/issues/21 +func TestMicrotasksIgnoreUnhandledPromiseRejection(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + var logs []string + ctx.Global().Set("log", ctx.Bind("log", func(in CallbackArgs) (*Value, error) { + logs = append(logs, in.Arg(0).String()) + return nil, nil + })) + output, err := ctx.Eval(` + log('start'); + let p = new Promise((_, reject) => { log("reject:'err'"); reject('err'); }); + p.then(v => log('then:'+v)); + log('done'); + `, `test.js`) + + expectedLogs := []string{ + "start", + "reject:'err'", + "done", + } + + if !reflect.DeepEqual(logs, expectedLogs) { + t.Errorf("Wrong logs.\nGot: %#q\nExp: %#q", logs, expectedLogs) + } + + // output should be 'undefined' because log('done') doesn't return anything. + if output.String() != "undefined" { + t.Errorf("Unexpected output value: %v", output) + } + + if err != nil { + t.Errorf("Expected err to be nil since we ignore unhandled promise rejections. "+ + "In the future, hopefully we'll handle these better -- in fact, maybe err "+ + "is not-nil right now because you fixed that! Got err = %v", err) + } +} + +func TestValueKind(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + + // WASM: This wasm code corresponds to the WAT: + // (module + // (func $add (param $x i32) (param $y i32) (result i32) + // (i32.add (get_local $x) (get_local $y))) + // (export "add" $add)) + // That exports an "add(a,b int32) int32" function. + const wasmCode = ` + new Uint8Array([0,97,115,109,1,0,0,0,1,135,128,128,128,0,1,96,2,127,127,1,127,3,130,128,128, + 128,0,1,0,6,129,128,128,128,0,0,7,135,128,128,128,0,1,3,97,100,100,0,0,10,141,128,128,128, + 0,1,135,128,128,128,0,0,32,0,32,1,106,11])` + + const wasmModule = `new WebAssembly.Module(` + wasmCode + `)` + + toTest := map[string]kindMask{ + `undefined`: mask(KindUndefined), + `null`: mask(KindNull), + `"test"`: unionKindString, + `Symbol("test")`: unionKindSymbol, + `(function(){})`: unionKindFunction, + `[]`: unionKindArray, + `new Object()`: mask(KindObject), + `true`: mask(KindBoolean), + `false`: mask(KindBoolean), + `1`: mask(KindNumber, KindInt32, KindUint32), + `new Date()`: unionKindDate, + `(function(){return arguments})()`: unionKindArgumentsObject, + `new Boolean`: unionKindBooleanObject, + `new Number`: unionKindNumberObject, + `new String`: unionKindStringObject, + `new Object(Symbol("test"))`: unionKindSymbolObject, + `/regexp/`: unionKindRegExp, + `new Promise((res, rjt)=>{})`: unionKindPromise, + `new Map()`: unionKindMap, + `new Set()`: unionKindSet, + `new ArrayBuffer(0)`: unionKindArrayBuffer, + `new Uint8Array(0)`: unionKindUint8Array, + `new Uint8ClampedArray(0)`: unionKindUint8ClampedArray, + `new Int8Array(0)`: unionKindInt8Array, + `new Uint16Array(0)`: unionKindUint16Array, + `new Int16Array(0)`: unionKindInt16Array, + `new Uint32Array(0)`: unionKindUint32Array, + `new Int32Array(0)`: unionKindInt32Array, + `new Float32Array(0)`: unionKindFloat32Array, + `new Float64Array(0)`: unionKindFloat64Array, + `new DataView(new ArrayBuffer(0))`: unionKindDataView, + `new SharedArrayBuffer(0)`: unionKindSharedArrayBuffer, + `new Proxy({}, {})`: unionKindProxy, + `new WeakMap`: unionKindWeakMap, + `new WeakSet`: unionKindWeakSet, + `(async function(){})`: unionKindAsyncFunction, + `(function* (){})`: unionKindGeneratorFunction, + `function* gen(){}; gen()`: unionKindGeneratorObject, + `new Map()[Symbol.iterator]()`: unionKindMapIterator, + `new Set()[Symbol.iterator]()`: unionKindSetIterator, + `new EvalError`: unionKindNativeError, + wasmModule: unionKindWebAssemblyCompiledModule, + + // TODO! + // ``: KindExternal, + } + + for script, kindMask := range toTest { + v, err := ctx.Eval(script, "kind_test.js") + if err != nil { + t.Errorf("%#q: failed: %v", script, err) + } else if v.kindMask != kindMask { + t.Errorf("%#q: expected result to be %q, but got %q", script, kindMask, v.kindMask) + } + } +} + +func TestDate(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + + res, err := ctx.Eval(`new Date("2018-05-08T08:16:46.918Z")`, "date.js") + if err != nil { + t.Fatal(err) + } + + tm, err := res.Date() + if err != nil { + t.Error(err) + } else if tm.UnixNano() != 1525767406918*1e6 { + t.Errorf("Wrong date: %q", tm) + } +} + +func TestPromise(t *testing.T) { + t.Parallel() + ctx := NewIsolate().NewContext() + + // Pending + v, err := ctx.Eval(`new Promise((resolve, reject)=>{})`, "pending-promise.js") + if err != nil { + t.Fatal(err) + } + + if state, result, err := v.PromiseInfo(); err != nil { + t.Error(err) + } else if state != PromiseStatePending { + t.Errorf("Expected promise to be pending, but got %v", state) + } else if result != nil { + t.Errorf("Expected nil result since it's pending, but got %v", result) + } + + // Resolved + v, err = ctx.Eval(`new Promise((resolve, reject)=>{resolve(42)})`, "resolved-promise.js") + if err != nil { + t.Fatal(err) + } + + if state, result, err := v.PromiseInfo(); err != nil { + t.Error(err) + } else if state != PromiseStateResolved { + t.Errorf("Expected promise to be resolved, but got %v", state) + } else if result == nil { + t.Errorf("Expected a result since it's resolved, but got nil") + } else if !result.IsKind(KindNumber) { + t.Errorf("Expected the result to be a number, but it's: %v (%v)", result.kindMask, result) + } else if result.Int64() != 42 { + t.Errorf("Expected the result to be 42, but got %v", result) + } + + // Rejected + v, err = ctx.Eval(`new Promise((resolve, reject)=>{reject(new Error("nope"))})`, "rejected-promise.js") + if err != nil { + t.Fatal(err) + } + + if state, result, err := v.PromiseInfo(); err != nil { + t.Error(err) + } else if state != PromiseStateRejected { + t.Errorf("Expected promise to be rejected, but got %v", state) + } else if result == nil { + t.Errorf("Expected an error result since it's rejected, but got nil") + } else if !result.IsKind(KindNativeError) { + t.Errorf("Expected the result to be an error, but it's: %v (%v)", result.kindMask, result) + } else if result.String() != `Error: nope` { + t.Errorf("Expected the error message to be 'nope', but got %#q", result) + } + + // Not a promise + v, err = ctx.Eval(`new Error('x')`, "not-a-promise.js") + if err != nil { + t.Fatal(err) + } + + if state, result, err := v.PromiseInfo(); err == nil { + t.Errorf("Expected an error, but got nil and state=%#v result=%#v", state, result) + } +} + +func TestPanicHandling(t *testing.T) { + // v8 runtime can register its own signal handlers which would interfere + // with Go's signal handlers which are needed for panic handling + defer func() { + if r := recover(); r != nil { + // if we reach this point, Go's panic mechanism is still intact + _, ok := r.(runtime.Error) + if !ok { + t.Errorf("expected runtime error, actual %v", r) + } + } + }() + + var f *big.Float + _ = NewIsolate() + _ = *f +} diff --git a/v8console/console.go b/v8console/console.go new file mode 100644 index 0000000..9390b42 --- /dev/null +++ b/v8console/console.go @@ -0,0 +1,126 @@ +// Package v8console provides a simple console implementation to allow JS to +// log messages. +// +// It supports the console.log, console.info, console.warn, and console.error +// functions and logs the result of .ToString() on each of the arguments. It +// can color warning and error messages, but does not support Chrome's fancy +// %c message styling. +package v8console + +import ( + "fmt" + "io" + + "github.com/augustoroman/v8" +) + +const ( + kRESET = "\033[0m" + kNO_COLOR = "" + kRED = "\033[91m" + kYELLOW = "\033[93m" +) + +// Config holds configuration for a particular console instance. +type Config struct { + // Prefix to prepend to every log message. + Prefix string + // Destination for all .log and .info calls. + Stdout io.Writer + // Destination for all .warn and .error calls. + Stderr io.Writer + // Whether to enable ANSI color escape codes in the output. + Colorize bool +} + +// Inject sets the global "console" object of the specified Context to bind +// .log, .info, .warn, and .error to call this Console object. If the console +// object already exists in the global namespace, only the log/info/warn/error +// properties are replaced. +func (c Config) Inject(ctx *v8.Context) { + ob, _ := ctx.Global().Get("console") + if ob == nil || ob.String() != "[object Object]" { + // If the object doesn't already exist, create a new object from scratch + // and inject the whole thing. + ctx, err := ctx.Create(map[string]interface{}{ + "log": c.Info, + "info": c.Info, + "warn": c.Warn, + "error": c.Error, + }) + if err != nil { + // This should never happen: our map is well-defined for ctx.Create. + panic(fmt.Errorf("cannot create ctx object: %v", err)) + } + ob = ctx + } else { + // If the console object already exists, just replace the logging + // methods. + functions := []struct { + name string + callback v8.Callback + }{ + {"log", c.Info}, + {"info", c.Info}, + {"warn", c.Warn}, + {"error", c.Error}, + } + for _, fn := range functions { + if err := ob.Set(fn.name, ctx.Bind(fn.name, fn.callback)); err != nil { + panic(fmt.Errorf("cannot set %s on console object: %v", fn.name, err)) + } + } + } + + // Update console object. + if err := ctx.Global().Set("console", ob); err != nil { + // This should never happen: Global() is always an object. + panic(fmt.Errorf("cannot set context into global: %v", err)) + } +} + +func (c Config) writeLog(w io.Writer, color string, vals ...interface{}) { + if color != "" && c.Colorize { + fmt.Fprint(w, color) + } + fmt.Fprint(w, c.Prefix) + fmt.Fprint(w, vals...) + if color != "" && c.Colorize { + fmt.Fprint(w, kRESET) + } + fmt.Fprint(w, "\n") +} +func (c Config) toInterface(vals []*v8.Value) []interface{} { + out := make([]interface{}, len(vals)) + for i, val := range vals { + out[i] = val + } + return out +} +func (c Config) toInterfaceWithLoc(caller v8.Loc, args []*v8.Value) []interface{} { + var vals []interface{} + vals = append(vals, fmt.Sprintf("[%s:%d] ", caller.Filename, caller.Line)) + vals = append(vals, c.toInterface(args)...) + return vals +} + +// Info is the v8 callback function that is registered for the console.log and +// console.info functions. +func (c Config) Info(in v8.CallbackArgs) (*v8.Value, error) { + c.writeLog(c.Stdout, kNO_COLOR, c.toInterface(in.Args)...) + return nil, nil +} + +// Warn is the v8 callback function that is registered for the console.warn +// functions. +func (c Config) Warn(in v8.CallbackArgs) (*v8.Value, error) { + c.writeLog(c.Stderr, kYELLOW, c.toInterfaceWithLoc(in.Caller, in.Args)...) + return nil, nil +} + +// Error is the v8 callback function that is registered for the console.error +// functions. +func (c Config) Error(in v8.CallbackArgs) (*v8.Value, error) { + c.writeLog(c.Stderr, kRED, c.toInterfaceWithLoc(in.Caller, in.Args)...) + return nil, nil +} diff --git a/v8console/console_snapshot.go b/v8console/console_snapshot.go new file mode 100644 index 0000000..8814fe3 --- /dev/null +++ b/v8console/console_snapshot.go @@ -0,0 +1,100 @@ +package v8console + +import ( + "fmt" + + "github.com/augustoroman/v8" +) + +const jsConsoleStub = `console = (function() { + var stored = []; + var exception = undefined; + function flush(new_console) { + stored.forEach(function(log) { + new_console[log.type].apply(new_console, log.args); + }); + return exception; + }; + function catch_exception(e) { + console.error('Failed to make snapshot:', e); + exception = e; + }; + return { + __flush: flush, + __catch: catch_exception, + log: function() { stored.push({type: 'log', args: arguments}); }, + info: function() { stored.push({type: 'info', args: arguments}); }, + warn: function() { stored.push({type: 'warn', args: arguments}); }, + error: function() { stored.push({type: 'error', args: arguments}); }, + }; +})();` + +// WrapForSnapshot wraps the provided javascript code with a small, global +// console stub object that will record all console logs. This is necessary +// when creating a snapshot for code that expects console.log to exist. It also +// surrounds the jsCode with a try/catch that logs the error, since otherwise +// the snapshot will quietly fail. +func WrapForSnapshot(jsCode string) string { + return fmt.Sprintf(` + // Prefix with the console stub: + %s + try { + %s + } catch (e) { + console.__catch(e); // Store and log the exception to error. + } + `, jsConsoleStub, jsCode) +} + +// FlushSnapshotAndInject replaces the stub console operations with the console +// described by Config and flushes any stored log messages to the new console. +// This is specifically intended for adapting a Context created using +// WrapForSnapshot(). +func FlushSnapshotAndInject(ctx *v8.Context, c Config) (exception *v8.Value) { + // Store a reference to the previous console code for flushing any stored + // log messages (see end of the func). This should never fail to return + // a *v8.Value, even if it's "undefined". + previous, err := ctx.Global().Get("console") + if err != nil || previous == nil { + panic(fmt.Errorf("Global() must be an object: %v", err)) + } + + // Inject the new Console. + c.Inject(ctx) + // Get the new console object. This should never fail to return a + // *v8.Value, even if it's "undefined". However, after the injection + // above it should be the console object we just injected. + current, err := ctx.Global().Get("console") + if err != nil || current == nil { + panic(fmt.Errorf("Global() must be an object: %v", err)) + } + + // Now flush any logs stored by the snapshot code above. If the snapshot + // code was not used, this will attempt to make a few calls and will fail + // with no bad side-effects. + + // However this may fail since "undefined" won't allow .Get() at all. Even + // if previous is an Object, it may return "undefined" if the previous + // console object didn't have __flush. + flush, err := previous.Get("__flush") + if err != nil || flush == nil { + return nil + } + + // If flush is "undefined", this will fail with err != nil. That's ok. + // If it works, we flushed any stored logs to the new Console. + // Otherwise, nothing happens. + exception, err = flush.Call(previous, current) + if err != nil || exception == nil { + return nil + } + + // Finally, check the returned exception value. It's probably "undefined", + // in which case we didn't have any error and we should return nil: + if exception.String() == "undefined" { + return nil + } + + // Uh oh. Looks like we actually got an exception. + return exception +} diff --git a/v8console/examples_test.go b/v8console/examples_test.go new file mode 100644 index 0000000..4198775 --- /dev/null +++ b/v8console/examples_test.go @@ -0,0 +1,55 @@ +package v8console_test + +import ( + "fmt" + "os" + + "github.com/augustoroman/v8" + "github.com/augustoroman/v8/v8console" +) + +func ExampleFlushSnapshotAndInject() { + const myJsCode = ` + // Typically this will be an auto-generated js bundle file. + function require() {} // fake stub + var when = require('when'); + var _ = require('lodash'); + function renderPage(name) { return "Hi " + name + "!"; } + console.warn('snapshot initialization'); + ` + snapshot := v8.CreateSnapshot(v8console.WrapForSnapshot(myJsCode)) + ctx := v8.NewIsolateWithSnapshot(snapshot).NewContext() + console := v8console.Config{"console> ", os.Stdout, os.Stdout, false} + if exception := v8console.FlushSnapshotAndInject(ctx, console); exception != nil { + panic(fmt.Errorf("Panic during snapshot creation: %v", exception.String())) + } + _, err := ctx.Eval(`console.warn('after snapshot');`, `somefile.js`) + if err != nil { + panic(err) + } + + // Output: + // console> [:8] snapshot initialization + // console> [somefile.js:1] after snapshot +} + +func ExampleConfig() { + ctx := v8.NewIsolate().NewContext() + v8console.Config{"> ", os.Stdout, os.Stdout, false}.Inject(ctx) + ctx.Eval(` + console.log('hi there'); + console.info('info 4 u'); + console.warn("Where's mah bucket?"); + console.error("Oh noes!"); + `, "filename.js") + // You can also update the console: + v8console.Config{":-> ", os.Stdout, os.Stdout, false}.Inject(ctx) + ctx.Eval(`console.log("I'm so happy");`, "file2.js") + + // Output: + // > hi there + // > info 4 u + // > [filename.js:4] Where's mah bucket? + // > [filename.js:5] Oh noes! + // :-> I'm so happy +}