create wiki

go home go home
  1. about
  2. code
  3. wiki
  4. blog

Script Engine Reference

From Create Wiki

Jump to: navigation, search

The following has been copy-and-pasted, and edited a bit, from a response on the v8 discussion group. It has an overview of both the internals and use of the Script Engine. It will be refined later.

Post

The code for the Script Engine is here: http://bazaar.launchpad.net/%7Ealex.iskander/create/trunk/files/head%3A/Script/


It has projects to compile on both Windows (using VC++ Express) and Mac (using XCode). However, it is not easily accessible (yet) standalone. But you can easily browse through the source. It has a few dependencies: the smart pointer files from the Bindable library, in the parent folder. I've been meaning to work around this, but haven't yet. And, naturally, it also depends on Google's v8.

You may want to just jump in and browse, or jump to the end of this post and check out some of the code using it I've included below, but in case you want some (perhaps too much) background:

There isn't much in the way of API documentation, as it was originally meant as part of the print output framework I've been developing. For an example use, look at test.cpp in the Tests directory.

The overall structure is relatively simple: There is a Global Context, which can contain multiple global sub-contexts. The global context does not have any ties to any scripting language or back-end. However, you add a V8Context sub-context object to it.

All objects, whether native from C++ or coming from JavaScript, get wrapped in intermediate script object wrappers. Shared JavaScript and C++ primitive types, such as Boolean and Number, get their own type; C++ native objects have their own type (along with a lot of other classes to help manage them). The V8Context object handles transforming these intermediate objects of whatever type into the Google v8-native types, and back to the intermediate types from there.

So, when you call myBoolean = $(true), ($() is the wrapper function which works for all objects), a new SBooleanObject (stored in an SBoolean pointer) is created.

When you call myContext->set("myBoolean", myBoolean); all sub-contexts of your global context (including the V8 context) get told to set "myBoolean" to myBoolean, an SBoolean object. The V8Context object then "unwraps" the boolean from the SBoolean, and gives it to v8 as a v8::Boolean.

All of that is really pretty simple.

The most complicated part is the part dealing with C++ native types. I wanted the wrapping to be completely transparent, so that, for any type, you could still just call $(myObject) and wrap it. To do this, I have a few things:

  1. A map of C++ types (usually determined via RTTI) to SPrototype objects in the global context.
  2. A type tree, used to determine the most derived type known for any object. This allows passing a base class pointer to JavaScript and getting in JavaScript the derived object.
  3. An SPrototype class which has a collection of children.
  4. For each type wrapped by JavaScript, an SNative type accompanying it.
    • Each SNative derives from base SNative classes in the same manner the native C++ types derive from base C++ classes.

For example, if you had Base, Derived1, Derived2, D2Derived, and D2DDerived, and you were only scripting Derived1, Derived2, and D2Derived, you might declare the native types thusly:

ScriptBaseNative(Base); //have to include so we can derive Derived1 and 2
ScriptNative(Derived1, Base);
ScriptNative(Derived2, Base);
ScriptNative(D2Derived, Derived2);

Now what? Say you have a D2DDerived (a class derived from D2Derived). Let us assume it is named d2dd. If you call myNative = $(d2dd), the engine will attempt to find an appropriate SNative wrapper (note that these wrappers do not handle function calls, properties, etc.; they are just containers).

To find this object, the system would first check its cached map (which I have, unfortunately, not coded yet), and if it cannot find the object type (currently always the case), it loops through each C++ sub-type, checking via dynamic_cast if the object is of that type. If it is, it then recursively checks all sub-types of that type, and so on, until it finds the most derived type. Slow if it happens every time (as it currently does), but it will be much faster when the results are cached (the performance hit will only occur the first time an object of that type is passed to JavaScript).

When you call globalContext->set("nativeObject", myNative); the V8Context looks up the SPrototype object mapped to that type. If it isn't found, it goes back up the inheritance tree (using the native's BaseClass typedef recursively) and checks each base class. If nothing is found, it should throw an error, but I don't have any error handling (at all) yet, so it instead just sends V8 v8::Undefined().


Finally, let me provide you with a sample of wrapping an object (an ostream, constructable):

ScriptBaseNative(std::ostream);
 
SWrap(ostreamWrapper, std::ostream)
{
public:
	//tested, in-production code for this object
	ostreamWrapper()
	{
		this->set("write", $(write));
		this->className = "OutputStream";
	}
 
	SMember(write)
	{
		for (int i = 0; i < arguments.length(); i++)
		{
			if (arguments[i]->isString())
				*extract(self) << arguments[i]->stringValue();
			else if (arguments[i]->isNumber())
				*extract(self) << arguments[i]->numberValue();
		}
 
		//write newline
		*extract(self) << "\n";
 
		return $();
	}
 
	//off the top of my head just now, and thus not actually tested
	SCreate
	{
		return new std::ofstream(arguments[0]->stringValue().data());
	}
 
	SConstruct
	{
		//if there was a smart pointer to ostream, we could increment it here
	}
 
	SDestruct
	{
		//if there was a smart pointer to ostream, we could decrement it here,
		//and decide if we should delete it.
 
		//this assumes that the object was created from JavaScript
		if (extract(self))
			delete extract(self);
	}
};
 
int main(int argc, char *argv[])
{
	//create contexts
	R<GlobalContext> context = new GlobalContext();
	R<V8Context> v8context = new V8Context();
	context->addContext(v8context);
 
	//register type
	context->registerType<std::ostream>(new ostreamWrapper());
 
	//run a test script
	v8context->runScript("var stream = new OutputStream('output.out'); stream.write('test'))");
 
	//return
	return 0;
}


And that's about it. It's overly complicated, and still a work in progress, but it works and fulfills my needs splendidly. One really nice aspect is that I implemented support for my smart pointer type by manually deriving SNative instead of using the macro. The macro usually deals in pointers; the manual class deals with the smart pointer instead:


/* This native is declared a-special so that we can add automatic destruction
 in a way that is transparent to derived objects.
 
 Aside from those additions, this is identical to the ScriptBaseNative macro.
 */
namespace Script 
{
	template<>
	class SCRIPT_NATIVE<Bind::Mem> : public SNativeObject
	{
	public:
		typedef Bind::Mem BaseClass_;
		typedef Bind::Mem RepresentsClass;
		typedef SCRIPT_NATIVE<Bind::Mem> ParentNative;
		SCRIPT_NATIVE (Bind::Mem *object)
		{
			//set internal object
			this->nativeObject = object;
		}
 
		Bind::Mem *getObject() 
		{ 
			return *this->nativeObject; 
		} 
 
		virtual void *getID()
		{ 
			return *this->nativeObject; 
		} 
 
		virtual int estimateNativeSize() { return sizeof(Bind::Mem); } 
 
		virtual STypeInfo getType(int depth = 0)
		{
			return STypeInfo(typeid(Bind::Mem));
		}
 
		~SCRIPT_NATIVE()
		{	
			this->ref();
			if (this->_prototype) {
				_prototype->destruct(this);
				_prototype = NULL;
			}
			this->deref();
		}
	protected:
		Bind::R<Bind::Mem> nativeObject;
	}; 
}

Dealing with smart pointers during SConstruct and SDestruct is no longer required. This made things dead simple: all I have to do with a smart-pointable type is tell Script, as I have to anyway, that the type inherits from Mem (the base class for a type pointable to by a smart pointer). It will automatically inherit from my SCRIPT_NATIVE<Bind::Mem> class. This is a big benefit as I have a very large number of classes, all inheriting from each other and ultimately from Mem, that I may want to wrap with minimal difficulty.

Smart pointers were a complete necessity as a native object could be referenced from C++, JavaScript, or both simultaneously, and would need to stay in memory until neither referred to them anymore.

Personal tools