forked from 0ad/0ad
445 lines
11 KiB
C++
Executable File
445 lines
11 KiB
C++
Executable File
// ScriptableObject.h
|
|
//
|
|
// A quick way to add (mostly) sensibly-behaving JavaScript interfaces to native classes.
|
|
//
|
|
// Mark Thompson (mark@wildfiregames.com / mot20@cam.ac.uk)
|
|
//
|
|
|
|
#include "scripting/ScriptingHost.h"
|
|
#include "JSConversions.h"
|
|
|
|
#ifndef SCRIPTABLE_INCLUDED
|
|
#define SCRIPTABLE_INCLUDED
|
|
|
|
#define ALLOW_NONSHARED_NATIVES
|
|
|
|
class IJSObject;
|
|
|
|
class IJSProperty
|
|
{
|
|
public:
|
|
virtual ~IJSProperty() {};
|
|
virtual jsval Get( JSContext* cx, IJSObject* obj ) = 0;
|
|
virtual void Set( JSContext* cx, IJSObject* obj, jsval value ) = 0;
|
|
};
|
|
|
|
class IJSObject
|
|
{
|
|
public:
|
|
typedef STL_HASH_MAP<CStrW, IJSProperty*, CStrW_hash_compare> PropertyTable;
|
|
|
|
// Property getters and setters
|
|
typedef jsval (IJSObject::*GetFn)();
|
|
typedef void (IJSObject::*SetFn)( jsval value );
|
|
|
|
// Return a pointer to a property, if it exists
|
|
virtual IJSProperty* HasProperty( CStrW PropertyName ) = 0;
|
|
|
|
// Retrieve the value of a property (returning false if that property is not defined)
|
|
virtual bool GetProperty( JSContext* cx, CStrW PropertyName, jsval* vp ) = 0;
|
|
|
|
// Add a property (with immediate value)
|
|
virtual void AddProperty( CStrW PropertyName, jsval Value ) = 0;
|
|
virtual void AddProperty( CStrW PropertyName, CStrW Value ) = 0;
|
|
|
|
inline IJSObject() {}
|
|
};
|
|
|
|
template<typename T, bool ReadOnly = false> class CJSObject;
|
|
|
|
template<typename T, bool ReadOnly> class CJSProperty : public IJSProperty
|
|
{
|
|
T IJSObject::*m_Data;
|
|
|
|
public:
|
|
CJSProperty( T IJSObject::*Data )
|
|
{
|
|
m_Data = Data;
|
|
}
|
|
jsval Get( JSContext* cx, IJSObject* owner )
|
|
{
|
|
return( ToJSVal( owner->*m_Data ) );
|
|
}
|
|
void Set( JSContext* cx, IJSObject* owner, jsval Value )
|
|
{
|
|
if( !ReadOnly )
|
|
ToPrimitive( cx, Value, owner->*m_Data );
|
|
}
|
|
};
|
|
|
|
#ifdef ALLOW_NONSHARED_NATIVES
|
|
|
|
template<typename T, bool ReadOnly> class CJSNonsharedProperty : public IJSProperty
|
|
{
|
|
T* m_Data;
|
|
|
|
public:
|
|
CJSNonsharedProperty( T* Data )
|
|
{
|
|
m_Data = Data;
|
|
}
|
|
jsval Get( JSContext* cx, IJSObject* owner )
|
|
{
|
|
return( ToJSVal( *m_Data ) );
|
|
}
|
|
void Set( JSContext* cx, IJSObject* owner, jsval Value )
|
|
{
|
|
if( !ReadOnly )
|
|
ToPrimitive( cx, Value, *m_Data );
|
|
}
|
|
};
|
|
|
|
#endif /* ALLOW_NONSHARED_NATIVES */
|
|
|
|
class CJSFunctionProperty : public IJSProperty
|
|
{
|
|
// Function on Owner to get the value
|
|
IJSObject::GetFn m_Getter;
|
|
|
|
// Function on Owner to set the value
|
|
IJSObject::SetFn m_Setter;
|
|
|
|
public:
|
|
CJSFunctionProperty( IJSObject::GetFn Getter, IJSObject::SetFn Setter )
|
|
{
|
|
m_Getter = Getter;
|
|
m_Setter = Setter;
|
|
// Must at least be able to read
|
|
assert( m_Getter );
|
|
}
|
|
jsval Get( JSContext* cx, IJSObject* obj )
|
|
{
|
|
return( (obj->*m_Getter)() );
|
|
}
|
|
void Set( JSContext* cx, IJSObject* obj, jsval value )
|
|
{
|
|
if( m_Setter )
|
|
(obj->*m_Setter)( value );
|
|
}
|
|
};
|
|
|
|
class CJSValProperty : public IJSProperty
|
|
{
|
|
template<typename Q, bool ReadOnly> friend class CJSObject;
|
|
|
|
jsval m_Data;
|
|
|
|
public:
|
|
CJSValProperty( jsval Data )
|
|
{
|
|
m_Data = Data;
|
|
Root();
|
|
}
|
|
~CJSValProperty()
|
|
{
|
|
Uproot();
|
|
}
|
|
void Root()
|
|
{
|
|
if( JSVAL_IS_GCTHING( m_Data ) )
|
|
JS_AddRoot( g_ScriptingHost.GetContext(), (void*)&m_Data );
|
|
}
|
|
void Uproot()
|
|
{
|
|
if( JSVAL_IS_GCTHING( m_Data ) )
|
|
JS_RemoveRoot( g_ScriptingHost.GetContext(), (void*)&m_Data );
|
|
}
|
|
jsval Get( JSContext* cx, IJSObject* object )
|
|
{
|
|
return( m_Data );
|
|
}
|
|
void Set( JSContext* cx, IJSObject* owner, jsval value )
|
|
{
|
|
Uproot();
|
|
m_Data = value;
|
|
Root();
|
|
}
|
|
};
|
|
|
|
// Wrapper around native functions that are attached to CJSObjects
|
|
|
|
template<typename T, bool ReadOnly, typename RType, RType (T::*NativeFunction)( JSContext* cx, uintN argc, jsval* argv )> class CNativeFunction
|
|
{
|
|
public:
|
|
static JSBool JSFunction( JSContext* cx, JSObject* obj, uintN argc, jsval* argv, jsval* rval )
|
|
{
|
|
T* Native = ToNative<T>( cx, obj );
|
|
if( !Native )
|
|
return( JS_TRUE );
|
|
|
|
*rval = ToJSVal<RType>( (Native->*NativeFunction)( cx, argc, argv ) );
|
|
|
|
return( JS_TRUE );
|
|
}
|
|
};
|
|
|
|
template<typename T, bool ReadOnly> class CJSObject : public IJSObject
|
|
{
|
|
// This object
|
|
JSObject* m_JS;
|
|
|
|
protected:
|
|
// The properties defined by the engine
|
|
static PropertyTable m_NativeProperties;
|
|
#ifdef ALLOW_NONSHARED_NATIVES
|
|
PropertyTable m_NonsharedProperties;
|
|
#endif
|
|
// Properties added by script
|
|
PropertyTable m_ScriptProperties;
|
|
|
|
// Whether native code is responsible for managing this object.
|
|
// Script constructors should clear this *BEFORE* creating a JS
|
|
// mirror (otherwise it'll be rooted).
|
|
|
|
bool m_EngineOwned;
|
|
|
|
public:
|
|
// Property getters and setters
|
|
typedef jsval (T::*TGetFn)();
|
|
typedef void (T::*TSetFn)( jsval value );
|
|
|
|
static JSClass JSI_class;
|
|
|
|
static void ScriptingInit( const char* ClassName, JSNative Constructor = NULL, uintN ConstructorMinArgs = 0 )
|
|
{
|
|
JSFunctionSpec* JSI_methods = new JSFunctionSpec[ m_Methods.size() + 1 ];
|
|
unsigned int MethodID;
|
|
for( MethodID = 0; MethodID < m_Methods.size(); MethodID++ )
|
|
JSI_methods[MethodID] = m_Methods[MethodID];
|
|
|
|
JSI_methods[MethodID].name = 0;
|
|
|
|
JSI_class.name = ClassName;
|
|
g_ScriptingHost.DefineCustomObjectType( &JSI_class, Constructor, ConstructorMinArgs, JSI_props, JSI_methods, NULL, NULL );
|
|
|
|
delete[]( JSI_methods );
|
|
|
|
atexit( ScriptingShutdown );
|
|
}
|
|
static void ScriptingShutdown()
|
|
{
|
|
PropertyTable::iterator it;
|
|
for( it = m_NativeProperties.begin(); it != m_NativeProperties.end(); it++ )
|
|
delete( it->second );
|
|
}
|
|
|
|
// JS Property access
|
|
bool GetProperty( JSContext* cx, CStrW PropertyName, jsval* vp )
|
|
{
|
|
IJSProperty* Property = HasProperty( PropertyName );
|
|
if( Property )
|
|
*vp = Property->Get( cx, this );
|
|
|
|
return( true );
|
|
}
|
|
void SetProperty( JSContext* cx, CStrW PropertyName, jsval* vp )
|
|
{
|
|
if( !ReadOnly )
|
|
{
|
|
IJSProperty* prop = HasProperty( PropertyName );
|
|
|
|
if( prop )
|
|
{
|
|
// Already exists
|
|
prop->Set( cx, this, *vp );
|
|
}
|
|
else
|
|
{
|
|
// Need to add it
|
|
AddProperty( PropertyName, *vp );
|
|
}
|
|
}
|
|
}
|
|
|
|
IJSProperty* HasProperty( CStrW PropertyName )
|
|
{
|
|
PropertyTable::iterator it;
|
|
|
|
// Engine-defined properties take precedence
|
|
it = m_NativeProperties.find( PropertyName );
|
|
if( it != m_NativeProperties.end() )
|
|
return( it->second );
|
|
// Next are the object-local engine-defined properties
|
|
// (if they're compiled in)
|
|
#ifdef ALLOW_NONSHARED_NATIVES
|
|
it = m_NonsharedProperties.find( PropertyName );
|
|
if( it != m_NonsharedProperties.end() )
|
|
return( it->second );
|
|
#endif
|
|
// Then check in script properties
|
|
it = m_ScriptProperties.find( PropertyName );
|
|
if( it != m_ScriptProperties.end() )
|
|
return( it->second );
|
|
|
|
// Otherwise not found
|
|
return( NULL );
|
|
}
|
|
|
|
void AddProperty( CStrW PropertyName, jsval Value )
|
|
{
|
|
assert( !HasProperty( PropertyName ) );
|
|
CJSValProperty* newProp = new CJSValProperty( Value );
|
|
m_ScriptProperties[PropertyName] = newProp;
|
|
}
|
|
void AddProperty( CStrW PropertyName, CStrW Value )
|
|
{
|
|
AddProperty( PropertyName, JSParseString( Value ) );
|
|
}
|
|
static void AddProperty( CStrW PropertyName, TGetFn Getter, TSetFn Setter = NULL )
|
|
{
|
|
m_NativeProperties[PropertyName] = new CJSFunctionProperty( (GetFn)Getter, (SetFn)Setter );
|
|
}
|
|
template<typename ReturnType, ReturnType (T::*NativeFunction)( JSContext* cx, uintN argc, jsval* argv )>
|
|
static void AddMethod( const char* Name, uintN MinArgs )
|
|
{
|
|
JSFunctionSpec FnInfo = { Name, CNativeFunction<T, ReadOnly, ReturnType, NativeFunction>::JSFunction, MinArgs, 0, 0 };
|
|
m_Methods.push_back( FnInfo );
|
|
}
|
|
template<typename PropType> static void AddProperty( CStrW PropertyName, PropType T::*Native, bool PropReadOnly = ReadOnly )
|
|
{
|
|
IJSProperty* prop;
|
|
if( PropReadOnly )
|
|
{
|
|
prop = new CJSProperty<PropType, true>( (PropType IJSObject::*)Native );
|
|
}
|
|
else
|
|
{
|
|
prop = new CJSProperty<PropType, ReadOnly>( (PropType IJSObject::*)Native );
|
|
}
|
|
m_NativeProperties[PropertyName] = prop;
|
|
}
|
|
#ifdef ALLOW_NONSHARED_NATIVES
|
|
template<typename PropType> void AddLocalProperty( CStrW PropertyName, PropType* Native, bool PropReadOnly = ReadOnly )
|
|
{
|
|
IJSProperty* prop;
|
|
if( PropReadOnly )
|
|
{
|
|
prop = new CJSNonsharedProperty<PropType, true>( Native );
|
|
}
|
|
else
|
|
prop = new CJSNonsharedProperty<PropType, ReadOnly>( Native );
|
|
m_NonsharedProperties[PropertyName] = prop;
|
|
}
|
|
#endif
|
|
|
|
// Object operations
|
|
JSObject* GetScript()
|
|
{
|
|
if( !m_JS )
|
|
CreateScriptObject();
|
|
return( m_JS );
|
|
}
|
|
// Creating and releasing script objects is done automatically most of the time, but you
|
|
// can do it explicitly.
|
|
void CreateScriptObject()
|
|
{
|
|
if( !m_JS )
|
|
{
|
|
m_JS = JS_NewObject( g_ScriptingHost.GetContext(), &JSI_class, NULL, NULL );
|
|
if( m_EngineOwned )
|
|
JS_AddRoot( g_ScriptingHost.GetContext(), (void*)&m_JS );
|
|
|
|
JS_SetPrivate( g_ScriptingHost.GetContext(), m_JS, (T*)this );
|
|
}
|
|
}
|
|
void ReleaseScriptObject()
|
|
{
|
|
if( m_JS )
|
|
{
|
|
JS_SetPrivate( g_ScriptingHost.GetContext(), m_JS, NULL );
|
|
if( m_EngineOwned )
|
|
JS_RemoveRoot( g_ScriptingHost.GetContext(), &m_JS );
|
|
m_JS = NULL;
|
|
}
|
|
}
|
|
|
|
//
|
|
// Functions and data that must be provided to JavaScript
|
|
//
|
|
private:
|
|
static JSBool JSGetProperty( JSContext* cx, JSObject* obj, jsval id, jsval* vp )
|
|
{
|
|
T* Instance = ToNative<T>( cx, obj );
|
|
if( !Instance )
|
|
return( JS_TRUE );
|
|
|
|
CStrW PropName = g_ScriptingHost.ValueToUCString( id );
|
|
|
|
if( !Instance->GetProperty( cx, PropName, vp ) )
|
|
return( JS_TRUE );
|
|
|
|
return( JS_TRUE );
|
|
}
|
|
static JSBool JSSetProperty( JSContext* cx, JSObject* obj, jsval id, jsval* vp )
|
|
{
|
|
T* Instance = ToNative<T>( cx, obj );
|
|
if( !Instance )
|
|
return( JS_TRUE );
|
|
|
|
CStrW PropName = g_ScriptingHost.ValueToUCString( id );
|
|
|
|
Instance->SetProperty( cx, PropName, vp );
|
|
|
|
return( JS_TRUE );
|
|
}
|
|
static void DefaultFinalize( JSContext *cx, JSObject *obj )
|
|
{
|
|
T* Instance = ToNative<T>( cx, obj );
|
|
if( !Instance || Instance->m_EngineOwned )
|
|
return;
|
|
|
|
delete( Instance );
|
|
JS_SetPrivate( cx, obj, NULL );
|
|
}
|
|
|
|
|
|
static JSPropertySpec JSI_props[];
|
|
static std::vector<JSFunctionSpec> m_Methods;
|
|
|
|
public:
|
|
// Standard constructors/destructors
|
|
CJSObject()
|
|
{
|
|
m_JS = NULL;
|
|
m_EngineOwned = true;
|
|
}
|
|
virtual ~CJSObject()
|
|
{
|
|
Shutdown();
|
|
}
|
|
void Shutdown()
|
|
{
|
|
PropertyTable::iterator it;
|
|
for( it = m_ScriptProperties.begin(); it != m_ScriptProperties.end(); it++ )
|
|
delete( it->second );
|
|
m_ScriptProperties.clear();
|
|
ReleaseScriptObject();
|
|
#ifdef ALLOW_NONSHARED_NATIVES
|
|
for( it = m_NonsharedProperties.begin(); it != m_NonsharedProperties.end(); it++ )
|
|
delete( it->second );
|
|
m_NonsharedProperties.clear();
|
|
#endif
|
|
}
|
|
};
|
|
|
|
template<typename T, bool ReadOnly> JSClass CJSObject<T, ReadOnly>::JSI_class = {
|
|
NULL, JSCLASS_HAS_PRIVATE,
|
|
JS_PropertyStub, JS_PropertyStub,
|
|
JSGetProperty, JSSetProperty,
|
|
JS_EnumerateStub, JS_ResolveStub,
|
|
JS_ConvertStub, DefaultFinalize,
|
|
NULL, NULL, NULL, NULL
|
|
};
|
|
|
|
template<typename T, bool ReadOnly> JSPropertySpec CJSObject<T, ReadOnly>::JSI_props[] = {
|
|
{ 0 },
|
|
};
|
|
|
|
template<typename T, bool ReadOnly> std::vector<JSFunctionSpec> CJSObject<T, ReadOnly>::m_Methods;
|
|
|
|
template<typename T, bool ReadOnly> typename CJSObject<typename T, ReadOnly>::PropertyTable CJSObject<T, ReadOnly>::m_NativeProperties;
|
|
|
|
#endif
|
|
|
|
|