//engine_v8.cpp /* Copyright 2009 10gen Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "engine_v8.h" #include "v8_wrapper.h" #include "v8_utils.h" #include "v8_db.h" #define V8_SIMPLE_HEADER v8::Locker l(_isolate); v8::Isolate::Scope iscope(_isolate); HandleScope handle_scope; Context::Scope context_scope( _context ); namespace mongo { // guarded by v8 mutex map< unsigned, int > __interruptSpecToThreadId; map< unsigned, v8::Isolate* > __interruptSpecToIsolate; /** * Unwraps a BSONObj from the JS wrapper */ static BSONObj unwrapBSONObj(const Handle& obj) { Handle field = Handle::Cast(obj->GetInternalField(0)); if (field.IsEmpty() || !field->IsExternal()) { return BSONObj(); } void* ptr = field->Value(); return ((BSONHolder*)ptr)->_obj; } static BSONHolder* unwrapHolder(const Handle& obj) { Handle field = Handle::Cast(obj->GetInternalField(0)); if (field.IsEmpty() || !field->IsExternal()) return 0; void* ptr = field->Value(); return (BSONHolder*)ptr; } static void weakRefBSONCallback(v8::Persistent p, void* scope) { // should we lock here? no idea, and no doc from v8 of course HandleScope handle_scope; if (!p.IsNearDeath()) return; Handle field = Handle::Cast(p->ToObject()->GetInternalField(0)); BSONHolder* data = (BSONHolder*) field->Value(); delete data; p.Dispose(); } Persistent V8Scope::wrapBSONObject(Local obj, BSONHolder* data) { obj->SetInternalField(0, v8::External::New(data)); Persistent p = Persistent::New(obj); p.MakeWeak(this, weakRefBSONCallback); return p; } static void weakRefArrayCallback(v8::Persistent p, void* scope) { // should we lock here? no idea, and no doc from v8 of course HandleScope handle_scope; if (!p.IsNearDeath()) return; Handle field = Handle::Cast(p->ToObject()->GetInternalField(0)); char* data = (char*) field->Value(); delete [] data; p.Dispose(); } Persistent V8Scope::wrapArrayObject(Local obj, char* data) { obj->SetInternalField(0, v8::External::New(data)); Persistent p = Persistent::New(obj); p.MakeWeak(this, weakRefArrayCallback); return p; } static Handle namedGet(Local name, const v8::AccessorInfo &info) { if ( info.This()->HasRealNamedProperty( name ) ) { // value already cached return info.This()->GetRealNamedProperty(name); } string key = toSTLString(name); BSONHolder* holder = unwrapHolder(info.Holder()); if ( holder->_removed.count( key ) ) return Handle(); BSONObj obj = holder->_obj; BSONElement elmt = obj.getField(key.c_str()); if (elmt.eoo()) return Handle(); Local< External > scp = External::Cast( *info.Data() ); V8Scope* scope = (V8Scope*)(scp->Value()); Handle val = scope->mongoToV8Element(elmt, false); info.This()->ForceSet(name, val, DontEnum); if (elmt.type() == mongo::Object || elmt.type() == mongo::Array) { // if accessing a subobject, it may get modified and base obj would not know // have to set base as modified, which means some optim is lost unwrapHolder(info.Holder())->_modified = true; } return val; } static Handle namedGetRO(Local name, const v8::AccessorInfo &info) { string key = toSTLString(name); BSONObj obj = unwrapBSONObj(info.Holder()); BSONElement elmt = obj.getField(key.c_str()); if (elmt.eoo()) return Handle(); Local< External > scp = External::Cast( *info.Data() ); V8Scope* scope = (V8Scope*)(scp->Value()); Handle val = scope->mongoToV8Element(elmt, true); return val; } static Handle namedSet(Local name, Local value_obj, const v8::AccessorInfo& info) { string key = toSTLString( name ); BSONHolder* holder = unwrapHolder(info.Holder()); holder->_removed.erase( key ); holder->_extra.push_back( key ); holder->_modified = true; // set into JS object return Handle(); } static Handle namedEnumerator(const AccessorInfo &info) { BSONHolder* holder = unwrapHolder(info.Holder()); BSONObj obj = holder->_obj; Handle arr = Handle(v8::Array::New(obj.nFields())); int i = 0; Local< External > scp = External::Cast( *info.Data() ); V8Scope* scope = (V8Scope*)(scp->Value()); set added; // note here that if keys are parseable number, v8 will access them using index for ( BSONObjIterator it(obj); it.more(); ++i) { const BSONElement& f = it.next(); // arr->Set(i, v8::String::NewExternal(new ExternalString(f.fieldName()))); string sname = f.fieldName(); if ( holder->_removed.count( sname ) ) continue; Handle name = scope->getV8Str( sname ); added.insert( sname ); arr->Set(i, name); } for ( list::iterator it = holder->_extra.begin(); it != holder->_extra.end(); it++ ) { string sname = *it; if ( added.count( sname ) ) continue; arr->Set(i++, scope->getV8Str( sname )); } return arr; } Handle namedDelete( Local name, const AccessorInfo& info ) { string key = toSTLString( name ); BSONHolder* holder = unwrapHolder(info.Holder()); holder->_removed.insert( key ); holder->_extra.remove( key ); holder->_modified = true; // also delete in JS obj return Handle(); } // v8::Handle namedQuery(Local property, const AccessorInfo& info) { // string key = ToString(property); // return v8::Integer::New(None); // } static Handle indexedGet(::uint32_t index, const v8::AccessorInfo &info) { StringBuilder ss; ss << index; string key = ss.str(); Local< External > scp = External::Cast( *info.Data() ); V8Scope* scope = (V8Scope*)(scp->Value()); Handle name = scope->getV8Str(key); if ( info.This()->HasRealIndexedProperty( index ) ) { // value already cached return info.This()->GetRealNamedProperty( name ); } BSONHolder* holder = unwrapHolder(info.Holder()); if ( holder->_removed.count( key ) ) return Handle(); BSONObj obj = holder->_obj; BSONElement elmt = obj.getField(key); if (elmt.eoo()) return Handle(); Handle val = scope->mongoToV8Element(elmt, false); info.This()->ForceSet(name, val, DontEnum); if (elmt.type() == mongo::Object || elmt.type() == mongo::Array) { // if accessing a subobject, it may get modified and base obj would not know // have to set base as modified, which means some optim is lost unwrapHolder(info.Holder())->_modified = true; } return val; } Handle indexedDelete( ::uint32_t index, const AccessorInfo& info ) { StringBuilder ss; ss << index; string key = ss.str(); BSONHolder* holder = unwrapHolder(info.Holder()); holder->_removed.insert( key ); holder->_extra.remove( key ); holder->_modified = true; // also delete in JS obj return Handle(); } static Handle indexedGetRO(::uint32_t index, const v8::AccessorInfo &info) { StringBuilder ss; ss << index; string key = ss.str(); Local< External > scp = External::Cast( *info.Data() ); V8Scope* scope = (V8Scope*)(scp->Value()); // cannot get v8 to properly cache the indexed val in the js object // Handle name = scope->getV8Str(key); // // v8 API really confusing here, must check existence on index, but then fetch with name // if (info.This()->HasRealIndexedProperty(index)) { // Handle val = info.This()->GetRealNamedProperty(name); // if (!val.IsEmpty() && !val->IsNull()) // return val; // } BSONObj obj = unwrapBSONObj(info.Holder()); BSONElement elmt = obj.getField(key); if (elmt.eoo()) return Handle(); Handle val = scope->mongoToV8Element(elmt, true); // info.This()->ForceSet(name, val); return val; } static Handle indexedSet(::uint32_t index, Local value_obj, const v8::AccessorInfo& info) { StringBuilder ss; ss << index; string key = ss.str(); BSONHolder* holder = unwrapHolder(info.Holder()); holder->_removed.erase( key ); holder->_extra.push_back( key ); holder->_modified = true; // set into JS object return Handle(); } // static Handle indexedEnumerator(const AccessorInfo &info) { // BSONObj *obj = unwrapBSONObj(info.Holder()); // Handle arr = Handle(v8::Array::New(obj->nFields())); // Local< External > scp = External::Cast( *info.Data() ); // V8Scope* scope = (V8Scope*)(scp->Value()); // int i = 0; // for ( BSONObjIterator it(*obj); it.more(); ++i) { // const BSONElement& f = it.next(); //// arr->Set(i, v8::String::NewExternal(new ExternalString(f.fieldName()))); // arr->Set(i, scope->getV8Str(f.fieldName())); // } // return arr; // } Handle NamedReadOnlySet( Local property, Local value, const AccessorInfo& info ) { string key = toSTLString(property); cout << "cannot write property " << key << " to read-only object" << endl; return value; } Handle NamedReadOnlyDelete( Local property, const AccessorInfo& info ) { string key = toSTLString(property); cout << "cannot delete property " << key << " from read-only object" << endl; return Boolean::New( false ); } Handle IndexedReadOnlySet( ::uint32_t index, Local value, const AccessorInfo& info ) { cout << "cannot write property " << index << " to read-only array" << endl; return value; } Handle IndexedReadOnlyDelete( ::uint32_t index, const AccessorInfo& info ) { cout << "cannot delete property " << index << " from read-only array" << endl; return Boolean::New( false ); } // --- engine --- // void fatalHandler(const char* s1, const char* s2) { // cout << "Fatal handler " << s1 << " " << s2 << endl; // } void gcCallback(GCType type, GCCallbackFlags flags) { HeapStatistics stats; V8::GetHeapStatistics( &stats ); log(1) << "V8 GC heap stats - " << " total: " << stats.total_heap_size() << " exec: " << stats.total_heap_size_executable() << " used: " << stats.used_heap_size()<< " limit: " << stats.heap_size_limit() << endl; } V8ScriptEngine::V8ScriptEngine() { // set resource contraints before any call int K = 1024; v8::ResourceConstraints rc; // rc.set_max_young_space_size(4 * K * K); rc.set_max_old_space_size( 64 * K * K ); v8::SetResourceConstraints( &rc ); // keep engine up after OOM v8::V8::IgnoreOutOfMemoryException(); // v8::V8::SetFatalErrorHandler(fatalHandler); // v8::Locker l; // v8::Locker::StartPreemption( 10 ); v8::V8::Initialize(); } V8ScriptEngine::~V8ScriptEngine() { } void ScriptEngine::setup() { if ( !globalScriptEngine ) { globalScriptEngine = new V8ScriptEngine(); } } void V8ScriptEngine::interrupt( unsigned opSpec ) { v8::Locker l; v8Locks::InterruptLock il; if ( __interruptSpecToThreadId.count( opSpec ) ) { int thread = __interruptSpecToThreadId[ opSpec ]; if ( thread == -2 || thread == -3) { // just mark as interrupted __interruptSpecToThreadId[ opSpec ] = -3; return; } V8::TerminateExecution( __interruptSpecToIsolate[ opSpec ] ); } } void V8ScriptEngine::interruptAll() { v8::Locker l; v8Locks::InterruptLock il; vector< Isolate* > toKill; // v8 mutex could potentially be yielded during the termination call for( map< unsigned, Isolate* >::const_iterator i = __interruptSpecToIsolate.begin(); i != __interruptSpecToIsolate.end(); ++i ) { toKill.push_back( i->second ); } for( vector< Isolate* >::const_iterator i = toKill.begin(); i != toKill.end(); ++i ) { V8::TerminateExecution( *i ); } } // --- scope --- V8Scope::V8Scope( V8ScriptEngine * engine ) : _engine( engine ) , _connectState( NOT ) { // create new isolate and enter it via a scope _isolate = v8::Isolate::New(); v8::Isolate::Scope iscope(_isolate); // resource constraints must be set on isolate, before any call or lock int K = 1024; v8::ResourceConstraints rc; // rc.set_max_young_space_size(4 * K * K); rc.set_max_old_space_size( 64 * K * K ); v8::SetResourceConstraints(&rc); V8::AddGCPrologueCallback(gcCallback, kGCTypeMarkSweepCompact); // keep engine up after OOM v8::V8::IgnoreOutOfMemoryException(); // V8::SetFatalErrorHandler(fatalHandler); v8::Locker l(_isolate); HandleScope handleScope; _context = Context::New(); Context::Scope context_scope( _context ); _global = Persistent< v8::Object >::New( _context->Global() ); _emptyObj = Persistent< v8::Object >::New( v8::Object::New() ); V8STR_CONN = getV8Str( "_conn" ); V8STR_ID = getV8Str( "_id" ); V8STR_LENGTH = getV8Str( "length" ); V8STR_LEN = getV8Str( "len" ); V8STR_TYPE = getV8Str( "type" ); V8STR_ISOBJECTID = getV8Str( "isObjectId" ); V8STR_RETURN = getV8Str( "return" ); V8STR_ARGS = getV8Str( "args" ); V8STR_T = getV8Str( "t" ); V8STR_I = getV8Str( "i" ); V8STR_EMPTY = getV8Str( "" ); V8STR_MINKEY = getV8Str( "$MinKey" ); V8STR_MAXKEY = getV8Str( "$MaxKey" ); V8STR_NUMBERLONG = getV8Str( "__NumberLong" ); V8STR_NUMBERINT = getV8Str( "__NumberInt" ); V8STR_DBPTR = getV8Str( "__DBPointer" ); V8STR_BINDATA = getV8Str( "__BinData" ); V8STR_NATIVE_FUNC = getV8Str( "_native_function" ); V8STR_NATIVE_DATA = getV8Str( "_native_data" ); V8STR_V8_FUNC = getV8Str( "_v8_function" ); V8STR_RO = getV8Str( "_ro" ); V8STR_FULLNAME = getV8Str( "_fullName" ); V8STR_BSON = getV8Str( "_bson" ); // initialize lazy object template lzObjectTemplate = Persistent::New(ObjectTemplate::New()); lzObjectTemplate->SetInternalFieldCount( 1 ); lzObjectTemplate->SetNamedPropertyHandler(namedGet, namedSet, 0, namedDelete, namedEnumerator, v8::External::New(this)); lzObjectTemplate->SetIndexedPropertyHandler(indexedGet, indexedSet, 0, indexedDelete, namedEnumerator, v8::External::New(this)); lzObjectTemplate->NewInstance()->GetPrototype()->ToObject()->Set(V8STR_BSON, v8::Boolean::New(true), DontEnum); roObjectTemplate = Persistent::New(ObjectTemplate::New()); roObjectTemplate->SetInternalFieldCount( 1 ); roObjectTemplate->SetNamedPropertyHandler(namedGetRO, NamedReadOnlySet, 0, NamedReadOnlyDelete, namedEnumerator, v8::External::New(this)); roObjectTemplate->SetIndexedPropertyHandler(indexedGetRO, IndexedReadOnlySet, 0, IndexedReadOnlyDelete, 0, v8::External::New(this)); roObjectTemplate->NewInstance()->GetPrototype()->ToObject()->Set(V8STR_BSON, v8::Boolean::New(true), DontEnum); // initialize lazy array template // unfortunately it is not possible to create true v8 array from a template // this means we use an object template and copy methods over // this it creates issues when calling certain methods that check array type lzArrayTemplate = Persistent::New(ObjectTemplate::New()); lzArrayTemplate->SetInternalFieldCount( 1 ); lzArrayTemplate->SetIndexedPropertyHandler(indexedGet, 0, 0, 0, 0, v8::External::New(this)); lzArrayTemplate->NewInstance()->GetPrototype()->ToObject()->Set(V8STR_BSON, v8::Boolean::New(true), DontEnum); internalFieldObjects = Persistent::New(ObjectTemplate::New()); internalFieldObjects->SetInternalFieldCount( 1 ); injectV8Function("print", Print); injectV8Function("version", Version); injectV8Function("load", load); _wrapper = Persistent< v8::Function >::New( getObjectWrapperTemplate(this)->GetFunction() ); injectV8Function("gc", GCV8); installDBTypes( this, _global ); } V8Scope::~V8Scope() { // make sure to disable interrupt, otherwise can get segfault on race condition disableV8Interrupt(); { V8_SIMPLE_HEADER _wrapper.Dispose(); _emptyObj.Dispose(); for( unsigned i = 0; i < _funcs.size(); ++i ) _funcs[ i ].Dispose(); _funcs.clear(); _global.Dispose(); std::map >::iterator it = _strCache.begin(); std::map >::iterator end = _strCache.end(); while (it != end) { it->second.Dispose(); ++it; } lzObjectTemplate.Dispose(); lzArrayTemplate.Dispose(); roObjectTemplate.Dispose(); internalFieldObjects.Dispose(); _context.Dispose(); } _isolate->Dispose(); } bool V8Scope::hasOutOfMemoryException() { if (!_context.IsEmpty()) return _context->HasOutOfMemoryException(); return false; } /** * JS Callback that will call a c++ function with BSON arguments. */ Handle< Value > V8Scope::nativeCallback( V8Scope* scope, const Arguments &args ) { // V8Lock l; HandleScope handle_scope; Local< External > f = External::Cast( *args.Callee()->Get( scope->V8STR_NATIVE_FUNC ) ); NativeFunction function = (NativeFunction)(f->Value()); Local< External > data = External::Cast( *args.Callee()->Get( scope->V8STR_NATIVE_DATA ) ); BSONObjBuilder b; for( int i = 0; i < args.Length(); ++i ) { stringstream ss; ss << i; scope->v8ToMongoElement( b, ss.str(), args[ i ] ); } BSONObj nativeArgs = b.obj(); BSONObj ret; try { ret = function( nativeArgs, data->Value() ); } catch( const std::exception &e ) { return v8::ThrowException(v8::String::New(e.what())); } catch( ... ) { return v8::ThrowException(v8::String::New("unknown exception")); } return handle_scope.Close( scope->mongoToV8Element( ret.firstElement() ) ); } Handle< Value > V8Scope::load( V8Scope* scope, const Arguments &args ) { Context::Scope context_scope(scope->_context); for (int i = 0; i < args.Length(); ++i) { std::string filename(toSTLString(args[i])); if (!scope->execFile(filename, false , true , false)) { return v8::ThrowException(v8::String::New((std::string("error loading file: ") + filename).c_str())); } } return v8::True(); } /** * JS Callback that will call a c++ function with the v8 scope and v8 arguments. * Handles interrupts, exception handling, etc * * The implementation below assumes that SERVER-1816 has been fixed - in * particular, interrupted() must return true if an interrupt was ever * sent; currently that is not the case if a new killop overwrites the data * for an old one */ v8::Handle< v8::Value > V8Scope::v8Callback( const v8::Arguments &args ) { Local< External > f = External::Cast( *args.Callee()->Get( v8::String::New( "_v8_function" ) ) ); v8Function function = (v8Function)(f->Value()); Local< External > scp = External::Cast( *args.Data() ); V8Scope* scope = (V8Scope*)(scp->Value()); // originally v8 interrupt where disabled here cause: don't want to have to audit all v8 calls for termination exceptions // but we do need to keep interrupt because much time may be spent here (e.g. sleep) bool paused = scope->pauseV8Interrupt(); v8::Handle< v8::Value > ret; string exception; try { ret = function( scope, args ); } catch( const std::exception &e ) { exception = e.what(); } catch( ... ) { exception = "unknown exception"; } if (paused) { bool resume = scope->resumeV8Interrupt(); if ( !resume || globalScriptEngine->interrupted() ) { v8::V8::TerminateExecution(scope->_isolate); return v8::ThrowException( v8::String::New( "Interruption in V8 native callback" ) ); } } if ( !exception.empty() ) { return v8::ThrowException( v8::String::New( exception.c_str() ) ); } return ret; } // ---- global stuff ---- void V8Scope::init( const BSONObj * data ) { // V8Lock l; if ( ! data ) return; BSONObjIterator i( *data ); while ( i.more() ) { BSONElement e = i.next(); setElement( e.fieldName() , e ); } } void V8Scope::setNumber( const char * field , double val ) { V8_SIMPLE_HEADER _global->Set( getV8Str( field ) , v8::Number::New( val ) ); } void V8Scope::setString( const char * field , const char * val ) { V8_SIMPLE_HEADER _global->Set( getV8Str( field ) , v8::String::New( val ) ); } void V8Scope::setBoolean( const char * field , bool val ) { V8_SIMPLE_HEADER _global->Set( getV8Str( field ) , v8::Boolean::New( val ) ); } void V8Scope::setElement( const char *field , const BSONElement& e ) { V8_SIMPLE_HEADER _global->Set( getV8Str( field ) , mongoToV8Element( e ) ); } void V8Scope::setObject( const char *field , const BSONObj& obj , bool readOnly) { V8_SIMPLE_HEADER // Set() accepts a ReadOnly parameter, but this just prevents the field itself // from being overwritten and doesn't protect the object stored in 'field'. _global->Set( getV8Str( field ) , mongoToLZV8( obj, false, readOnly) ); } int V8Scope::type( const char *field ) { V8_SIMPLE_HEADER Handle v = get( field ); if ( v->IsNull() ) return jstNULL; if ( v->IsUndefined() ) return Undefined; if ( v->IsString() ) return String; if ( v->IsFunction() ) return Code; if ( v->IsArray() ) return Array; if ( v->IsBoolean() ) return Bool; // needs to be explicit NumberInt to use integer // if ( v->IsInt32() ) // return NumberInt; if ( v->IsNumber() ) return NumberDouble; if ( v->IsExternal() ) { uassert( 10230 , "can't handle external yet" , 0 ); return -1; } if ( v->IsDate() ) return Date; if ( v->IsObject() ) return Object; throw UserException( 12509, (string)"don't know what this is: " + field ); } v8::Handle V8Scope::get( const char * field ) { return _global->Get( getV8Str( field ) ); } double V8Scope::getNumber( const char *field ) { V8_SIMPLE_HEADER return get( field )->ToNumber()->Value(); } int V8Scope::getNumberInt( const char *field ) { V8_SIMPLE_HEADER return get( field )->ToInt32()->Value(); } long long V8Scope::getNumberLongLong( const char *field ) { V8_SIMPLE_HEADER return get( field )->ToInteger()->Value(); } string V8Scope::getString( const char *field ) { V8_SIMPLE_HEADER return toSTLString( get( field ) ); } bool V8Scope::getBoolean( const char *field ) { V8_SIMPLE_HEADER return get( field )->ToBoolean()->Value(); } BSONObj V8Scope::getObject( const char * field ) { V8_SIMPLE_HEADER Handle v = get( field ); if ( v->IsNull() || v->IsUndefined() ) return BSONObj(); uassert( 10231 , "not an object" , v->IsObject() ); return v8ToMongo( v->ToObject() ); } // --- functions ----- bool hasFunctionIdentifier( const string& code ) { if ( code.size() < 9 || code.find( "function" ) != 0 ) return false; return code[8] == ' ' || code[8] == '('; } Local< v8::Function > V8Scope::__createFunction( const char * raw ) { raw = jsSkipWhiteSpace( raw ); string code = raw; if ( !hasFunctionIdentifier( code ) ) { if ( code.find( "\n" ) == string::npos && ! hasJSReturn( code ) && ( code.find( ";" ) == string::npos || code.find( ";" ) == code.size() - 1 ) ) { code = "return " + code; } code = "function(){ " + code + "}"; } int num = _funcs.size() + 1; string fn; { stringstream ss; ss << "_funcs" << num; fn = ss.str(); } code = fn + " = " + code; TryCatch try_catch; // this might be time consuming, consider allowing an interrupt Handle