website is under construction
Embedding

Embedding

Ghost was not only designed to be a standalone general-purpose programming language, but also to be a scripting language that can be embedded into other applications.

Installing Ghost

To get started, simply go get the latest version of Ghost. Make sure your project has already been initialized with go mod init.

go get ghostlang.org/x/ghost

Creating an Interpreter

The first step to embedding Ghost is to create a new interpreter instance. This is what will be used to execute Ghost code.

vm := ghost.New()

Each instance carries its own scope, so variables defined by one script are visible to the next script the same instance runs, and invisible to any other instance.

From here we need to configure and set a couple of things before we can execute any Ghost code.

Setting the Root Directory

The root directory is the directory that Ghost will use to resolve relative imports from your code. For example, if you have a file called foo.gs in the root directory, you can import it like this anywhere in your code:

import Foo from 'foo'

To set the root directory, simply call the SetDirectory method on the Ghost VM.

vm.SetDirectory("/path/to/root/directory")

If you are embedding Ghost into an application, you can use the os.Executable function to get the path to the executable file, and then use the filepath.Dir function to get the directory that the executable is in.

executable, err := os.Executable()

if err != nil {
  panic(err)
}

vm.SetDirectory(filepath.Dir(executable))

Setting the Source Code

The next step is to set the source code that you want to execute. This can be done by calling the SetSource method on the Ghost VM.

vm.SetSource(`console.log('Hello, universe!')`)

Setting the File Name

SetFile names the file the source came from. It has no effect on execution — it is what error reports quote and point at, so setting it is what turns an anonymous report into one naming game.gs and the line inside it.

vm.SetFile("game.gs")

Executing Ghost Code

Once you have set the source code, you can execute it by calling the Execute method on the Ghost VM. This will return a Ghost object that you can use to get the result of the execution.

result := vm.Execute()

The result will be a Ghost object. If the execution was successful, the result will be the value of the last expression in the source code. If the execution failed, the result will be an error object.

// Check if the result is an error
if object.IsError(result) {
  // Handle the error
  os.Exit(1)
}

Execute can be called more than once on the same instance. Set a new source and call it again, and the second script picks up where the first left off.

Calling Back Into Ghost

Once a script has run, the functions it defined are still there. Call invokes one by name with a list of Ghost objects as arguments, and hands back the result:

vm.SetSource(`function update(dt) { return dt * 2 }`)
vm.Execute()

result := vm.Call("update", []object.Object{object.NewFloat(0.016)})

This is how a host program drives a script — a game loop calling update and draw every frame, for example. Calling a name the script never defined returns an error object rather than panicking.

Extending Ghost From Go

Your program can add its own functions, modules, and classes to the language. Every registration function lives on the ghost package rather than on an instance, so what you register is available to every instance in the process.

// A function, reached with `import "ghost:greet"`
ghost.RegisterFunction("greet", func(scope *object.Scope, tok token.Token, args ...object.Object) object.Object {
	return &object.String{Value: "Hello, " + args[0].String()}
})

// A module of methods and properties, reached with `import "ghost:example"`
ghost.RegisterModule("example", ExampleMethods, ExampleProperties)

A module's methods and properties are map[string]*object.LibraryFunction and map[string]*object.LibraryProperty, built with the RegisterMethod and RegisterProperty helpers in ghostlang.org/x/ghost/library/modules. This is exactly how Ghost's own standard library is written, and how Lumen adds canvas, image, audio, and the rest — the standard library's source is the best reference to work from.

What you register is not global to scripts. Nothing is, apart from console and type. A registered name becomes reachable the same way every built-in module does: through an import. RegisterFunction and RegisterModule put their names under Ghost's own ghost: scheme.

Registration does not have to happen before Execute. import resolves at the point it runs, so a module registered while a script is already running — by a plugin the script loaded itself — is importable from the next line on.

Claiming a Scheme

ghost: is not special-cased. It is simply the scheme Ghost's own standard library registers under, and your program can claim one of its own so its modules read as yours rather than as Ghost's. This is what Lumen does:

ghost.RegisterModuleForScheme("lumen", "canvas", CanvasMethods, CanvasProperties)
ghost.RegisterFunctionForScheme("lumen", "measure", measureFunc)
import "lumen:canvas"
import { setColor } from "lumen:canvas"

There is one registry per scheme and one scheme: import mechanism under all of them, so there is no second convention to learn — only which call to make. The unscoped RegisterFunction/RegisterModule target ghost:; a host that wants a namespace reading as its own reaches for the ForScheme pair.

A scheme nothing has ever registered under is a distinct import error from a misspelled name inside a real one, so a typo in the prefix tells you it is the prefix that is wrong.

Registering a Class

RegisterClass and RegisterClassForScheme add a class to a module whose instances are built and driven entirely by Go. A script news one exactly as it would a class declared in Ghost.

ghost.RegisterClassForScheme("lumen", "audio", "Audio", audioConstructor)
import { Audio } from "lumen:audio"

sound = new Audio("hit.wav")
sound.play()

The constructor has the same signature every registered function has — func(scope *object.Scope, tok token.Token, args ...object.Object) object.Object — and returns whatever value an instance should be. That value's own Method() implementation decides what calling something on an instance does, entirely on your side; Ghost does not prescribe an instance shape beyond "is an object.Object".

Reading a property or calling a method on the class value rather than an instance is refused with the same wording a Ghost-declared class gives, so a script has no way to tell — and no reason to need to tell — which kind of class it is holding.

This is a Go-level extension point only. There is still exactly one way to declare a class in .gs source, and nothing here adds a second. It is the right tool when the class wraps a genuinely native operation with no Ghost-level logic around it — opening a file handle, decoding audio — where tree-walking a method that only calls straight back into Go would buy nothing.

Ghost cannot expose properties on objects your program defines — only methods. An embedded object's state has to be read through getter methods.

Reporting Errors

By default a failed Execute prints its own report to standard error. Two settings change that:

vm.SetQuiet(true)         // do not print anything; just hand back the error object
vm.SetReportWriter(w)     // print the report somewhere other than stderr

object.IsError(result) is the tidiest way to test the result:

if object.IsError(result) {
	os.Exit(1)
}

A Go panic anywhere below Execute — including one caused by a bug in Ghost itself — is recovered at this boundary and returned as an internal error rather than taking your program down with it.

Registration is process-global, so a single Go process cannot host two independently-configured Ghost setups. A host that needs isolated configurations runs separate processes.

A Complete Example

Below is a complete example of what we have covered so far. It's a simple program that creates a Ghost VM, loads a script, and executes it.

package main

import (
	"os"
	"path/filepath"

	"ghostlang.org/x/ghost/ghost"
	"ghostlang.org/x/ghost/object"
)

func main() {
	// Create a new Ghost VM
	vm := ghost.New()

	// Set the root directory
	// Ghost will use this to resolve imports from your code
	executable, err := os.Executable()

	if err != nil {
		panic(err)
	}

	vm.SetDirectory(filepath.Dir(executable))

	// Set the source code to execute, and the name errors should report
	vm.SetSource(`console.log('Hello, universe!')`)
	vm.SetFile("main.gs")

	// Execute the source code
	// The result will be a ghost object
	result := vm.Execute()

	// Check if the result is an error
	if object.IsError(result) {
		os.Exit(1)
	}
}