/monkey

SpiderMonkey wrapper for Go

Primary LanguageGo

What is

This package is SpiderMonkey wrapper for Go.

You can use this package to embed JavaScript into your Go program.

Why make

You can found some project like "go-v8" and "gomonkey" on GitHub.

Thy have same purpose: Embed JavaScript into Go program, make it more dynamic.

But I found all of existing projects are not powerful enough.

For example "go-v8" use JSON to pass data between Go and JavaScript, each callback has a significant performance cost.

And those packages all have thread-safe problem.

V8 and SpiderMonkey both use thread-local storage.

Embed them into Go program, you need to make sure each JS runtime used by creator thread only.

So monkey born.

It has a rich API and can be freely used in any multi-goroutine Go program.

Install

You need install SpiderMonkey first.

Mac OS X:

brew install spidermonkey

Ubuntu:

sudo apt-get install libmozjs185-dev

Or compile by yourself (reference).

And then install Monkey by "go get" command.

go get github.com/realint/monkey

Performance

There are some benchmark test in "monkey_test.go".

You can run those test like this:

go test -bench="."

The benchmark result on my Mac:

Benchmark_ADD_IN_JS           200000     13410 ns/op
Benchmark_ADD_BY_JS           200000     15265 ns/op
Benchmark_ADD_BY_GO           100000     24779 ns/op
Benchmark_OOXX_IN_JS           10000    271113 ns/op
Benchmark_OOXX_IN_GO           50000     57133 ns/op
Benchmark_OOXX_BY_GO           20000     81031 ns/op
Benchmark_ADD_IN_JS_IN_USE   1000000      1446 ns/op
Benchmark_ADD_BY_JS_IN_USE   1000000      1427 ns/op
Benchmark_ADD_BY_GO_IN_USE    500000      7139 ns/op
Benchmark_OOXX_IN_JS_IN_USE    10000    262562 ns/op
Benchmark_OOXX_BY_GO_IN_USE    50000     63353 ns/op

Examples

All the example codes can be found in "examples" folder.

You can run all of the example codes like this:

go run examples/hello_world.go

Hello World

The "hello_world.go" shows what Monkey can do.

package main

import "fmt"
import js "github.com/realint/monkey"

func main() {
	// Create script runtime
	runtime := js.NewRuntime(8 * 1024 * 1024)

	// Create script context
	context := runtime.NewContext()

	// Evaluate script
	value := context.Eval("'Hello ' + 'World!'")
	println(value.ToString())

	// Define a function and call it
	context.DefineFunction("println", func(f *js.Func) {
		for i := 0; i < f.Argc(); i++ {
			fmt.Print(f.Argv(i))
		}
		fmt.Println()
		f.Return(f.Context().Void())
	})
	context.Eval("println('Hello Function!')")

	// Compile once, run many times
	script := context.Compile(
		"println('Hello Compiler!')",
		"<no name>", 0,
	)
	script.Execute()
	script.Execute()
	script.Execute()

	// Error handler
	context.SetErrorReporter(func(report *js.ErrorReport) {
		println(fmt.Sprintf(
			"%s:%d: %s",
			report.FileName, report.LineNum, report.Message,
		))
		if report.LineBuf != "" {
			println("\t", report.LineBuf)
		}
	})
	context.Eval("not_exists()")
}

This code will output:

Hello World!
Hello Function!
Hello Compiler!
Hello Compiler!
Hello Compiler!
Eval():0: ReferenceError: not_exists is not defined

Thread Safe

The "many_many.go" shows Monkey is thread safe.

package main

import "fmt"
import "sync"
import "runtime"
import js "github.com/realint/monkey"

func assert(c bool) bool {
	if !c {
		panic("assert failed")
	}
	return c
}

func main() {
	runtime.GOMAXPROCS(20)

	// Create script runtime
	runtime := js.NewRuntime(8 * 1024 * 1024)

	// Create script context
	context := runtime.NewContext()

	context.DefineFunction("println", func(f *js.Func) {
		for i := 0; i < f.Argc(); i++ {
			fmt.Print(f.Argv(i))
		}
		fmt.Println()
		f.Return(f.Context().Void())
	})

	wg := new(sync.WaitGroup)

	// One runtime instance used by many goroutines
	for i := 0; i < 100; i++ {
		wg.Add(1)
		go func() {
			for j := 0; j < 100; j++ {
				v := context.Eval("println('Hello World!')")
				assert(v != nil)
			}
			wg.Done()
		}()
	}

	wg.Wait()
}

Value

The "op_value.go" shows how to convert JS value to Go value.

package main

import js "github.com/realint/monkey"

func assert(c bool) bool {
	if !c {
		panic("assert failed")
	}
	return c
}

func main() {
	// Create script Runtime
	runtime := js.NewRuntime(8 * 1024 * 1024)

	// Create script context
	context := runtime.NewContext()

	// String
	if value := context.Eval("'abc'"); assert(value != nil) {
		assert(value.IsString())
		assert(value.ToString() == "abc")
	}

	// Int
	if value := context.Eval("123456789"); assert(value != nil) {
		assert(value.IsInt())

		if value1, ok1 := value.ToInt(); assert(ok1) {
			assert(value1 == 123456789)
		}
	}

	// Number
	if value := context.Eval("12345.6789"); assert(value != nil) {
		assert(value.IsNumber())

		if value1, ok1 := value.ToNumber(); assert(ok1) {
			assert(value1 == 12345.6789)
		}
	}
}

Function

The "op_func.go" shows how to play with JS function value.

package main

import js "github.com/realint/monkey"

func assert(c bool) bool {
	if !c {
		panic("assert failed")
	}
	return c
}

func main() {
	// Create script runtime
	runtime := js.NewRuntime(8 * 1024 * 1024)

	// Create script context
	context := runtime.NewContext()

	// Return a function object from JavaScript
	if value := context.Eval("function(a,b){ return a+b; }"); assert(value != nil) {
		// Type check
		assert(value.IsFunction())

		// Call
		value1 := value.Call([]*js.Value{
			context.Int(10),
			context.Int(20),
		})

		// Result check
		assert(value1 != nil)
		assert(value1.IsNumber())

		if value2, ok2 := value1.ToNumber(); assert(ok2) {
			assert(value2 == 30)
		}
	}

	// Define a function that return an object with function from Go
	ok := context.DefineFunction("get_data", func(f *js.Func) {
		obj := f.Context().NewObject(nil)

		ok := obj.DefineFunction("abc",
			func(object *js.Object, name string, args []*js.Value) *js.Value {
				return f.Context().Int(100)
			},
		)

		assert(ok)

		f.Return(obj.ToValue())
	})

	assert(ok)

	if value := context.Eval(`
		a = get_data(); 
		a.abc();
	`); assert(value != nil) {
		assert(value.IsInt())

		if value2, ok2 := value.ToInt(); assert(ok2) {
			assert(value2 == 100)
		}
	}
}

Array

The "op_array.go" shows how to play with JS array.

package main

import js "github.com/realint/monkey"

func assert(c bool) bool {
	if !c {
		panic("assert failed")
	}
	return c
}

func main() {
	// Create script runtime
	runtime := js.NewRuntime(8 * 1024 * 1024)

	// Create script context
	context := runtime.NewContext()

	// Return an array from JavaScript
	if value := context.Eval("[123, 456];"); assert(value != nil) {
		// Check type
		assert(value.IsArray())
		array := value.ToArray()
		assert(array != nil)

		// Check length
		assert(array.GetLength() == 2)

		// Check first item
		value1, ok1 := array.GetInt(0)
		assert(ok1)
		assert(value1 == 123)

		// Check second item
		value2, ok2 := array.GetInt(1)
		assert(ok2)
		assert(value2 == 456)

		// Set first item
		assert(array.SetInt(0, 789))
		value3, ok3 := array.GetInt(0)
		assert(ok3)
		assert(value3 == 789)

		// Grows
		assert(array.SetLength(3))
		assert(array.GetLength() == 3)
	}

	// Return an array from Go
	if ok := context.DefineFunction("get_data", func(f *js.Func) {
		array := f.Context().NewArray()
		array.SetInt(0, 100)
		array.SetInt(1, 200)
		f.Return(array.ToValue())
	}); assert(ok) {
		if value := context.Eval("get_data()"); assert(value != nil) {
			// Check type
			assert(value.IsArray())
			array := value.ToArray()
			assert(array != nil)

			// Check length
			assert(array.GetLength() == 2)

			// Check first item
			value1, ok1 := array.GetInt(0)
			assert(ok1)
			assert(value1 == 100)

			// Check second item
			value2, ok2 := array.GetInt(1)
			assert(ok2)
			assert(value2 == 200)
		}
	}
}

Object

The "op_object.go" shows how to play with JS object.

package main

import js "github.com/realint/monkey"

func assert(c bool) bool {
	if !c {
		panic("assert failed")
	}
	return c
}

func main() {
	// Create script runtime
	runtime := js.NewRuntime(8 * 1024 * 1024)

	// Create script context
	context := runtime.NewContext()

	// Return an object from JavaScript
	if value := context.Eval("x={a:123}"); assert(value != nil) {
		// Type check
		assert(value.IsObject())
		obj := value.ToObject()

		// Get property 'a'
		value1, ok1 := obj.GetInt("a")
		assert(ok1)
		assert(value1 == 123)

		// Set property 'b'
		assert(obj.SetInt("b", 456))
		value2, ok2 := obj.GetInt("b")
		assert(ok2)
		assert(value2 == 456)
	}

	// Return and object From Go
	ok := context.DefineFunction("get_data", func(f *js.Func) {
		obj := f.Context().NewObject(nil)
		obj.SetInt("abc", 100)
		obj.SetInt("def", 200)
		f.Return(obj.ToValue())
	})

	assert(ok)

	if value := context.Eval("get_data()"); assert(value != nil) {
		// Type check
		assert(value.IsObject())
		obj := value.ToObject()

		// Get property 'abc'
		value1, ok1 := obj.GetInt("abc")
		assert(ok1)
		assert(value1 == 100)

		// Get property 'def'
		value2, ok2 := obj.GetInt("def")
		assert(ok2)
		assert(value2 == 200)
	}
}

Property

The "op_prop.go" shows how to handle JavaScript object's property in Go.

package main

import js "github.com/realint/monkey"

func assert(c bool) bool {
	if !c {
		panic("assert failed")
	}
	return c
}

type T struct {
	abc int32
}

func main() {
	// Create Script Runtime
	runtime := js.NewRuntime(8 * 1024 * 1024)

	// Create script context
	context := runtime.NewContext()

	// Return Object With Property Getter And Setter From Go
	ok := context.DefineFunction("get_data", func(f *js.Func) {
		cx := f.Context()
		obj := cx.NewObject(&T{123})

		// Define the property 'abc' with getter and setter
		ok := obj.DefineProperty("abc",
			// Init value
			cx.Void(),
			// T getter callback called each time
			// JavaScript code accesses the property's value
			func(g *js.Getter) {
				t := g.Object().GetPrivate().(*T)
				if g.Name() == "abc" {
					g.Return(cx.Int(t.abc))
				} else {
					panic("undefined property " + g.Name())
				}
			},
			// The setter callback is called each time
			// JavaScript code assigns to the property
			func(s *js.Setter) {
				t := s.Object().GetPrivate().(*T)
				if s.Name() == "abc" {
					d, ok := s.Value().ToInt()
					assert(ok)
					t.abc = d
				} else {
					panic("undefined property " + s.Name())
				}
			},
			0,
		)

		assert(ok)

		f.Return(obj.ToValue())
	})

	assert(ok)

	if value := context.Eval(`
		a = get_data();
		v1 = a.abc;
		a.abc = 456;
		v2 = a.abc;
		[v1, v2, a];
	`); assert(value != nil) {
		// Type check
		assert(value.IsArray())
		array := value.ToArray()
		assert(array != nil)

		// Length check
		assert(array.GetLength() == 3)

		// Check v1
		value1, ok1 := array.GetInt(0)
		assert(ok1)
		assert(value1 == 123)

		// Check v2
		value2, ok2 := array.GetInt(1)
		assert(ok2)
		assert(value2 == 456)

		// Check v3
		obj := array.GetObject(2)
		assert(obj != nil)
		t, ok3 := obj.GetPrivate().(*T)
		assert(ok3)
		assert(t.abc == 456)
	}

	if value := context.Eval(`
		var a = {};
		a.field1 = 1;
		a.field2 = "hello";
		a.field3 = 1.2;
		a.field4 = true;
		a.field5 = {};
		a.func1 = function(){};
		a;
	`); assert(value != nil) {
		obj := value.ToObject()
		assert(obj != nil)

		keys := obj.Keys()
		assert(len(keys) == 6)
		assert(keys[0] == "field1")
		assert(keys[1] == "field2")
		assert(keys[2] == "field3")
		assert(keys[3] == "field4")
		assert(keys[4] == "field5")
		assert(keys[5] == "func1")

		keys = obj.GetProperty("field5").ToObject().Keys()
		assert(len(keys) == 0)
	}
}