/// features JavaScript execution using the SpiderMonkey library // - this unit is a part of the freeware Synopse framework, // licensed under a MPL/GPL/LGPL tri-license; version 1.18 unit SynSM; { This file is part of Synopse framework. Synopse framework. Copyright (C) 2022 Arnaud Bouchez Synopse Informatique - https://synopse.info Scripting support for mORMot Copyright (C) 2022 Pavel Mashlyakovsky pavel.mash at gmail.com Some ideas taken from http://code.google.com/p/delphi-javascript http://delphi.mozdev.org/javascript_bridge/ *** BEGIN LICENSE BLOCK ***** Version: MPL 1.1/GPL 2.0/LGPL 2.1 The contents of this file are subject to the Mozilla Public License Version 1.1 (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.mozilla.org/MPL Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the specific language governing rights and limitations under the License. The Initial Developer of the Original Code is Pavel Mashlyakovsky. Portions created by the Initial Developer are Copyright (C) 2022 the Initial Developer. All Rights Reserved. Contributor(s): - Arnaud Bouchez - Vadim Orel - win2014 Alternatively, the contents of this file may be used under the terms of either the GNU General Public License Version 2 or later (the "GPL"), or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), in which case the provisions of the GPL or the LGPL are applicable instead of those above. If you wish to allow use of your version of this file only under the terms of either the GPL or the LGPL, and not to allow others to use your version of this file under the terms of the MPL, indicate your decision by deleting the provisions above and replace them with the notice and other provisions required by the GPL or the LGPL. If you do not delete the provisions above, a recipient may use your version of this file under the terms of any one of the MPL, the GPL or the LGPL. ***** END LICENSE BLOCK ***** --------------------------------------------------------------------------- Download the SpiderMonkey library at https://synopse.info/files/synsm.7z ! --------------------------------------------------------------------------- Version 1.18 - initial release. Use SpiderMonkey 24 - add TSMObject.defineNativeMethod - add JSError procedure for Exception handling inside of JSNative function - enhanced multi thread process - add TSMEngine.MaybeGarbageCollect method - add timeout Framework } {$I Synopse.inc} // define HASINLINE CPU32 CPU64 OWNNORMTOUPPER {$I SynSM.inc} // define SM_DEBUG JS_THREADSAFE CONSIDER_TIME_IN_Z interface uses Windows, {$ifdef ISDELPHIXE2}System.SysUtils,{$else}SysUtils,{$endif} Classes, {$ifndef LVCL} Contnrs, {$endif} Variants, SynCommons, SynLog, SynTests, SynTable, SynSMAPI; const /// default stack growing size, in bytes STACK_CHUNK_SIZE: cardinal = 8192; type /// generic parent class of all SpiderMonkey-related Exception types ESMException = class(ESynException); {$M+} TSMEngineManager = class; {$M-} TSMEngine = class; /// just a wrapper around jsval API type, to be used with our object wrappers // - SpiderMonkey jsval type can be directly casted to this type via TSMValue(jsval) // - note that some methods expect an execution context to be supplied as // parameter, as soon as it contains a non primitive type (double/integer) TSMValue = object protected FValue: jsval; public /// type of the value // - you should better use this before calling other To*() methods function ValType(cx: PJSContext): JSType; {$ifdef HASINLINE}inline;{$endif} /// direct access to the internal jsval instance property AsJSVal: jsval read FValue write FValue; /// set the value as one 32 bit integer procedure SetInteger(const Value: integer); {$ifdef HASINLINE}inline;{$endif} /// read the value as one 32 bit integer function ToInteger: integer; {$ifdef WITHASSERT}{$ifdef HASINLINE}inline;{$endif}{$endif} /// access to the value as integer property AsInteger: integer read ToInteger write SetInteger; /// set the value as floating point procedure SetDouble(const Value: double); {$ifdef HASINLINE}inline;{$endif} /// read the value as floating point function ToDouble: double; {$ifdef WITHASSERT}{$ifdef HASINLINE}inline;{$endif}{$endif} /// access to the value as floating point property AsDouble: double read ToDouble write SetDouble; /// set the value as boolean procedure SetBoolean(const Value: boolean); {$ifdef HASINLINE}inline;{$endif} /// read the value as boolean function ToBoolean: boolean; {$ifdef HASINLINE}inline;{$endif} /// access to the value as boolean property AsBoolean: boolean read ToBoolean write SetBoolean; /// set the value as one 64 bit integer // - this is a somewhat dirty hack, since SpiderMonkey don't support int64: // but it is possible to transform int64 to double for ant value < (1 shl 51) // - sometimes we need int64 to be passed do SpiderMonkey (e.g. for an ID) procedure SetInt64(const Value: int64); /// read the value as one 64 bit integer // - note that SpiderMonkey is not able to store all Int64 values directly function ToInt64: int64; /// access to the value as one 64 bit integer property AsInt64: int64 read ToInt64 write SetInt64; /// set the value as VOID procedure SetVoid; {$ifdef HASINLINE}inline;{$endif} /// set the value as NULL procedure SetNull; {$ifdef HASINLINE}inline;{$endif} /// set the value as variant (not implemented yet) // - will set any custom variant type (e.g. TDocVariant) as a JavaScript // object value computed from the JSON serialization of the variant // - in SpiderMonkey non-simple type instances do exist in a given JSContext, // so we need to know the execution context (using a property is not an option) procedure SetVariant(cx: PJSContext; const Value: Variant); /// return the value as variant (not implemented yet) // - will return any JavaScript string value directly as a RawUTF8 // - will return any JavaScript object value as a TDocVariant document // - in SpiderMonkey non-simple type instances do exist in a given JSContext, // so we need to know the execution context (using a property is not an option) function ToVariant(cx: PJSContext): Variant; overload; {$ifdef HASINLINE}inline;{$endif} /// return the value as variant (not implemented yet) // - will return any JavaScript string value directly as a RawUTF8 // - will return any JavaScript object value as a TDocVariant document // - in SpiderMonkey non-simple type instances do exist in a given JSContext, // so we need to know the execution context (using a property is not an option) procedure ToVariant(cx: PJSContext; var result: Variant); overload; /// set the value as TVarRec (i.e. an "array of const" open parameter) // - here any AnsiString parameter is expected to be a RawUTF8 before Delphi // 2009, or its correct code page will be retrieved since Delphi 2009 // - in SpiderMonkey non-simple type instances do exist in a given JSContext, // so we need to know the execution context (using a property is not an option) procedure SetTVarRec(cx: PJSContext; const V: TVarRec); /// set the value as an UTF-16 encoded buffer // - in SpiderMonkey non-simple type instances do exist in a given JSContext, // so we need to know the execution context (using a property is not an option) // - warning - JSString string is a subject for GC so you must root it or set // as property of some object or use SetNativeString() method to pass the // value by reference procedure SetWideChar(cx: PJSContext; Text: PWideChar; TextLen: integer); /// set the value as an Ansi encoded buffer (may be UTF-8 or any code page) // - if CodePage is 0, will use the CurrentAnsiCodePage value // - in SpiderMonkey non-simple type instances do exist in a given JSContext, // so we need to know the execution context (using a property is not an option) // - warning - JSString string is a subject for GC so you must root it or set // as property of some object or use SetNativeString() method to pass the // value by reference procedure SetAnsiChar(cx: PJSContext; Text: PAnsiChar; TextLen, CodePage: integer); /// set the value as an Unicode String // - in SpiderMonkey non-simple type instances do exist in a given JSContext, // so we need to know the execution context (using a property is not an option) // - warning - JSString string is a subject for GC so you must root it or set // as property of some object or use SetNativeString() method to pass the // value by reference procedure SetSynUnicode(cx: PJSContext; const aStr: SynUnicode); {$ifdef HASINLINE}inline;{$endif} /// return the value as an Unicode String // - in SpiderMonkey non-simple type instances do exist in a given JSContext, // so we need to know the execution context (using a property is not an option) function ToSynUnicode(cx: PJSContext): SynUnicode; overload; {$ifdef HASINLINE}inline;{$endif} /// return the value as an Unicode String // - in SpiderMonkey non-simple type instances do exist in a given JSContext, // so we need to know the execution context (using a property is not an option) procedure ToSynUnicode(cx: PJSContext; var result: SynUnicode); overload; {$ifdef HASINLINE}inline;{$endif} /// set the value as an Unicode WideString // - in SpiderMonkey non-simple type instances do exist in a given JSContext, // so we need to know the execution context (using a property is not an option) // - warning - JSString string is a subject for GC so you must root it or set // as property of some object or use SetNativeString() method to pass the // value by reference procedure SetWideString(cx: PJSContext; const aStr: WideString); {$ifdef HASINLINE}inline;{$endif} /// return the value as an Unicode WideString // - in SpiderMonkey non-simple type instances do exist in a given JSContext, // so we need to know the execution context (using a property is not an option) function ToWideString(cx: PJSContext): WideString; {$ifdef HASINLINE}inline;{$endif} /// set the value as an UTF-8 String // - in SpiderMonkey non-simple type instances do exist in a given JSContext, // so we need to know the execution context (using a property is not an option) // - warning - JSString string is a subject for GC so you must root it or set // as property of some object or use SetNativeString() method to pass the // value by reference procedure SetUTF8(cx: PJSContext; const aStr: RawUTF8); {$ifdef HASINLINE}inline;{$endif} /// return the value as an UTF-8 String // - in SpiderMonkey non-simple type instances do exist in a given JSContext, // so we need to know the execution context (using a property is not an option) function ToUTF8(cx: PJSContext): RawUTF8; {$ifdef HASINLINE}inline;{$endif} /// set the value from UTF-8 encoded JSON // - returns TRUE if aJSON was valid, FALSE in case of an error function SetJSON(cx: PJSContext; const aJSON: RawUTF8): boolean; /// return the value as UTF-8 encoded JSON function ToJSON(cx: PJSContext): RawUTF8; /// add the value as UTF-8 encoded JSON procedure AddJSON(cx: PJSContext; W: TTextWriter); /// set the value as Unicode String by reference // - this is the fastest way to add a string to SpiderMonley: String is in // fact not copied to the SpiderMonkey engine, just passed by reference // - Only SynUnicode string support by now (SpiderMonkey is internally UTF-16 based) // - WARNING - as a consequence, aStr must be UNCHANGED until SpiderMonkey engine // points to it (SpiderMonkey will also consider its strings as immutable, so will // never change its content during execution) - for instance, never pass a // function result as aStr, nor use a local SynUnicode variable unless you // trigger the Garbage Collection before the end of the local method procedure SetNativeString(cx: PJSContext; const aStr: SynUnicode); /// set the value as a date/time // - in SpiderMonkey non-simple type instances do exist in a given JSContext, // so we need to know the execution context (using a property is not an option) procedure SetDateTime(cx: PJSContext; const Value: TDateTime); /// return the value as a date/time // - in SpiderMonkey non-simple type instances do exist in a given JSContext, // so we need to know the execution context (using a property is not an option) function ToDateTime(cx: PJSContext): TDateTime; /// transform a JSValue to its UTF-16 string representation // - JavaScript equivalent is // ! variable.toString() function TransformToSynUnicode(cx: PJSContext): SynUnicode; /// transform a JSValue to its UTF-8 string representation // - JavaScript equivalent is // ! variable.toString() function TransformToUTF8(cx: PJSContext): RawUTF8; /// attemps to convert the value into a native function pointer function ToNativeFunction(cx: PJSContext): PJSFunction; /// attemps to convert the value into a native function name function ToNativeFunctionName(cx: PJSContext): RawUTF8; end; /// a pointer to a jsval wrapper PSMValue = ^TSMValue; /// a jsval wrappers array TSMValues = array[0..(MaxInt div sizeof(TSMValue))-1] of TSMValue; /// a pointer to a jsval wrappers array PSMValues = ^TSMValues; /// a dynamic array of jsval wrappers SMValArray = array of TSMValue; /// just a wrapper around JavaScript Object API type, to be used with other // values wrappers // - SpiderMonkey object type can NOT be directly casted to this type via // TSMObject(jsobject) - use JSObject wrapper instead - since we expects // an execution context to be specified // - to create instance of this structure, use TSMEngine.NewObject() or // MakeObject() overloaded methods TSMObject = object private FDefaultPropertyAttrs: TJSPropertyAttrs; function GetPrivate: pointer; procedure SetPrivate(const Value: pointer); function GetItem(aIndex: integer): variant; procedure SetItem(aIndex: integer; const Value: variant); procedure SetDefaultPropertyAttrs(const Value: TJSPropertyAttrs); protected fCx: PJSContext; fObj: PJSObject; procedure SetPropVariant(const propName: SynUnicode; const Value: variant); public /// get the parent object of a given object function Parent: TSMObject; {$ifdef HASINLINE}inline;{$endif} /// get the prototype of a given object function Prototype: TSMObject; {$ifdef HASINLINE}inline;{$endif} /// access the private data field of an object // - wrapper to JS_GetPrivate()/JS_SetPrivate() // - only works if the object's JSClass has the JSCLASS_HAS_PRIVATE flag: // it is safer to use GetPrivateData() method and providing the JSClass property PrivateData: pointer read GetPrivate write SetPrivate; /// retrieve the private data associated with an object, if that object // is an instance of a specified class // - wrapper to JS_GetInstancePrivate() function GetPrivateData(expectedClass: PJSClass): pointer; /// return TRUE if the object is an array function IsArray: boolean; /// return the number of elements in this array function ItemsCount: cardinal; /// delete an item of this object as array procedure DeleteItem(aIndex: integer); /// access to an item of this object as array property Items[aIndex: integer]: variant read GetItem write SetItem; /// define an object property with a value, specified as jsvalue // - this is not a direct JavaScript equivalent of // ! obj[name] = val // since any setter will be called // - to set a property in a global object, call either // ! SMEngine.Global.property := ... // via late-binding // ! SMEngine.GlobalObject.DefineProperty() // direct via TSMObject // equivalent in JavaScript to: // ! var name = value // outside a JavaScript function context (i.e. in global scope) // - if property already exists, it will just replace its value with the // supplied value // - this method will use the default properties attributes of this engine procedure DefineProperty(const name: SynUnicode; const value: TSMValue); overload; /// define an object property with a value, specified as jsvalue // - this is not a direct JavaScript equivalent of // ! obj[name] = val // since any setter will be called // - to set a property in a global object, call either // ! SMEngine.Global.property := ... // via late-binding // ! SMEngine.GlobalObject.DefineProperty() // direct via TSMObject // equivalent in JavaScript to: // ! var name = value // outside a JavaScript function context (i.e. in global scope) // - if property already exists, it will just replace its value with the // supplied value // - this method will allow to set custom properties attributes of this engine procedure DefineProperty(const name: SynUnicode; const value: TSMValue; attrs: TJSPropertyAttrs); overload; /// define an object property with a value, specified as variant // - you can also use the property Properties[] // - this is not a direct JavaScript equivalent of // ! obj[name] = val // since any setter will be called // - to set a property in a global object, call either // ! SMEngine.Global.property := ... // via late-binding // ! SMEngine.GlobalObject.DefineProperty() // direct via TSMObject // equivalent in JavaScript to: // ! var name = value // outside a JavaScript function context (i.e. in global scope) // - if property already exists, it will just replace its value with the // supplied value // - this method will use the default properties attributes of this engine procedure DefineProperty(const name: SynUnicode; const value: variant); overload; {$ifdef HASINLINE}inline;{$endif} /// define an object property with a value, specified as variant // - you can also use the property Properties[] // - this is not a direct JavaScript equivalent of // ! obj[name] = val // since any setter will be called // - to set a property in a global object, call either // ! SMEngine.Global.property := ... // via late-binding // ! SMEngine.GlobalObject.DefineProperty() // direct via TSMObject // equivalent in JavaScript to: // ! var name = value // outside a JavaScript function context (i.e. in global scope) // - if property already exists, it will just replace its value with the // supplied value // - this method will allow to set custom properties attributes of this engine procedure DefineProperty(const name: SynUnicode; const value: variant; attrs: TJSPropertyAttrs); overload; {$ifdef HASINLINE}inline;{$endif} /// add JSNative compatible function into JS object // - here the method name is specified as SynUnicode // - func if reference to function with JSNative signature // - nargs is function argument count // - actually this method creates a JSFunction and assing its value to // ! obj[methodName] // - to add a global function, define it into the "global" object - i.e. call // ! TSMEngine.GlobalObject.DefineNativeMethod() // - this method will use the default properties attributes of this engine function DefineNativeMethod(const methodName: SynUnicode; func: JSNative; nargs: uintN): PJSFunction; overload; /// add JSNative compatible function into JS object // - here the method name is specified as SynUnicode // - func if reference to function with JSNative signature // - nargs is function argument count // - actually this method creates a JSFunction and assing its value to // ! obj[methodName] // - to add a global function, define it into the "global" object - i.e. call // ! TSMEngine.GlobalObject.DefineNativeMethod() // - this method will allow to set custom properties attributes of this engine function DefineNativeMethod(const methodName: SynUnicode; func: JSNative; nargs: uintN; attrs: TJSPropertyAttrs): PJSFunction; overload; /// add JSNative compatible function into JS object // - here the method name is specified as AnsiString // - func if reference to function with JSNative signature // - nargs is function argument count // - this method will use the default properties attributes of this engine function DefineNativeMethod(const methodName: AnsiString; func: JSNative; nargs: uintN): PJSFunction; overload; /// add JSNative compatible function into JS object // - here the method name is specified as AnsiString // - func if reference to function with JSNative signature // - nargs is function argument count // - this method will allow to set custom properties attributes of this engine function DefineNativeMethod(const methodName: AnsiString; func: JSNative; nargs: uintN; attrs: TJSPropertyAttrs): PJSFunction; overload; /// check object property does exist (including prototype chain lookup) function HasProperty(const propName: SynUnicode): Boolean; /// Determine whether a property is physically present on a object // - JavaScript equivalent of // ! Object.hasOwnProperty(propName) function HasOwnProperty(const propName: SynUnicode): Boolean; /// get object property value (call geter for native) // - JavaScript equivalent of // ! obj[name] // - returns JSVAL_VOID if object does not have such property function GetPropValue(const propName: SynUnicode): TSMValue; /// get object property value (call geter for native) // - you can also use the property Properties[] // - JavaScript equivalent of // ! obj[name] // - returns null if object does not have such property function GetPropVariant(const propName: SynUnicode): variant; /// read/write access to the object properties as variant property Properties[const propName: SynUnicode]: variant read GetPropVariant write SetPropVariant; default; /// evaluate JavaScript script in the current object scope // - if exception raised in script - raise Delphi ESMException // - on success, returns the last executed expression statement processed // in the script in low-level result output variable // - JavaScript Equivalent of // ! with(obj) eval(script) // - be careful about execution scope - see JS_ExecuteScript() description // - usualy you need to evaluate script only in global object scope, so you // should better always call TSMEngine.Evaluate() procedure Evaluate(const script: SynUnicode; const scriptName: RawUTF8; lineNo: Cardinal; out result: TSMValue); /// executes a JavaScript object method using low-level SMVal arguments // - returns the function result as a TSMValue // - JavaScript equivalent of // ! rval := obj.methodName(argv[0], ....); procedure RunMethod(const methodName: AnsiString; const argv: SMValArray; out rval: TSMValue); overload; /// executes a JavaScript object method using a Delphi array of const // - returns the function result as a TSMValue // - JavaScript equivalent of // ! rval := obj.methodName(argv[0], ....); // - here any AnsiString parameter is expected to be a RawUTF8 before Delphi // 2009, or its correct code page will be retrieved since Delphi 2009 procedure RunMethod(const methodName: AnsiString; const argv: array of const; out rval: TSMValue); overload; /// executes a JavaScript object method using a Delphi array of variants // - returns the function result as a variant // - JavaScript equivalent of // ! rval := obj.methodName(argv[0], ....); function Run(const methodName: AnsiString; const argv: array of variant): variant; /// returns the associated execution context property cx: PJSContext read fCx; /// returns the associated jsobject instance property obj: PJSObject read fObj; /// returns the associated jsobject instance as a jsvalue function AsSMValue: TSMValue; /// protect the object from Garbage Collection // - if this object is not set as property value of any other object // or passed as parameter to function, you must protect it procedure Root; /// unprotect a previously "rooted" object // - WARNING!! Object MUST be protected by a previous Root method call, // otherwise you get an access violation procedure UnRoot; /// set properties obj and cx to nil procedure Clear; /// returns the associated script engine instance function Engine: TSMEngine; {$ifdef HASINLINE}inline;{$endif} /// access to the default attributes when accessing any properties property DefaultPropertyAttrs: TJSPropertyAttrs read FDefaultPropertyAttrs write SetDefaultPropertyAttrs; end; //// variant-based callback signature used for TSMEngine.RegisterMethod() // - any Delphi exception raised during this execution will be converted into // a JavaScript exception by TSMEngine // - "this" JavaScript calling object is transmitted as a TSMVariant custom // variant: you can use late-binding over it to access its methods // or properties, or transtype it using TSMVariantData(Instance) // and access its low-level API content // - input arguments (and function result) are simple variant values, or // TDocVariant custom variant instance for any object as complex document // - corresponds to meVariant kind of callback method TSMEngineMethodEventVariant = function(const This: variant; const Args: array of variant): variant of object; //// JSON-based callback signature used for TSMEngine.RegisterMethod() // - any Delphi exception raised during this execution will be converted into // a JavaScript exception by TSMEngine // - similar to TServiceMethod.InternalExecute() as defined in mORMot.pas // (for instance, this callback will be used to execute native Delphi // interface-based methods from JavaScript code in mORMotSM.pas unit) // - "this" JavaScript calling object is transmitted as low-level TSMObject // - will expect as input a JSON array of parameters from Args, e.g. // ! '[1,2,3]' // - if the method only expect one result, shall return one JSON value, e.g. // ! '6' // - if the method expect more than one result (i.e. several var/out parameters // in addition to the main function result), it shall return a JSON object, // with parameter names for all var/out/result values, e.g. // ! '{"first":1,"second":2,"result":3}' // - this allows the function result to be consumed by the JavaScript as // a regular JS value or object // - corresponds to meJSON kind of callback method TSMEngineMethodEventJSON = function(const This: TSMObject; const Args: RawUTF8): RawUTF8 of object; /// pointer to our wrapper around JavaScript Object PSMObject = ^TSMObject; /// kinds of callback methods available for TSMEngine.RegisterMethod() TSMEngineMethodEventKind = (meVariant, meJSON); /// used to store one registered method event TSMEngineMethodEvent = record Func: PJSFunction; case EventKind: TSMEngineMethodEventKind of meVariant: (CallbackVariant: TSMEngineMethodEventVariant); meJSON: (CallbackJSON: TSMEngineMethodEventJSON); end; /// used to store the registered method events TSMEngineMethodEventDynArray = array of TSMEngineMethodEvent; /// implements a ThreadSafe JavaScript engine // - use TSMEngineManager.ThreadSafeEngine to retrieve the Engine instance // corresponding to the current thread, in multithread application // - contains JSRuntime + JSContext (to be ready for new SpiderMonkey version where // context and runtime is the same) // - contains also one "global" JavaScript object. From script it is // accessible via "global." (in browser, this is the "window." object) // - set SpiderMonkey error reporter and store last SpiderMonkey error in // LastError property TSMEngine = class protected fRt: PJSRuntime; fCx: PJSContext; fcomp: PJSCompartment; fNativeMethod: TSMEngineMethodEventDynArray; fNativeMethods: TDynArrayHashed; fNativeMethodCount: integer; FManager: TSMEngineManager; FGlobal: variant; FGlobalObject: TSMObject; FEngineContentVersion: Cardinal; FStringFinalizer: JSStringFinalizer; FThreadID: TThreadID; FLastErrorMsg: RawUTF8; FLastErrorFileName: RawUTF8; FLastErrorLine: integer; FLastErrorStackTrace: RawUTF8; FErrorExist: boolean; function InternalRegisterMethod(obj: PJSObject; const MethodName: SynUnicode; const Event: TMethod; Kind: TSMEngineMethodEventKind; ArgumentsCount: integer): PJSFunction; /// called from SpiderMonkey callback. Do not raise exception here // instead use CheckJSError metod after JSAPI compile/evaluate call procedure DoProcessJSError(errMsg: PCChar; report: PJSErrorReport); virtual; /// called from SpiderMonkey callback. It used for interrupt execution of script // when it executes too long function DoProcessOperationCallback: JSBool; virtual; procedure CancelExecution; private FDefaultPropertyAttrs: TJSPropertyAttrs; procedure SetDefaultPropertyAttrs(const Value: TJSPropertyAttrs); protected // used by Watchdog thread state. See js.cpp fTimeOutAborted: Boolean; fTimedOut: Boolean; fWatchdogLock: PRLock; fWatchdogWakeup: PRCondVar; fWatchdogThread: PRThread; fWatchdogHasTimeout: Boolean; fWatchdogTimeout: Int64; fSleepWakeup: PRCondVar; fTimeoutInterval: double; function ScheduleWatchdog(t: Double): Boolean; procedure KillWatchdog; function InitWatchdog: boolean; procedure SetTimeoutValue(const Value: Double); public /// create one threadsafe JavaScript Engine instance // - initialize internal JSRuntime, JSContext, and global objects and // standard JavaScript classes // - do not create Engine directly via this constructor, but instead call // TSMEngineManager.ThreadSafeEngine constructor Create(aManager: TSMEngineManager); virtual; /// finalize the JavaScript engine instance destructor Destroy; override; /// check if last call to JSAPI compile/eval fucntion was successful // - raise ESMException if any error occurred // - put error description to SynSMLog procedure CheckJSError(res: JSBool); virtual; /// clear last JavaScript error // - called before every evaluate() function call procedure ClearLastError; /// trigger Garbage Collection // - all unrooted things (JSString, JSObject, VSVal) will be released procedure GarbageCollect; /// Offer the JavaScript engine an opportunity to perform garbage collection if needed // - Tries to determine whether garbage collection in would free up enough // memory to be worth the amount of time it would take. If so, it performs // some garbage collection // - Frequent calls are safe and will not cause the application to spend a // lot of time doing redundant garbage collection work procedure MaybeGarbageCollect; /// create new ordinary JavaScript object // - JavaScript equivalent of // ! {} // - new object is subject to Garbage Collection, so must be rooted or // assigned as value for a property to create new object type property, // as in JavaScript: // ! var obj = {} procedure NewObject(out newobj: TSMObject); overload; /// create new ordinary JavaScript object, stored as TSMVariant custom type // - JavaScript equivalent of // ! {} // - new object is subject to Garbage Collection, so should be // assigned as value for a property to create new object type property, // as in JavaScript: // ! var obj = {} function NewSMVariant: variant; {$ifdef HASINLINE}inline;{$endif} /// create new ordinary JavaScript object, stored as TSMVariant custom type, // and rooted to avoid garbage collection // - JavaScript equivalent of // ! {} // - new object is subject to Garbage Collection, so is rooted and should // be explicitly unrooted, e.g. via: // ! obj: variant; // ! ... // ! FManager.ThreadSafeEngine.NewSMVariantRooted(obj); // ! try // ! ... work with obj // ! finally // ! obj._UnRoot; // pseudo-method // ! end; procedure NewSMVariantRooted(out newobj: variant); /// create new JavaScript object with prototype // - JavaScript equivalent of // ! {}.__proto__ := prototype; procedure NewObject(const prototype: TSMObject; out newobj: TSMObject); overload; /// create new JavaScript object from its class procedure NewObjectWithClass(clasp: PJSClass; var newobj: TSMObject); overload; /// create new JavaScript object from its prototype procedure NewObjectWithClass(clasp: PJSClass; const prototype: TSMObject; const parent: TSMObject; var newobj: TSMObject); overload; /// create new JavaScript object from its class and property specifications procedure InitClass(clasp: PJSClass; ps: PJSPropertySpec; var newobj: TSMObject); /// converts a JavaScript value into a JavaScript object procedure MakeObject(const value: TSMValue; out obj: TSMObject); overload; {$ifdef HASINLINE}inline;{$endif} /// converts a JavaScript low-level value into a JavaScript object procedure MakeObject(const value: jsval; out obj: TSMObject); overload; /// converts a JavaScript low-level object into a JavaScript object procedure MakeObject(jsobj: PJSObject; out obj: TSMObject); overload; /// register a native Delphi variant-based method for a given object // - the supplied function name is case-sensitive // - the supplied callback will be executed directly by the JavaScript // engine, supplying all parameters as variant (including TDocVariant for // any complex object), and returning the function result as variant // - raise an ESMException if the function could not be registered function RegisterMethod(obj: PJSObject; const MethodName: SynUnicode; const Event: TSMEngineMethodEventVariant; ArgumentsCount: integer): PJSFunction; overload; /// register a native Delphi JSON-based method for a given object // - the supplied function name is case-sensitive // - the supplied callback will be executed directly by the JavaScript // engine, supplying all parameters as JSON array, and returning the // function result either as a JSON value or a JSON object // - raise an ESMException if the function could not be registered function RegisterMethod(obj: PJSObject; const MethodName: SynUnicode; const Event: TSMEngineMethodEventJSON; ArgumentsCount: integer): PJSFunction; overload; /// unregister a native Delphi method for a given object // - raise an ESMException if the function was not previously registered // - you should not call it usually, but it is available in case procedure UnRegisterMethod(JSFunction: PJSFunction); /// evaluate a JavaScript script in the global scope // - a wrapper to GlobalObject.Evaluate(...) // - if exception raised in script - raise Delphi ESMException // - on success returns last executed expression statement processed // in the script as a variant // - JavaScript equivalent to // ! eval(script) function Evaluate(const script: SynUnicode; const scriptName: RawUTF8='script'; lineNo: Cardinal=1): variant; /// access to the associated global object as a TSMVariant custom variant // - allows direct property and method executions in Delphi code, via // late-binding, for instance: // ! engine.Global.MyVariable := 1.0594631; // ! engine.Global.MyFunction(1,'text'); property Global: variant read FGlobal; /// access to the associated global object as a TSMObject wrapper // - you can use it to register a method property GlobalObject: TSMObject read FGlobalObject; /// access to the associated global object as low-level PJSObject property GlobalObj: PJSObject read FGlobalObject.fobj; /// access to the associated execution context property cx: PJSContext read fCx; /// access to the associated execution runtime property rt: PJSRuntime read frt; /// access to the associated execution compartment property comp: PJSCompartment read fcomp; /// internal version number of engine scripts // - used in TSMEngine.ThreadSafeEngine to determine if context is up to // date, in order to trigger on-the-fly reload of scripts without the need // if restarting the application // - caller must change this parameter value e.g. in case of changes in // the scripts folder in an HTTP server property EngineContentVersion: Cardinal read FEngineContentVersion; /// last error message triggered during JavaScript execution property LastErrorMsg: RawUTF8 read FLastErrorMsg; /// last error source code line number triggered during JavaScript execution property LastErrorLine: integer read FLastErrorLine; /// last error file name triggered during JavaScript execution property LastErrorFileName: RawUTF8 read FLastErrorFileName; /// TRUE if an error was triggered during JavaScript execution property ErrorExist: boolean read FErrorExist; /// notifies a WatchDog timeout property TimeOutAborted: boolean read FTimeOutAborted; /// define a WatchDog timeout interval // - is set to -1 by default, i.e. meaning no execution timeout property TimeOutValue: Double read fTimeoutInterval write SetTimeoutValue; /// access to the default attributes when accessing any properties property DefaultPropertyAttrs: TJSPropertyAttrs read FDefaultPropertyAttrs write SetDefaultPropertyAttrs; end; /// prototype of SpideMonkey notification callback method TEngineEvent = procedure(const Engine: TSMEngine) of object; /// main access point to the SpiderMonkey per-thread scripting engines // - allow thread-safe access to an internal per-thread TSMEngine instance list // - contains runtime-level properties shared between thread-safe engines // - you can create several TSMEngineManager instances, if you need several // separate scripting instances // - set OnNewEngine callback to initialize each TSMEngine, when a new thread // is accessed, and tune per-engine memory allocation via MaxPerEngineMemory // and MaxRecursionDepth // - get the current per-thread TSMEngine instance via ThreadSafeEngine method TSMEngineManager = class protected FMaxPerEngineMemory: Cardinal; FMaxRecursionDepth: Cardinal; FEnginePool: TObjectList; FEngineCS: TRTLCriticalSection; FContentVersion: Cardinal; FOnNewEngine: TEngineEvent; procedure SetMaxPerEngineMemory(AMaxMem: Cardinal); /// returns -1 if none was defined yet // - this method is not protected via the global FEngineCS mutex/lock function ThreadEngineIndex(ThreadID: TThreadID): Integer; /// returns nil if none was defined yet function CurrentThreadEngine: TSMEngine; /// create a new SpiderMonkey Engine // - used by ThreadSafeEngine method to instantiate a new per-thread Engine function CreateNewEngine: TSMEngine; virtual; /// called when a new Engine is created // - this default implementation will run the OnNewEngine callback (if any) procedure DoOnNewEngine(const Engine: TSMEngine); virtual; public /// initialize the SpiderMonkey scripting engine constructor Create; virtual; /// finalize the SpiderMonkey scripting engine destructor Destroy; override; /// get or create one Engine associated with current running thread // - in single thread application will return the MainEngine function ThreadSafeEngine: TSMEngine; /// method to be called when a thread is about to be finished // - you can call this method just before a thread is finished to ensure // that the associated scripting Engine will be released // - could be used e.g. in a try...finally block inside a TThread.Execute // overriden method procedure ReleaseCurrentThreadEngine; /// internal version of the script files // - used in TSMEngine.ThreadSafeEngine to determine if context is up to // date, in order to trigger on-the-fly reload of scripts without the need // if restarting the application property ContentVersion: Cardinal read FContentVersion write FContentVersion; /// lock/mutex used for thread-safe access to the TSMEngine list property Lock: TRTLCriticalSection read FEngineCS; published /// max amount of memory (in bytes) for a single SpiderMonkey instance // - this parameter will be set only at Engine start, i.e. it must be set // BEFORE any call to ThreadSafeEngine // - default is 8 MB property MaxPerEngineMemory: Cardinal read FMaxPerEngineMemory write SetMaxPerEngineMemory default 8*1024*1024; /// maximum expected recursion depth for JavaScript functions // - to avoid out of memory situation in functions like // ! function f(){ f() }; // - default is 32, but you can specify some higher value property MaxRecursionDepth: Cardinal read FMaxRecursionDepth write FMaxRecursionDepth default 32; /// event triggered every time a new Engine is created // - here your code can change the initial state of the Engine property OnNewEngine: TEngineEvent read FOnNewEngine write FOnNewEngine; end; {$M-} var /// the internal custom variant type used to register TSMVariant SMVariantType: TSynInvokeableVariantType = nil; type /// pointer to a TSMVariant storage PSMVariantData = ^TSMVariantData; /// a custom variant type used to store a SpiderMonkey object in Delphi code // - via the magic of late binding, it will allow access of any JavaScript // object property, or execute any of its methods // - primitive types (i.e. null, string, or numbers) will be stored as // simple variant instances, but JavaScript objects (i.e. objects, prototypes // or functions) can be stored as an instance of this TSMVariant custom type // - you can use the _Root and _UnRoot pseudo-methods, which will protect // the object instance to avoid unexpected Garbage Collection TSMVariant = class(TSynInvokeableVariantType) protected /// fast getter/setter implementation of object properties function IntGet(var Dest: TVarData; const Instance: TVarData; Name: PAnsiChar; NameLen: PtrInt): boolean; override; function IntSet(const Instance, Value: TVarData; Name: PAnsiChar; NameLen: PtrInt): boolean; override; public /// initialize a variant instance to store a JavaScript object class procedure New(const aObject: TSMObject; out aValue: variant); overload; /// initialize a variant instance to store a JavaScript object class procedure New(cx: PJSContext; obj: PJSObject; out aValue: variant); overload; /// initialize a variant instance to store a new JavaScript object class procedure New(engine: TSMEngine; out aValue: variant); overload; // this implementation will let SpiderMonkey write directly the JSON content procedure ToJSON(W: TTextWriter; const Value: variant; Escape: TTextWriterKind); override; /// handle type conversion // - any TSMVariant will be converted to '<>' text procedure Cast(var Dest: TVarData; const Source: TVarData); override; /// handle type conversion // - any TSMVariant will be converted to '<>' text procedure CastTo(var Dest: TVarData; const Source: TVarData; const AVarType: TVarType); override; /// low-level callback to execute any JavaScript object method // - add the _(Index: integer): variant method to retrieve an item // if the object is an array function DoFunction(var Dest: TVarData; const V: TVarData; const Name: string; const Arguments: TVarDataArray): Boolean; override; end; {$A-} { packet object not allowed since Delphi 2009 :( } /// memory structure used for TSMVariant storage of any JavaScript object // as Delphi variant // - primitive types (i.e. null, string, or numbers) will be stored as // simple variant instances, but JavaScript objects (i.e. objects, prototypes // or functions) can be stored as an instance of this TSMVariant custom type // - this variant stores its execution context, so is pretty convenient to // work with in plain Delphi code, also thanks to late-binding feature {$ifdef UNICODE} TSMVariantData = record private {$else} TSMVariantData = object protected {$endif} VType: TVarType; {$IFDEF FPC} {$PUSH} {$ENDIF} {$HINTS OFF} // does not complain if Filler is declared but never used Filler: array[1..SizeOf(TVarData)-SizeOf(TVarType)-SizeOf(TSMObject)] of byte; {$IFDEF FPC} {$POP} {$ELSE} {$HINTS ON} {$ENDIF} VObject: TSMObject; public /// initialize a TSMVariant structure to store a specified JavaScript object procedure Init(const aObject: TSMObject); overload; {$ifdef HASINLINE}inline;{$endif} /// initialize a TSMVariant structure to store a specified JavaScript object procedure Init(aCx: PJSContext; aObj: PJSObject); overload; /// initialize a TSMVariant structure to store a new JavaScript object procedure InitNew(engine: TSMEngine); /// retrieve the global object of this execution context // - you can use this from a native function, e.g.: //!function TMyClass.MyFunction(const This: variant; const Args: array of variant): variant; //!var global: variant; //!begin //! TSMVariantData(This).GetGlobal(global); //! global.anotherFunction(Args[0],Args[1],'test'); //! // same as: //! global := TSMVariantData(This).SMObject.Engine.Global; //! global.anotherFunction(Args[0],Args[1],'test'); //! // but you may also write directly: //! with TSMVariantData(This).SMObject.Engine do //! Global.anotherFunction(Args[0],Args[1],'test'); //! result := AnyTextFileToSynUnicode(Args[0]); //!end; procedure GetGlobal(out global: variant); /// return the custom variant type identifier, i.e. SMVariantType.VarType property VarType: word read VType; /// returns the associated TSMObject instance property SMObject: TSMObject read VObject; /// returns the associated execution context property cx: PJSContext read VObject.fcx; /// returns the associated jsobject instance property obj: PJSObject read VObject.fobj; end; {$A+} /// to be used to catch Delphi exceptions inside JSNative function implementation // - usage example: // ! try // ! doSomething() // ! Result := JS_TRUE; // ! except // ! on E: Exception do begin // ! JS_SET_RVAL(cx, vp, JSVAL_VOID); // ! JSError(cx, E); // ! Result := JS_FALSE; // ! end; procedure JSError(cx: PJSContext; aException: Exception; const aContext: RawByteString=''); /// convert a variant to a Java Script value function VariantToJSVal(cx: PJSContext; const Value: Variant): jsval; var /// define the TSynLog class used for logging for all our SynSM related units // - you may override it with TSQLLog, if available from mORMot.pas // - since not all exceptions are handled specificaly by this unit, you // may better use a common TSynLog class for the whole application or module SynSMLog: TSynLogClass=TSynLog; implementation uses Math; const jsglobal_class: JSClass = (name: 'global'; flags: JSCLASS_HAS_PRIVATE or JSCLASS_GLOBAL_FLAGS { or JSCLASS_NEW_RESOLVE }; addProperty: JS_PropertyStub; delProperty: JS_DeletePropertyStub; getProperty: JS_PropertyStub; setProperty: JS_StrictPropertyStub; enumerate: @JS_EnumerateStub; resolve: JS_ResolveStub; convert: JS_ConvertStub //finalize is Optionally non-null member //in source it marked as Mandatory, but it doesn't //use in tests and there is no exported function JS_FinalizeStub ); /// handle errors from JavaScript. Just call DoProcessJSError of corresponding TSMEngine // to set TSMEngine error properties procedure ErrorReporter(cx: PJSContext; pErrMsg: PCChar; report: PJSErrorReport); cdecl; begin TSMEngine(cx.PrivateData).DoProcessJSError(pErrMsg, report) end; procedure JSError(cx: PJSContext; aException: Exception; const aContext: RawByteString); begin if JS_IsExceptionPending(cx)=JS_FALSE then // raise only if this is the first exception in chain if aException is EOutOfMemory then JS_ReportOutOfMemory(cx) else JS_ReportError(cx, PCchar(AnsiString(aException.Message)+AnsiString(aContext))); end; function OperationCallback(cx: PJSContext): JSBool; cdecl; begin Result := TSMEngine(cx.PrivateData).DoProcessOperationCallback; end; { TSMEngine } // do nothing here procedure ExternalStringFinalizer(fin: PJSStringFinalizer; chars: Pjschar); cdecl; begin {} end; constructor TSMEngine.Create(aManager: TSMEngineManager); const Opt: CompartmentOptions = ( zoneSpec: zsFreshZone; hasVersion: True; version: JSVERSION_LATEST; ); gMaxStackSize = 128 * sizeof(size_t) * 1024; begin if aManager = nil then raise ESMException.CreateUTF8('%.Create(nil): No manager provided',[self]); FDefaultPropertyAttrs := [jspEnumerate]; fNativeMethods.Init(TypeInfo(TSMEngineMethodEventDynArray), fNativeMethod,HashPointer,SortDynArrayPointer,nil,@fNativeMethodCount); {$ifdef RESETFPUEXCEPTION} TSynFPUException.ForLibraryCode; {$endif} FManager := aManager; FEngineContentVersion := FManager.ContentVersion; frt := JS_NewRuntime(FManager.MaxPerEngineMemory, JS_USE_HELPER_THREADS); if frt = nil then raise ESMException.CreateUTF8('%.Create runtime: out of memory',[self]); JS_SetNativeStackQuota(rt, gMaxStackSize); JS_SetGCParameter(frt, JSGC_MAX_BYTES, FManager.MaxPerEngineMemory); JS_SetGCParameter(frt, JSGC_MAX_MALLOC_BYTES, FManager.MaxPerEngineMemory div 2); JS_SetGCParameter(frt, JSGC_MODE, uint32(JSGC_MODE_INCREMENTAL) ); fCx := JS_NewContext(rt, STACK_CHUNK_SIZE); if fCx = nil then raise ESMException.CreateUTF8('%.Create: JS_NewContext failure',[self]); // You must set jsoBaseLine,jsoTypeInference,jsoIon for the enabling ION // ION is disabled without these options {$ifdef FIXBUGXE3} fCx.SetOptions([jsoVarObjFix,jsoBaseLine,jsoTypeInference,jsoIon,jsoAsmJs]); {$else} fCx.Options := [jsoVarObjFix,jsoBaseLine,jsoTypeInference,jsoIon,jsoAsmJs]; {$endif} fStringFinalizer.finalize := ExternalStringFinalizer; JS_SetContextPrivate(cx, self); JS_SetErrorReporter(cx, ErrorReporter); FGlobalObject.fCx := cx; FGlobalObject.fObj := JS_NewGlobalObject(cx, @jsglobal_class, nil, @Opt); if GlobalObj = nil then raise ESMException.CreateUTF8('%.Create: JS_NewGlobalObject failure',[self]); fcomp := fcomp.EnterCompartment(cx,GlobalObj); if JS_InitStandardClasses(cx, GlobalObj)<>JS_TRUE then raise ESMException.CreateUTF8('%.Create: JS_InitStandardClasses failure',[self]); FGlobalObject.DefineProperty('global', GlobalObject.AsSMValue, [jspEnumerate,jspPermanent,jspReadOnly]); TSMVariantData(FGlobal).Init(FGlobalObject); fTimeoutInterval := -1; if not InitWatchdog then raise ESMException.CreateUTF8('%.Create: InitWatchDog failure',[self]); JS_SetOperationCallback(cx, OperationCallback); end; destructor TSMEngine.Destroy; begin inherited Destroy; VarClear(FGlobal); {$ifdef RESETFPUEXCEPTION} TSynFPUException.ForLibraryCode; {$endif} //JS_RemoveExternalStringFinalizer(ExternalStringFinalizer); // comp^.Destroy; JS_LeaveCompartment(cx, comp); if FThreadID=GetCurrentThreadId then cx^.Destroy; // SM 24 expects the context to be released in the same thread KillWatchdog; rt^.Destroy; end; procedure TSMEngine.DoProcessJSError(errMsg: PCChar; report: PJSErrorReport); const PATTERN: PUTF8Char = '[JSError %] % (%): %'; var exc: jsval; pExObj: PJSObject; msg: SynUnicode; begin FErrorExist := True; if report^.filename = nil then FLastErrorFileName := '(w/o name)' else FLastErrorFileName := CurrentAnsiConvert.AnsiBufferToRawUTF8( report^.filename,StrLen(pointer(report^.filename))); FLastErrorLine := report^.lineno; if report^.ucmessage=nil then FLastErrorMsg := FormatUTF8(PATTERN,[report^.errorNumber,FLastErrorFileName, FLastErrorLine,errMsg]) else FLastErrorMsg := FormatUTF8(PATTERN,[report^.errorNumber,FLastErrorFileName, FLastErrorLine,PWideChar(report^.ucmessage)]); FLastErrorStackTrace := ''; if ( JS_GetPendingException(cx, exc) = JS_TRUE ) then begin if JSVAL_IS_OBJECT(exc) then begin pExObj := JSVAL_TO_OBJECT(exc); JS_GetProperty(cx, pExObj, 'stack', exc); if (exc <> JSVAL_VOID) and JSVAL_IS_STRING(exc) then begin msg := JSVAL_TO_STRING(exc).ToSynUnicode(cx); if msg <> '' then // this can happend in case syntax error while parse script FLastErrorStackTrace := SynUnicodeToUtf8(msg); end; end; end; (* // This situation is possible when application are run from the IDE // and stop on the breakpoint. // When we evaluate some js script with errors(like call JS_Stringify // for global object) this function will be called. // If breakpoint is set between ClearLastError and CheckJSError we get // FErrorExist value is equivalent true, but script have no error if DebugHook=0 then try CheckJSError(JS_FALSE); finally FErrorExist := false; end; *) end; procedure TSMEngine.CheckJSError(res: JSBool); begin if FTimeOutAborted then raise ESMException.CreateUTF8('%: script runs for too long, abort',[self]); if FErrorExist then begin SynSMLog.Add.Log(sllError, FLastErrorMsg); raise ESMException.CreateUTF8('% error: %',[self,FLastErrorMsg]); end; if res=JS_FALSE then begin SynSMLog.Add.Log(sllError, 'Error compiling script %', FLastErrorFileName); raise ESMException.CreateUTF8('%: Error compiling script [%]. Line %', [self,FLastErrorFileName,FLastErrorLine]); end; end; procedure TSMEngine.ClearLastError; begin JS_ClearPendingException(cx); FErrorExist := False; FTimeOutAborted := False; end; procedure TSMEngine.GarbageCollect; begin JS_GC(rt); end; procedure TSMEngine.MaybeGarbageCollect; begin JS_MaybeGC(cx); end; procedure TSMEngine.NewObject(out newobj: TSMObject); begin newobj.fCx := cx; newobj.fObj := JS_NewObject(cx, nil{class}, nil{proto}, globalObj{parent}); newobj.FDefaultPropertyAttrs := self.FDefaultPropertyAttrs; if newobj.fObj=nil then raise ESMException.CreateUTF8('%.NewObject',[self]); end; procedure TSMEngine.NewObject(const prototype: TSMObject; out newobj: TSMObject); begin newobj.fCx := cx; newobj.fObj := JS_NewObject(cx, nil{class}, prototype.obj, nil{parent}); newobj.FDefaultPropertyAttrs := self.FDefaultPropertyAttrs; if newobj.fObj=nil then raise ESMException.CreateUTF8('%.NewObject(prototype)',[self]); end; procedure TSMEngine.NewObjectWithClass(clasp: PJSClass; var newobj: TSMObject); begin newobj.fCx := cx; newobj.fObj := JS_NewObject(cx, clasp, nil, nil); newobj.FDefaultPropertyAttrs := self.FDefaultPropertyAttrs; if newobj.fObj=nil then raise ESMException.CreateUTF8('%.NewObjectWithClass',[self]); end; procedure TSMEngine.NewObjectWithClass(clasp: PJSClass; const prototype: TSMObject; const parent: TSMObject; var newobj: TSMObject); begin newobj.fCx := cx; newobj.fObj := JS_NewObject(cx, clasp, prototype.obj, parent.obj); newobj.FDefaultPropertyAttrs := self.FDefaultPropertyAttrs; if newobj.fObj=nil then raise ESMException.CreateUTF8('%.NewObjectWithClass(parent)',[self]); end; procedure TSMEngine.InitClass(clasp: PJSClass; ps: PJSPropertySpec; var newobj: TSMObject); begin newobj.fCx := cx; newobj.fObj := JS_InitClass(cx, GlobalObj, nil, clasp, nil, 0, ps , nil, nil, nil); newobj.FDefaultPropertyAttrs := self.FDefaultPropertyAttrs; if newobj.obj=nil then raise ESMException.CreateUTF8('%.InitClass',[self]); end; procedure TSMEngine.MakeObject(const value: TSMValue; out obj: TSMObject); begin MakeObject(value.FValue,obj); end; procedure TSMEngine.MakeObject(const value: jsval; out obj: TSMObject); begin if JSVAL_IS_OBJECT(value) then begin obj.fCx := cx; obj.fObj := JSVAL_TO_OBJECT(value); obj.FDefaultPropertyAttrs := self.FDefaultPropertyAttrs; end else raise ESMException.CreateUTF8('%.MakeObject(value: not an object)',[self]); end; procedure TSMEngine.MakeObject(jsobj: PJSObject; out obj: TSMObject); begin obj.fCx := cx; obj.fObj := jsobj; end; function TSMEngine.NewSMVariant: variant; begin TSMVariant.New(self,result); end; procedure TSMEngine.NewSMVariantRooted(out newobj: variant); begin TSMVariant.New(self,newobj); TSMVariantData(newobj).VObject.Root; end; function TSMEngine.Evaluate(const script: SynUnicode; const scriptName: RawUTF8; lineNo: Cardinal): variant; var res: TSMValue; begin globalObject.Evaluate(script, scriptName, lineNo, res); res.ToVariant(cx,result); end; function nsm_methodDelphi(cx: PJSContext; argc: uintN; vp: Pjsval): JSBool; cdecl; var engine: TSMEngine; argv: PSMValues; f: PJSFunction; instance: TSMVariantData; callee,res: TSMValue; method: integer; procedure RunAsVariant(const CallbackVariant: TSMEngineMethodEventVariant); var a: integer; Args: TVariantDynArray; begin SetLength(Args,argc); for a := 0 to argc-1 do argv^[a].ToVariant(cx,Args[a]); res.SetVariant(cx,CallbackVariant(Variant(instance),Args)); end; procedure RunAsJson(const CallbackJSON: TSMEngineMethodEventJSON); var a: integer; W: TTextWriter; begin W := TTextWriter.CreateOwnedStream(4096); try W.Add('['); for a := 0 to argc-1 do begin argv^[a].AddJSON(cx,W); W.Add(','); end; W.CancelLastComma; W.Add(']'); res.SetJSON(cx,CallbackJSON(instance.SMObject,W.Text)); finally W.Free; end; end; procedure RunError(E: Exception); begin // avoid temporary allocation of strings on the stack JSError(cx, E, FormatUTF8(' for function %()', [callee.ToNativeFunctionName(cx)])); end; begin {$ifdef RESETFPUEXCEPTION} TSynFPUException.ForDelphiCode; // ensure we are back in Delphi FPU mask {$endif} try engine := cx.PrivateData; callee.FValue := vp^; f := callee.ToNativeFunction(cx); if f=nil then method := -1 else method := engine.fNativeMethods.FindHashed(f); if method<0 then raise ESMException.Create('nsm_methodDelphi: No callback defined'); argv := pointer(JS_ARGV(cx,vp)); instance.Init(cx,JS_THIS_OBJECT(cx,vp)); with engine.fNativeMethod[method] do case EventKind of meVariant: RunAsVariant(CallbackVariant); meJSON: RunAsJson(CallbackJSON); else raise ESMException.CreateUTF8('nsm_methodDelphi: Unknown EventKind=%', [ord(EventKind)]); end; JS_SET_RVAL(cx,vp,res.FValue); result := JS_TRUE; except on E: Exception do begin RunError(E); JS_SET_RVAL(cx,vp,JSVAL_VOID); result := JS_FALSE; end; end; end; function TSMEngine.InternalRegisterMethod(obj: PJSObject; const MethodName: SynUnicode; const Event: TMethod; Kind: TSMEngineMethodEventKind; ArgumentsCount: integer): PJSFunction; var added: boolean; i: integer; begin result := JS_DefineUCFunction(cx, obj, pointer(MethodName), Length(MethodName), nsm_methodDelphi, ArgumentsCount, JSPROP_ENUMERATE); if result=nil then raise ESMException.CreateUTF8( '%.InternalRegisterMethod(%): Defining native function',[self,MethodName]); i := fNativeMethods.FindHashedForAdding(result,added); if added then with fNativeMethod[i] do begin Func := result; EventKind := Kind; TMethod(CallbackVariant) := Event; end else raise ESMException.CreateUTF8( '%.InternalRegisterMethod(%): Duplicated name',[self,MethodName]); end; function TSMEngine.RegisterMethod(obj: PJSObject; const MethodName: SynUnicode; const Event: TSMEngineMethodEventVariant; ArgumentsCount: integer): PJSFunction; begin result := InternalRegisterMethod(obj,MethodName, TMethod(Event),meVariant,ArgumentsCount); end; function TSMEngine.RegisterMethod(obj: PJSObject; const MethodName: SynUnicode; const Event: TSMEngineMethodEventJSON; ArgumentsCount: integer): PJSFunction; begin result := InternalRegisterMethod(obj,MethodName, TMethod(Event),meJSON,ArgumentsCount); end; procedure TSMEngine.UnRegisterMethod(JSFunction: PJSFunction); var i: integer; begin i := fNativeMethods.FindHashed(JSFunction); if i<0 then raise ESMException.CreateUTF8( '%.UnRegisterMethod(%): Method not previously registered',[self,JSFunction]); fNativeMethods.Delete(i); end; { TSMEngineManager } constructor TSMEngineManager.Create; begin FMaxPerEngineMemory := 8*1024*1024; FMaxRecursionDepth := 32; FEnginePool := TObjectList.Create; InitializeCriticalSection(fEngineCS); end; procedure TSMEngineManager.SetMaxPerEngineMemory(AMaxMem: Cardinal); begin if aMaxMem= STACK_CHUNK_SIZE*%, i.e. %', [self,aMaxMem,MaxRecursionDepth,STACK_CHUNK_SIZE*MaxRecursionDepth]); FMaxPerEngineMemory := AMaxMem; end; function TSMEngineManager.ThreadEngineIndex(ThreadID: TThreadID): Integer; begin if self<>nil then for result := 0 to FEnginePool.Count-1 do if TSMEngine(FEnginePool.List[result]).fThreadID=ThreadID then exit; result := -1; end; destructor TSMEngineManager.Destroy; begin FEnginePool.Free; inherited; DeleteCriticalSection(fEngineCS); end; procedure TSMEngineManager.DoOnNewEngine(const Engine: TSMEngine); begin if Assigned(FOnNewEngine) then FOnNewEngine(Engine); end; function TSMEngineManager.ThreadSafeEngine: TSMEngine; var i: integer; ThreadID: TThreadID; begin EnterCriticalSection(fEngineCS); try ThreadID := GetCurrentThreadId; i := ThreadEngineIndex(ThreadID); // inlined CurrentThreadEngine if i<0 then result := nil else result := FEnginePool.List[i]; if result<>nil then if result.EngineContentVersion=Self.ContentVersion then // return existing Engine corresponding to the current thread exit else begin // content version changed -> force recreate thread Engine {$ifdef SM_DEBUG} SynSMLog.Add.Log(sllDebug, 'Drop SpiderMonkey Engine for thread % - modification found',ThreadID); {$endif} FEnginePool.Delete(i); // as in ReleaseCurrentThreadEngine end; // here result=nil or to be ignored (just dropped) {$ifdef SM_DEBUG} SynSMLog.Add.Log(sllDebug, 'Create new JavaScript Engine for thread %',ThreadID); {$endif} result := CreateNewEngine; result.fThreadID := ThreadID; FEnginePool.Add(result); finally LeaveCriticalSection(fEngineCS); end; end; procedure TSMEngineManager.ReleaseCurrentThreadEngine; var i: integer; begin EnterCriticalSection(fEngineCS); try i := ThreadEngineIndex(GetCurrentThreadId); if i>=0 then begin (FEnginePool[i] as TSMEngine).GarbageCollect; FEnginePool.Delete(i); end; finally LeaveCriticalSection(fEngineCS); end; end; function TSMEngineManager.CurrentThreadEngine: TSMEngine; var i: integer; begin EnterCriticalSection(fEngineCS); try i := ThreadEngineIndex(GetCurrentThreadId); if i < 0 then result := nil else result := FEnginePool.List[i]; finally LeaveCriticalSection(fEngineCS); end; end; function TSMEngineManager.CreateNewEngine: TSMEngine; begin Result := TSMEngine.Create(Self); if Assigned(FOnNewEngine) then begin {$ifdef JS_THREADSAFE} JS_BeginRequest(Result.cx); try {$endif} FOnNewEngine(Result); {$ifdef JS_THREADSAFE} finally JS_EndRequest(Result.cx); end; {$endif} end; end; { TSMValue } function VariantToJSVal(cx: PJSContext; const Value: Variant): jsval; begin TSMValue(result).SetVariant(cx,Value); end; function TSMValue.ToInteger: integer; begin {$ifndef WITHASSERT} if not JSVAL_IS_INT(FValue) then raise ESMException.Create('TSMValue.ToInteger!'); {$endif} Result := JSVAL_TO_INT(FValue); end; procedure TSMValue.SetInteger(const Value: integer); begin FValue := INT_TO_JSVAL(Value); end; function TSMValue.ToDouble: double; begin {$ifndef WITHASSERT} if not JSVAL_IS_DOUBLE(FValue) then raise ESMException.Create('TSMValue.ToDouble!'); {$endif} Result := JSVAL_TO_DOUBLE(FValue); end; procedure TSMValue.SetDouble(const Value: double); begin FValue := DOUBLE_TO_JSVAL(Value); end; function TSMValue.ToBoolean: boolean; begin Result := (FValue=JSVAL_TRUE); end; procedure TSMValue.SetBoolean(const Value: boolean); begin if Value then FValue := JSVAL_TRUE else FValue := JSVAL_FALSE; end; function TSMValue.ToInt64: int64; begin if JSVAL_IS_INT(FValue) then result := JSVAL_TO_INT(FValue) else {$ifndef WITHASSERT} if not JSVAL_IS_DOUBLE(FValue) then raise ESMException.Create('TSMValue.ToInt64!') else {$endif} result := trunc(JSVAL_TO_DOUBLE(FValue)); end; procedure TSMValue.SetInt64(const Value: int64); begin if (Value>=Low(integer)) and (Value<=High(integer)) then FValue := INT_TO_JSVAL(Value) else FValue := DOUBLE_TO_JSVAL(Value); end; function TSMValue.ValType(cx: PJSContext): JSType; begin Result := JS_TypeOfValue(cx, FValue); end; function TSMValue.ToVariant(cx: PJSContext): Variant; begin ToVariant(cx,result); end; procedure TSMValue.ToVariant(cx: PJSContext; var result: Variant); begin case ValType(cx) of JSTYPE_VOID: VarClear(result); JSTYPE_NULL: SetVariantNull(result); JSTYPE_OBJECT: TSMVariant.New(cx,JSVAL_TO_OBJECT(FValue),result); JSTYPE_STRING: JSVAL_TO_STRING(FValue).ToVariant(cx,result); JSTYPE_NUMBER: if JSVAL_IS_INT(FValue) then result := JSVAL_TO_INT(FValue) else result := JSVAL_TO_DOUBLE(FValue); JSTYPE_BOOLEAN: result := JSVAL_TO_BOOLEAN(FValue)=JS_TRUE; JSTYPE_FUNCTION: result := TransformToSynUnicode(cx); else raise ESMException.CreateUTF8('Unhandled ToVariant(%)',[ord(ValType(cx))]); end; end; procedure TSMValue.SetVariant(cx: PJSContext; const Value: Variant); var CustomVariantType: TCustomVariantType; begin with TVarData(Value) do case VType of varNull: FValue := JSVAL_NULL; varEmpty: FValue := JSVAL_VOID; varBoolean: if VBoolean then FValue := JSVAL_TRUE else FValue := JSVAL_FALSE; varSmallint: FValue := INT_TO_JSVAL(VSmallInt); {$ifndef DELPHI5OROLDER} varShortInt: FValue := INT_TO_JSVAL(VShortInt); varWord: FValue := INT_TO_JSVAL(VWord); varLongWord: if VLongWord<=cardinal(high(Integer)) then FValue := INT_TO_JSVAL(VLongWord) else FValue := DOUBLE_TO_JSVAL(VLongWord); {$endif} varByte: FValue := INT_TO_JSVAL(VByte); varInteger: FValue := INT_TO_JSVAL(VInteger); varInt64: SetInt64(VInt64); varSingle: FValue := DOUBLE_TO_JSVAL(VSingle); varDouble: FValue := DOUBLE_TO_JSVAL(VDouble); varCurrency: FValue := DOUBLE_TO_JSVAL(VCurrency); varDate: SetDateTime(cx,VDate); varOleStr: SetWideString(cx,WideString(VAny)); varString: SetAnsiChar(cx,VAny,length(RawByteString(VAny)), {$ifndef HASVARUSTRING} CP_UTF8); {$else} StringCodePage(RawByteString(VAny))); varUString: SetSynUnicode(cx,UnicodeString(VAny)); {$endif} else if VType=varByRef or varVariant then SetVariant(cx,PVariant(VPointer)^) else if VType=varByRef or varOleStr then SetWideString(cx,PWideString(VAny)^) else {$ifdef HASVARUSTRING} if VType=varByRef or varUString then SetSynUnicode(cx,PUnicodeString(VAny)^) else {$endif} if (SMVariantType<>nil) and (VType=SMVariantType.VarType) then FValue := OBJECT_TO_JSVAL(TSMVariantData(Value).obj) else if FindCustomVariantType(VType,CustomVariantType) and CustomVariantType.InheritsFrom(TSynInvokeableVariantType) then SetJSON(cx,VariantSaveJSON(Value)) else raise ESMException.CreateUTF8('Unhandled variant type %',[VType]); end; end; procedure TSMValue.SetTVarRec(cx: PJSContext; const V: TVarRec); begin case V.VType of vtPointer: FValue := JSVAL_VOID; vtBoolean: if V.VBoolean then FValue := JSVAL_TRUE else FValue := JSVAL_FALSE; vtInteger: FValue := INT_TO_JSVAL(V.VInteger); vtInt64{$ifdef FPC},vtQWord{$endif}: SetInt64(V.VInt64^); vtCurrency: FValue := DOUBLE_TO_JSVAL(V.VCurrency^); vtExtended: FValue := DOUBLE_TO_JSVAL(V.VExtended^); vtVariant: SetVariant(cx,V.VVariant^); vtWideString: SetWideString(cx,WideString(V.VPointer)); vtAnsiString: SetAnsiChar(cx,V.VPointer,length(RawByteString(V.VAnsiString)), {$ifndef HASCODEPAGE} CP_UTF8); {$else} StringCodePage(RawByteString(V.VAnsiString))); vtUnicodeString: SetSynUnicode(cx,UnicodeString(V.VPointer)); {$endif} vtString: SetAnsiChar(cx,PAnsiChar(@V.VString^[1]),ord(V.VString^[0]),0); vtPChar: SetAnsiChar(cx,V.VPChar,StrLen(V.VPointer),0); vtChar: SetAnsiChar(cx,@V.VChar,1,0); vtWideChar: FValue := STRING_TO_JSVAL(cx^.NewJSString(PWideChar(@V.VWideChar),1)); else raise ESMException.CreateUTF8('Unhandled TVarRec.VType=%',[V.VType]); end; end; function TSMValue.ToSynUnicode(cx: PJSContext): SynUnicode; begin ToSynUnicode(cx,result); end; procedure TSMValue.ToSynUnicode(cx: PJSContext; var result: SynUnicode); begin Result := JSVAL_TO_STRING(FValue).ToSynUnicode(cx); end; procedure TSMValue.SetSynUnicode(cx: PJSContext; const aStr: SynUnicode); begin SetWideChar(cx,pointer(aStr),length(aStr)); end; function TSMValue.ToWideString(cx: PJSContext): WideString; begin Result := JSVAL_TO_STRING(FValue).ToWideString(cx); end; procedure TSMValue.SetWideString(cx: PJSContext; const aStr: WideString); begin SetWideChar(cx,pointer(aStr),length(aStr)); end; procedure TSMValue.SetWideChar(cx: PJSContext; Text: PWideChar; TextLen: integer); begin if (Text=nil) or (TextLen=0) then FValue := JS_GetEmptyStringValue(cx) else FValue := STRING_TO_JSVAL(cx^.NewJSString(Text,TextLen)); end; procedure TSMValue.SetAnsiChar(cx: PJSContext; Text: PAnsiChar; TextLen, CodePage: integer); begin if (Text=nil) or (TextLen=0) then FValue := JS_GetEmptyStringValue(cx) else FValue := STRING_TO_JSVAL(cx^.NewJSString(Text,TextLen,CodePage)); end; function TSMValue.ToUTF8(cx: PJSContext): RawUTF8; begin Result := JSVAL_TO_STRING(FValue).ToUTF8(cx); end; procedure TSMValue.SetUTF8(cx: PJSContext; const aStr: RawUTF8); begin FValue := STRING_TO_JSVAL(cx^.NewJSString(aStr)); end; procedure TSMValue.SetNativeString(cx: PJSContext; const aStr: SynUnicode); begin FValue := STRING_TO_JSVAL(JS_NewExternalString(cx, pointer(aStr), length(aStr), @TSMEngine(cx.PrivateData).FStringFinalizer)); end; function TSMValue.ToDateTime(cx: PJSContext): TDateTime; var oDate: PJSObject; {$ifdef CONSIDER_TIME_IN_Z} // as defined in SynSM.inc ms: double; ms64: Int64; fval: jsval; {$else} d, m, Y, h, mn, s, ml: Integer; v, fval: jsval; function GetIntFuncPropVal(funcName: PWideChar): Integer; begin Result := 0; if JS_GetUCProperty(cx, oDate, pointer(funcName), Length(funcName), fval) = JS_TRUE then if JS_CallFunctionValue(cx, oDate, fval, 0, nil, v) = JS_TRUE then Result := JSVAL_TO_INT(v); end; {$endif} begin oDate := JSVAL_TO_OBJECT(FValue); if JS_ObjectIsDate(cx, oDate) = JS_FALSE then raise ESMException.Create('TSMValue.ToDateTime: not a DateTime object'); {$ifdef CONSIDER_TIME_IN_Z} ms := 0; if JS_CallFunctionName(cx, oDate, PCChar('getTime'), 0, nil, fval) = JS_TRUE then ms := JSVAL_TO_DOUBLE(fval); if ms = 0 then raise ESMException.Create('TSMValue.ToDateTime: no getTime() in Date object'); ms64 := Trunc(ms); // W/O millisec: Result := IncMilliSecond(UnixDateDelta, ms64); Result := UnixMSTimeToDateTime(ms64); {$else} d := GetIntFuncPropVal('getDate'); m := GetIntFuncPropVal('getMonth') + 1; //WTF months start from 0 Y := GetIntFuncPropVal('getFullYear'); h := GetIntFuncPropVal('getHours'); mn := GetIntFuncPropVal('getMinutes'); s := GetIntFuncPropVal('getSeconds'); ml := GetIntFuncPropVal('getMilliseconds'); Result := EncodeDateTime(Y, m, d, h, mn, s, ml); {$endif} end; procedure TSMValue.SetDateTime(cx: PJSContext; const Value: TDateTime); var dmsec: double; unixTime: Int64; {$ifdef CONSIDER_TIME_IN_Z} // as defined in SynSM.inc oDate: PJSObject; {$else} // this realisation is buggy - it ignores timezone rules change history // for server-side realisation the best solution is to use GMT time here ms: Word; STLocal, STUtc: TSystemTime; TZ: TTimeZoneInformation; AUTCDateTime: TDateTime; {$endif} begin {$ifdef CONSIDER_TIME_IN_Z} unixTime := DateTimeToUnixMSTime(Value); dmsec := unixTime-(unixTime mod 1000); oDate := JS_NewDateObjectMsec(cx, dmsec); if JS_ObjectIsDate(cx, oDate)<>JS_TRUE then raise ESMException.CreateUTF8('TSMValue.SetDateTime(%): not a valid date',[Value]); FValue := oDate.ToJSValue; {$else} DateTimeToSystemTime(Value, STLocal); GetTimeZoneInformation(TZ); // use TzSpecificLocalTimeToSystemTime? TZ.Bias := -TZ.Bias; TZ.StandardBias := -TZ.StandardBias; TZ.DaylightBias := -TZ.DaylightBias; SystemTimeToTzSpecificLocalTime(@TZ, STLocal, STUtc); ms := STUtc.wMilliseconds; AUTCDateTime := SystemTimeToDateTime(STUtc); dmSec := DateTimeToUnixMSTime(AUTCDateTime) + ms; FValue := JS_NewDateObjectMsec(cx, dmsec).ToJSValue; {$endif} end; function TSMValue.TransformToSynUnicode(cx: PJSContext): SynUnicode; begin Result := JS_ValueToString(cx, FValue).ToSynUnicode(cx); end; function TSMValue.TransformToUTF8(cx: PJSContext): RawUTF8; begin Result := JS_ValueToString(cx, FValue).ToUTF8(cx); end; function TSMValue.ToNativeFunction(cx: PJSContext): PJSFunction; begin if (not JSVAL_IS_OBJECT(FValue)) or (JS_ObjectIsFunction(cx,JSVAL_TO_OBJECT(FValue))=JS_FALSE) then result := nil else result := JS_ValueToFunction(cx,FValue); end; function TSMValue.ToNativeFunctionName(cx: PJSContext): RawUTF8; var str: PJSString; buf,name: PWideChar; len: size_t; begin result := ''; if (@self=nil) or (not JSVAL_IS_OBJECT(FValue)) or (JS_ObjectIsFunction(cx,JSVAL_TO_OBJECT(FValue))=JS_FALSE) then exit; str := JS_ValueToString(cx, FValue); if str=nil then exit; buf := PWideChar(JS_GetStringCharsAndLength(cx, str, len)); if (len<10) or not IdemPCharW(buf,'FUNCTION ') then exit; dec(len,9); inc(buf,9); name := buf; while (len>0) and (buf^<>'(') do begin dec(len); inc(buf); end; RawUnicodeToUtf8(name,buf-name,result); end; function writeCallback(const buf: Pjschar; len: uint32; data: pointer): JSBool; cdecl; begin TTextWriter(data).AddNoJSONEscapeW(pointer(buf),len); result := JS_TRUE; end; procedure TSMValue.AddJSON(cx: PJSContext; W: TTextWriter); begin if @self=nil then W.AddShort('null') else case ValType(cx) of JSTYPE_VOID, JSTYPE_NULL: W.AddShort('null'); JSTYPE_STRING: JSVAL_TO_STRING(FValue).ToJSONString(cx,W); JSTYPE_NUMBER: if JSVAL_IS_INT(FValue) then W.Add(JSVAL_TO_INT(FValue)) else W.AddDouble(JSVAL_TO_DOUBLE(FValue)); JSTYPE_BOOLEAN: W.Add(JSVAL_TO_BOOLEAN(FValue)=JS_TRUE); JSTYPE_OBJECT, JSTYPE_FUNCTION: begin if JS_Stringify(cx, @FValue, nil, JSVAL_NULL, writeCallback, pointer(W))<>JS_TRUE then begin TSMEngine(cx.PrivateData).CheckJSError(JS_FALSE); TSMEngine(cx.PrivateData).ClearLastError; end end else raise ESMException.CreateUTF8( 'Unhandled TSMValue.AddJSON(%)',[ord(ValType(cx))]); end; end; function TSMValue.ToJSON(cx: PJSContext): RawUTF8; var W: TJSONWriter; tmp: TTextWriterStackBuffer; begin W := TJSONWriter.CreateOwnedStream(tmp); try AddJSON(cx,W); W.SetText(result); finally W.Free; end; end; function TSMValue.SetJSON(cx: PJSContext; const aJSON: RawUTF8): boolean; var tmp: RawUnicode; len: integer; begin if aJSON='' then begin SetVoid; result := true; end else begin len := Utf8DecodeToRawUnicodeUI(aJSON,tmp); result := JS_ParseJSON(cx,pointer(tmp),len shr 1,@self)<>JS_FALSE; end; end; procedure TSMValue.SetNull; begin FValue := JSVAL_NULL; end; procedure TSMValue.SetVoid; begin FValue := JSVAL_VOID; end; { TSMObject } function TSMObject.Engine: TSMEngine; begin if @self=nil then Result := nil else Result := TSMEngine(cx.PrivateData); end; function TSMObject.AsSMValue: TSMValue; begin if (@self=nil) or (obj=nil) then Result.FValue := JSVAL_NULL else Result.FValue := OBJECT_TO_JSVAL(obj); end; procedure TSMObject.DefineProperty(const name: SynUnicode; const value: TSMValue; attrs: TJSPropertyAttrs); begin if (@self=nil) or (cx=nil) or (obj=nil) or (JS_DefineUCProperty(cx, Obj, pointer(name), length(name), value.AsJSVal, nil, nil, word(attrs))<>JS_TRUE) then raise ESMException.CreateUTF8('TSMObject.DefineProperty(%)', [name]); end; procedure TSMObject.DefineProperty(const name: SynUnicode; const value: variant; attrs: TJSPropertyAttrs); begin DefineProperty(name,TSMValue(VariantToJsVal(cx,value)),attrs); end; procedure TSMObject.DefineProperty(const name: SynUnicode; const value: variant); begin DefineProperty(name, value, FDefaultPropertyAttrs); end; procedure TSMObject.SetPropVariant(const propName: SynUnicode; const Value: variant); begin DefineProperty(propName,Value); end; function TSMObject.HasProperty(const propName: SynUnicode): Boolean; var has: JSBool; begin Result := (JS_HasUCProperty(cx, obj, pointer(propName), length(propName), has)=JS_TRUE) and (has=JS_TRUE); end; function TSMObject.HasOwnProperty(const propName: SynUnicode): Boolean; var has: JSBool; begin Result := (JS_AlreadyHasOwnUCProperty(cx, obj, pointer(propName), length(propName), has)=JS_TRUE) and (has=JS_TRUE); end; function TSMObject.GetPropValue(const propName: SynUnicode): TSMValue; begin if JS_GetUCProperty(cx, obj, pointer(propName), length(propName), Result.FValue)=JS_FALSE then raise ESMException.CreateUTF8('TSMObject.GetPropValue(%)',[propName]); end; function TSMObject.GetPropVariant(const propName: SynUnicode): variant; var res: TSMValue; // need a temp. var to compile with latest Delphi! :( begin res := GetPropValue(propName); res.ToVariant(cx,result); end; procedure TSMObject.Evaluate(const script: SynUnicode; const scriptName: RawUTF8; lineNo: Cardinal; out result: TSMValue); var r: JSBool; eng: TSMEngine; begin {$ifdef RESETFPUEXCEPTION} TSynFPUException.ForLibraryCode; {$endif} eng := Engine; eng.ClearLastError; eng.ScheduleWatchdog(eng.fTimeoutInterval); r := JS_EvaluateUCScript(cx, obj, pointer(script), length(script), pointer(scriptName), lineNo, Result.FValue); eng.ScheduleWatchdog(-1); eng.CheckJSError(r); end; procedure TSMObject.RunMethod(const methodName: AnsiString; const argv: SMValArray; out rval: TSMValue); var r: JSBool; eng: TSMEngine; begin {$ifdef RESETFPUEXCEPTION} TSynFPUException.ForLibraryCode; {$endif} eng := Engine; eng.ClearLastError; eng.ScheduleWatchdog(Engine.fTimeoutInterval); r := JS_CallFunctionName(cx, obj, pointer(methodName), Length(argv), pointer(argv), rval.FValue); eng.ScheduleWatchdog(-1); eng.CheckJSError(r); end; procedure TSMObject.RunMethod(const methodName: AnsiString; const argv: array of const; out rval: TSMValue); var args: SMValArray; a: integer; begin SetLength(args,length(argv)); for a := 0 to high(argv) do args[a].SetTVarRec(cx,argv[a]); RunMethod(methodName,args,rval); end; function TSMObject.Run(const methodName: AnsiString; const argv: array of variant): variant; var args: SMValArray; a: integer; res: TSMValue; begin SetLength(args,length(argv)); for a := 0 to high(argv) do args[a].SetVariant(cx,argv[a]); RunMethod(methodName,args,res); res.ToVariant(cx,result); end; procedure TSMObject.Root; begin if obj<>nil then JS_AddObjectRoot(cx, @obj); end; procedure TSMObject.UnRoot; begin if obj<>nil then JS_RemoveObjectRoot(cx, @obj); end; function TSMObject.DefineNativeMethod(const methodName: SynUnicode; func: JSNative; nargs: uintN; attrs: TJSPropertyAttrs): PJSFunction; begin Result := JS_DefineUCFunction(cx, obj, Pjschar(methodName), Length(methodName), func, nargs, word(attrs)); if Result=nil then raise ESMException.CreateUTF8('TSMObject.DefineNativeMethod(%)',[methodName]); end; procedure TSMObject.Clear; begin fCx := nil; fObj := nil; end; function TSMObject.DefineNativeMethod(const methodName: AnsiString; func: JSNative; nargs: uintN; attrs: TJSPropertyAttrs): PJSFunction; begin Result := JS_DefineFunction(cx, obj, PCChar(methodName), func, nargs, word(attrs)); if Result=nil then raise ESMException.CreateUTF8('TSMObject.DefineNativeMethod(%)',[methodName]); end; function TSMObject.DefineNativeMethod(const methodName: AnsiString; func: JSNative; nargs: uintN): PJSFunction; begin result := DefineNativeMethod(methodName, func, nargs, DefaultPropertyAttrs); end; function TSMObject.DefineNativeMethod(const methodName: SynUnicode; func: JSNative; nargs: uintN): PJSFunction; begin result := DefineNativeMethod(methodName, func, nargs, DefaultPropertyAttrs); end; procedure TSMObject.DefineProperty(const name: SynUnicode; const value: TSMValue); begin DefineProperty(name, value, FDefaultPropertyAttrs); end; function TSMObject.Parent: TSMObject; begin result.fCx := cx; if obj=nil then result.fObj := nil else result.fObj := JS_GetParent(obj); end; function TSMObject.Prototype: TSMObject; begin result.fCx := cx; if obj=nil then result.fObj := nil else JS_GetPrototype(cx, obj, result.fObj); end; function TSMEngine.DoProcessOperationCallback: JSBool; begin if fTimedOut then Result := JS_FALSE else Result := JS_TRUE; end; procedure TSMEngine.CancelExecution; begin fTimedOut := True; FTimeOutAborted := True; FErrorExist := True; FLastErrorFileName := '(w/o name)'; FLastErrorLine := 0; FLastErrorMsg := FormatUTF8('JSError. Filename: %. Line %. Message: %', [FLastErrorFileName, FLastErrorLine, 'Script runs for too long, terminating']); JS_TriggerOperationCallback(rt); end; function TSMEngine.InitWatchdog: boolean; begin Assert(not Assigned(fWatchdogThread)); fWatchdogLock := PR_NewLock; if Assigned(fWatchdogLock) then begin fWatchdogWakeup := PR_NewCondVar(fWatchdogLock); if Assigned(fWatchdogWakeup) then begin fSleepWakeup := PR_NewCondVar(fWatchdogLock); if Assigned(fSleepWakeup) then begin result := True; exit; end; PR_DestroyCondVar(fWatchdogWakeup); end; end; result := False; end; procedure TSMEngine.KillWatchdog; var thread: PRThread; begin PR_Lock(fWatchdogLock); thread := fWatchdogThread; if Assigned(thread) then begin // The watchdog thread is running, tell it to terminate waking it up // if necessary. fWatchdogThread := nil; PR_NotifyCondVar(fWatchdogWakeup); end; PR_Unlock(fWatchdogLock); if Assigned(thread) then PR_JoinThread(thread); PR_DestroyCondVar(fSleepWakeup); PR_DestroyCondVar(fWatchdogWakeup); PR_DestroyLock(fWatchdogLock); end; function IsBefore( t1, t2: int64): Boolean; begin Result := int32(t1 - t2) < 0; end; procedure WatchdogMain(arg: pointer); cdecl; var eng: TSMEngine; rt: PJSRuntime; now_: int64; sleepDuration: PRIntervalTime; status: PRStatus; begin PR_SetCurrentThreadName('JS Watchdog'); eng := TSMEngine(arg); rt := eng.rt; PR_Lock(eng.fWatchdogLock); while Assigned(eng.fWatchdogThread) do begin now_ := JS_Now(); if (eng.fWatchdogHasTimeout and not IsBefore(now_, eng.fWatchdogTimeout)) then begin // The timeout has just expired. Trigger the operation callback outside the lock eng.fWatchdogHasTimeout := false; PR_Unlock(eng.fWatchdogLock); eng.CancelExecution; PR_Lock(eng.fWatchdogLock); // Wake up any threads doing sleep PR_NotifyAllCondVar(eng.fSleepWakeup); end else begin if (eng.fWatchdogHasTimeout) then begin // Time hasn't expired yet. Simulate an operation callback // which doesn't abort execution. JS_TriggerOperationCallback(rt); end; sleepDuration := PR_INTERVAL_NO_TIMEOUT; if (eng.fWatchdogHasTimeout) then sleepDuration := PR_TicksPerSecond() div 10; status := PR_WaitCondVar(eng.fWatchdogWakeup, sleepDuration); Assert(status = PR_SUCCESS); end end; PR_Unlock(eng.fWatchdogLock); end; function TSMEngine.ScheduleWatchdog(t: Double): Boolean; var interval: Int64; timeout: Int64; begin if (t <= 0) then begin PR_Lock(fWatchdogLock); fWatchdogHasTimeout := false; PR_Unlock(fWatchdogLock); result := true; exit; end; interval := int64(ceil(t * PRMJ_USEC_PER_SEC)); timeout := JS_Now() + interval; PR_Lock(fWatchdogLock); if not Assigned(fWatchdogThread) then begin Assert(not fWatchdogHasTimeout); fWatchdogThread := PR_CreateThread(PR_USER_THREAD, @WatchdogMain, Self, PR_PRIORITY_NORMAL, PR_LOCAL_THREAD, PR_JOINABLE_THREAD, 0); if not Assigned(fWatchdogThread) then begin PR_Unlock(fWatchdogLock); Result := false; Exit; end end else if (not fWatchdogHasTimeout or IsBefore(timeout, fWatchdogTimeout)) then begin PR_NotifyCondVar(fWatchdogWakeup); end; fWatchdogHasTimeout := true; fWatchdogTimeout := timeout; PR_Unlock(fWatchdogLock); Result := true; end; procedure TSMEngine.SetDefaultPropertyAttrs(const Value: TJSPropertyAttrs); begin FDefaultPropertyAttrs := Value; end; procedure TSMEngine.SetTimeoutValue(const Value: Double); begin fTimeoutInterval := Value; ScheduleWatchdog(Value); end; function TSMObject.GetPrivate: pointer; {$ifdef WITHASSERT} var C: PJSClass; {$endif} begin if obj=nil then result := nil else {$ifdef WITHASSERT} // JS_GetPrivate can return some not-nil pointer when we call JS_GetPrivate for object // with class which has no flag JSCLASS_HAS_PRIVATE begin C := JS_GetClass(obj); if C.flags and JSCLASS_HAS_PRIVATE = 0 then result := nil // May be need to raise exception else {$endif} result := JS_GetPrivate(obj); {$ifdef WITHASSERT} end; {$endif} end; procedure TSMObject.SetPrivate(const Value: pointer); {$ifdef WITHASSERT} var C: PJSClass; {$endif} begin if obj<>nil then {$ifdef WITHASSERT} begin // If we set private data into object with class which has no flag JSCLASS_HAS_PRIVATE // SM don't raise exception, but we can get AV in any other place C := JS_GetClass(obj); if C.flags and JSCLASS_HAS_PRIVATE = 0 then exit // May be need to raise exception else {$endif} JS_SetPrivate(obj, Value); {$ifdef WITHASSERT} end; {$endif} end; function TSMObject.GetPrivateData(expectedClass: PJSClass): pointer; begin if obj=nil then result := nil else result := JS_GetInstancePrivate(cx, obj, expectedClass, nil); end; function TSMObject.ItemsCount: cardinal; begin JS_GetArrayLength(cx,obj,result) end; function TSMObject.IsArray: boolean; begin result := JS_IsArrayObject(cx,obj)=JS_TRUE; end; function TSMObject.GetItem(aIndex: integer): variant; var res: TSMValue; begin if JS_GetElement(cx,obj,aIndex,res.FValue)=JS_FALSE then raise ESMException.CreateUTF8('get TSMObject.Items[%]',[aIndex]) else res.ToVariant(cx,result); end; procedure TSMObject.SetDefaultPropertyAttrs(const Value: TJSPropertyAttrs); begin FDefaultPropertyAttrs := Value; end; procedure TSMObject.SetItem(aIndex: integer; const Value: variant); var val: TSMValue; begin val.SetVariant(cx,Value); if JS_SetElement(cx,obj,aIndex,val.FValue)=JS_FALSE then raise ESMException.CreateUTF8('set TSMObject.Items[%]',[aIndex]); end; procedure TSMObject.DeleteItem(aIndex: integer); begin if JS_DeleteElement(cx,obj,aIndex)=JS_FALSE then raise ESMException.CreateUTF8('TSMObject.DeleteItem(%)',[aIndex]); end; { TSMVariant } function TSMVariant.IntGet(var Dest: TVarData; const Instance: TVarData; Name: PAnsiChar; NameLen: PtrInt): boolean; var res: TSMValue; begin //Assert(Instance.VType=SMVariantType.VarType); with TSMVariantData(Instance) do if JS_GetProperty(cx,obj,Name,res.FValue)=JS_FALSE then raise ESMException.CreateUTF8('Unexpected %.%',[self,Name]) else res.ToVariant(cx,variant(Dest)); result := true; end; function TSMVariant.DoFunction(var Dest: TVarData; const V: TVarData; const Name: string; const Arguments: TVarDataArray): Boolean; var args: SMValArray; a: integer; res: TSMValue; {$ifdef UNICODE} nam: array[byte] of AnsiChar; {$endif} begin //Assert(V.VType=SMVariantType.VarType); result := true; with TSMVariantData(V).VObject do begin if (Arguments=nil) and (Name[1]='_') then begin {$ifdef UNICODE} UpperCopy255W(nam,Name)^ := #0; case IdemPCharArray(@nam[1], {$else} case IdemPCharArray(@Name[2], {$endif} ['ROOT','UNROOT']) of 0: begin Root; exit; end; 1: begin UnRoot; exit; end; end; end; SetLength(args,length(Arguments)); for a := 0 to high(args) do args[a].SetVariant(cx,Variant(Arguments[a])); RunMethod(AnsiString(Name),args,res); res.ToVariant(cx,variant(Dest)); end; end; function TSMVariant.IntSet(const Instance, Value: TVarData; Name: PAnsiChar; NameLen: PtrInt): boolean; var smValue: TSMValue; begin //Assert(Instance.VType=SMVariantType.VarType); with TSMVariantData(Instance) do begin smValue.SetVariant(cx,Variant(Value)); result := JS_SetProperty(cx,obj,Name,smValue.FValue)<>JS_FALSE; end; if not result then raise ESMException.CreateUTF8('Error setting %.%',[self,Name]); end; procedure TSMVariant.ToJSON(W: TTextWriter; const Value: variant; Escape: TTextWriterKind); var val: jsval; begin with TSMVariantData(Value) do if VType=VarType then begin val := OBJECT_TO_JSVAL(obj); if JS_Stringify(cx, @val, nil, JSVAL_NULL, writeCallback, pointer(W))<>JS_TRUE then begin TSMEngine(cx.PrivateData).CheckJSError(JS_FALSE); TSMEngine(cx.PrivateData).ClearLastError; end; end else raise ESMException.CreateUTF8( '%.ToJSON: Unexpected variant type %',[self,VType]); end; class procedure TSMVariant.New(const aObject: TSMObject; out aValue: variant); begin VarClear(aValue); TSMVariantData(aValue).Init(aObject); end; class procedure TSMVariant.New(cx: PJSContext; obj: PJSObject; out aValue: variant); begin VarClear(aValue); TSMVariantData(aValue).Init(cx,obj); end; class procedure TSMVariant.New(engine: TSMEngine; out aValue: variant); begin VarClear(aValue); TSMVariantData(aValue).InitNew(engine); end; procedure TSMVariant.Cast(var Dest: TVarData; const Source: TVarData); begin CastTo(Dest,Source,VarType); end; procedure TSMVariant.CastTo(var Dest: TVarData; const Source: TVarData; const AVarType: TVarType); var tmp: RawUTF8; begin if Source.VType<>VarType then RaiseCastError; tmp := VariantToUTF8(variant(Source)); if tmp='' then Variant(Dest) := '<>' else RawUTF8ToVariant(tmp,Variant(Dest)); end; { TSMVariantData } procedure TSMVariantData.GetGlobal(out global: variant); begin global := VObject.Engine.Global; end; procedure TSMVariantData.Init(const aObject: TSMObject); begin Init(aObject.cx,aObject.obj); end; procedure TSMVariantData.Init(aCx: PJSContext; aObj: PJSObject); begin if SMVariantType=nil then SMVariantType := SynRegisterCustomVariantType(TSMVariant); ZeroFill(@self); VType := SMVariantType.VarType; VObject.fCx := aCx; VObject.fObj := aObj; end; procedure TSMVariantData.InitNew(engine: TSMEngine); var aObj: TSMObject; begin engine.NewObject(aObj); Init(aObj.cx,aObj.obj); end; initialization Assert(sizeof(TSMVariantData)=sizeof(variant)); end.