/* Copyright (C) 2009 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ // ScriptableObject.h // // A quick way to add (mostly) sensibly-behaving JavaScript interfaces to native classes. // #ifndef INCLUDED_SCRIPTABLEOBJECT #define INCLUDED_SCRIPTABLEOBJECT #include "scripting/ScriptingHost.h" #include "JSConversions.h" #include "lib/sysdep/stl.h" #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 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() {} virtual ~IJSObject() {} }; template class CJSObject; template 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 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 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 class CNativeFunction { public: static JSBool JSFunction( JSContext* cx, JSObject* obj, uintN argc, jsval* argv, jsval* rval ) { T* Native = ToNative( cx, obj ); if( !Native ) return( JS_TRUE ); *rval = ToJSVal( (Native->*NativeFunction)( cx, argc, argv ) ); return( JS_TRUE ); } }; // Special case for void functions template class CNativeFunction { public: static JSBool JSFunction( JSContext* cx, JSObject* obj, uintN argc, jsval* argv, jsval* UNUSED(rval) ) { T* Native = ToNative( cx, obj ); if( !Native ) return( JS_TRUE ); (Native->*NativeFunction)( cx, argc, argv ); return( JS_TRUE ); } }; template 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 ]; size_t 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 ); m_NativeProperties.clear(); } // 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 static void AddMethod( const char* Name, uintN MinArgs ) { JSFunctionSpec FnInfo = { Name, CNativeFunction::JSFunction, (uint8)MinArgs, 0, 0 }; m_Methods.push_back( FnInfo ); } template static void AddProperty( const CStrW& PropertyName, PropType T::*Native, bool PropReadOnly = ReadOnly ) { IJSProperty* prop; if( PropReadOnly ) { prop = new CJSProperty( (PropType IJSObject::*)Native ); } else { prop = new CJSProperty( (PropType IJSObject::*)Native ); } m_NativeProperties[PropertyName] = prop; } #ifdef ALLOW_NONSHARED_NATIVES template void AddLocalProperty( const CStrW& PropertyName, PropType* Native, bool PropReadOnly = ReadOnly ) { IJSProperty* prop; if( PropReadOnly ) { prop = new CJSNonsharedProperty( Native ); } else prop = new CJSNonsharedProperty( 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( 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( 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( cx, obj ); if( !Instance || Instance->m_EngineOwned ) return; delete( Instance ); JS_SetPrivate( cx, obj, NULL ); } static JSPropertySpec JSI_props[]; static std::vector 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 JSClass CJSObject::JSI_class = { NULL, JSCLASS_HAS_PRIVATE, JS_PropertyStub, JS_PropertyStub, JSGetProperty, JSSetProperty, JS_EnumerateStub, JS_ResolveStub, JS_ConvertStub, DefaultFinalize, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }; template JSPropertySpec CJSObject::JSI_props[] = { { NULL, 0, 0, NULL, NULL }, }; template std::vector CJSObject::m_Methods; template typename CJSObject::PropertyTable CJSObject::m_NativeProperties; #endif