0ad/source/scripting/ScriptableObject.h
Ykkrosh fba692c8b5 # Fixed some warnings and potentially misleading code
* Removed ToJSVal<jsval> because it's treated as ToJSVal<long> and
causes minor confusion and/or compiler errors.
   Changed script interface functions to return either C++ types or a
jsval_t wrapper.
 * Replaced some C casts with static_cast to avoid significant confusion
and to cause compiler errors instead.
 * Removed some redundant argument-checking code. Simplified some
string-generating code.
 * Fixed some "dereferencing type-punned pointer will break
strict-aliasing rules" warnings (from `g++ -O3`).

This was SVN commit r5115.
2007-05-29 19:01:21 +00:00

461 lines
12 KiB
C++

// ScriptableObject.h
//
// A quick way to add (mostly) sensibly-behaving JavaScript interfaces to native classes.
//
#include "scripting/ScriptingHost.h"
#include "JSConversions.h"
#ifndef INCLUDED_SCRIPTABLEOBJECT
#define INCLUDED_SCRIPTABLEOBJECT
#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)( JSContext* cx );
typedef void (IJSObject::*SetFn)( JSContext* cx, jsval value );
// Return a pointer to a property, if it exists
virtual IJSProperty* HasProperty( const CStrW& PropertyName ) = 0;
// Retrieve the value of a property (returning false if that property is not defined)
virtual bool GetProperty( JSContext* cx, const CStrW& PropertyName, jsval* vp ) = 0;
// Add a property (with immediate value)
virtual void AddProperty( const CStrW& PropertyName, jsval Value ) = 0;
virtual void AddProperty( const CStrW& PropertyName, const 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* UNUSED(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* UNUSED(cx), IJSObject* UNUSED(owner) )
{
return( ToJSVal( *m_Data ) );
}
void Set( JSContext* cx, IJSObject* UNUSED(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
debug_assert( m_Getter );
}
jsval Get( JSContext* cx, IJSObject* obj )
{
return( (obj->*m_Getter)(cx) );
}
void Set( JSContext* cx, IJSObject* obj, jsval value )
{
if( m_Setter )
(obj->*m_Setter)( cx, 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_AddNamedRoot( g_ScriptingHost.GetContext(), (void*)&m_Data, "ScriptableObjectProperty" );
}
void Uproot()
{
if( JSVAL_IS_GCTHING( m_Data ))
JS_RemoveRoot( g_ScriptingHost.GetContext(), (void*)&m_Data );
}
jsval Get( JSContext* UNUSED(cx), IJSObject* UNUSED(object))
{
return( m_Data );
}
void Set( JSContext* UNUSED(cx), IJSObject* UNUSED(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 );
}
};
// Special case for void functions
template<typename T, bool ReadOnly, void (T::*NativeFunction)( JSContext* cx, uintN argc, jsval* argv )>
class CNativeFunction<T, ReadOnly, void, NativeFunction>
{
public:
static JSBool JSFunction( JSContext* cx, JSObject* obj, uintN argc, jsval* argv, jsval* UNUSED(rval) )
{
T* Native = ToNative<T>( cx, obj );
if( !Native )
return( JS_TRUE );
(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)( JSContext* );
typedef void (T::*TSetFn)( JSContext*, 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, const CStrW& PropertyName, jsval* vp )
{
IJSProperty* Property = HasProperty( PropertyName );
if( Property )
*vp = Property->Get( cx, this );
return( true );
}
void SetProperty( JSContext* cx, const 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( const 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( const CStrW& PropertyName, jsval Value )
{
debug_assert( !HasProperty( PropertyName ) );
CJSValProperty* newProp = new CJSValProperty( Value );
m_ScriptProperties[PropertyName] = newProp;
}
void AddProperty( const CStrW& PropertyName, const CStrW& Value )
{
AddProperty( PropertyName, JSParseString( Value ) );
}
static void AddProperty( const 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, (uint8)MinArgs, 0, 0 };
m_Methods.push_back( FnInfo );
}
template<typename PropType> static void AddProperty( const 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( const 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_AddNamedRoot( g_ScriptingHost.GetContext(), (void*)&m_JS, JSI_class.name );
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<T, ReadOnly>::PropertyTable CJSObject<T, ReadOnly>::m_NativeProperties;
#endif