/// SyNodeSimpleProto - create a JS prototypes with Delphi method/props realisation // - this unit is a part of the freeware Synopse framework, // licensed under a MPL/GPL/LGPL tri-license; version 1.18 unit SyNodeSimpleProto; { This file is part of Synopse framework. Synopse framework. Copyright (C) 2020 Arnaud Bouchez Synopse Informatique - http://synopse.info SyNode for mORMot Copyright (C) 2020 Pavel Mashlyakovsky & Vadim Orel 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) 2014 the Initial Developer. All Rights Reserved. Contributor(s): - Arnaud Bouchez - Vadim Orel - Pavel Mashlyakovsky - 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 ***** Version 1.18 - initial release. Use SpiderMonkey 45 } {$I Synopse.inc} interface uses SpiderMonkey, SyNodeProto, mORMot { PClassProp }; type /// A prototype class for wrapping a Delphi class based on a "old" RTTI // - create a properties in JavaScript based on the published properties of original class // - all published methods of a original calss MUST have a TSMFastNativeCall signature TSMSimpleRTTIProtoObject = class(TSMCustomProtoObject) protected procedure InitObject(aParent: PJSRootedObject); override; /// Can be used to optimize JS engine proerty access. // - if isReadonly setted to true property become read-only for JS engine // - if property valu don't changed during object lifecircle set isDeterministic=true // to prevent creating of JS value every time JS engine read property value // If method return false propery will not be created in the JS function GetPropertyAddInformation(cx: PJSContext; PI:PPropInfo; out isReadonly: boolean; out isDeterministic: boolean; aParent: PJSRootedObject): boolean; virtual; function GetJSvalFromProp(cx: PJSContext; PI:PPropInfo; instance: PSMInstanceRecord): jsval; virtual; public end; function CreateJSInstanceObjForSimpleRTTI(cx: PJSContext; AInstance: TObject; aParent: PJSRootedObject=nil): jsval; implementation uses {$ifdef ISDELPHIXE2}System.SysUtils,{$else}SysUtils,{$endif} SyNode, SynCommons; function JSRTTINativeMethodCall(cx: PJSContext; argc: uintN; var vp: JSArgRec): Boolean; cdecl; var fCallFn: TSMFastNativeCall; fCallMethod: TMethod; lfunc: PJSFunction; jsobj: PJSObject; Inst: PSMInstanceRecord; mc: PSMMethodRec; proto: PJSObject; begin try // JS_ConvertValue(cx,JS_CALLEE(cx, vp),JSTYPE_FUNCTION, lfuncVal); lfunc := vp.calleObject; Assert(Assigned(lfunc)); jsobj := vp.thisObject[cx]; if not IsInstanceObject(cx, jsobj, Inst) then raise ESMException.Create(SM_NOT_A_NATIVE_OBJECT); jsobj.GetPrototype(cx, proto); mc := TSMSimpleRTTIProtoObject(Inst^.proto).getMethod(lfunc, proto); if mc = nil then raise ESMException.CreateUTF8('The class has no method "%"', [lfunc.GetFunctionId().ToSynUnicode(cx)]); fCallMethod.Code := mc^.method; fCallMethod.Data := Pointer(Inst.instance); fCallFn := TSMFastNativeCall(fCallMethod); Result := fCallFn(cx, argc, vp); except on E: Exception do begin Result := False; vp.rval := JSVAL_VOID; JSError(cx, E); end; end; end; function GetPropCacheForWrite(cx: PJSContext; obj: PJSObject; id: jsid; var aObj: PSMInstanceRecord): PSMRTTIPropCache; var i: Integer; propName: AnsiString; found: boolean; begin Result := nil; if not IsInstanceObject(cx, obj, aObj) then raise ESMException.Create(SM_NOT_A_NATIVE_OBJECT); propName := PJSString(id).ToAnsi(cx); found := False; for I := 0 to Length((AObj.proto as TSMSimpleRTTIProtoObject).FRTTIPropsCache)-1 do begin Result := @(AObj.proto as TSMSimpleRTTIProtoObject).FRTTIPropsCache[i]; {$IFDEF SM52} if strComparePropGetterSetter(propName, Result.jsName, false) then begin {$ELSE} if Result.jsName = propName then begin {$ENDIF} found := True; Break; end; end; if not found then raise ESMException.CreateFmt('% not found', [propName]); if Result.isReadOnly then raise ESMException.CreateUtf8('Property %.% is ReadOnly', [aObj.proto.jsObjName, Result.jsName]); end; function JSRTTIPropWrite(cx: PJSContext; argc: uintN; var vp: JSArgRec): Boolean; cdecl; var Instance: PSMInstanceRecord; PI: PPropInfo; id: jsid; val: jsval; begin id := jsid(vp.calleObject.FunctionId); PI := GetPropCacheForWrite(cx, vp.thisObject[cx], id, Instance).mbr; val := vp.argv[0]; case PI.PropType^{$IFNDEF FPC}^{$ENDIF}.Kind of tkInteger, tkEnumeration, tkSet{$ifdef FPC},tkBool{$endif}: PI.SetOrdProp(Instance^.instance,val.asInteger); tkInt64: PI.SetInt64Prop(Instance^.instance, val.asInt64); tkFloat: PI.SetFloatProp(Instance^.instance, val.asDouble); tkLString,{$IFDEF FPC}tkLStringOld{$ENDIF},tkWString{$ifdef HASVARUSTRING},tkUString{$endif}: PI.SetLongStrValue(Instance^.instance, val.asJsString.ToUTF8(cx)); else raise ESMException.Create('NotImplemented'); end; Result := True; end; function JSRTTIPropRead(cx: PJSContext; argc: uintN; var vp: JSArgRec): Boolean; cdecl; var Instance: PSMInstanceRecord; proto: TSMCustomProtoObject; propCache: PSMRTTIPropCache; PI: PPropInfo; rval: jsval; this: PJSObject; storedVal: jsval; i: Integer; id: PJSString; prop_name: AnsiString; found: Boolean; begin try this := vp.thisObject[cx]; if IsProtoObject(cx, this, proto) then begin vp.rval := JSVAL_NULL; Result := True; exit; end; if not IsInstanceObject(cx, this, Instance) then raise ESMException.Create(SM_NOT_A_NATIVE_OBJECT); id := vp.calleObject.FunctionId; prop_name := ID.ToAnsi(cx); propCache := nil; found := false; for i := 0 to Length((Instance.proto as TSMSimpleRTTIProtoObject).FRTTIPropsCache)-1 do begin propCache := @(Instance.proto as TSMSimpleRTTIProtoObject).FRTTIPropsCache[i]; {$IFDEF SM52} if strComparePropGetterSetter(prop_name, propCache.jsName, true) then begin {$ELSE} if propCache.jsName = prop_name then begin {$ENDIF} found := True; Break; end; end; if not found then raise ESMException.CreateFmt('% not found', [prop_name]); if (propCache.DeterministicIndex>=0) then storedVal := this.ReservedSlot[propCache.DeterministicIndex] else storedVal.setVoid; if (not storedVal.isVoid) then rval := storedVal else begin PI := propCache.mbr; rval := (Instance.proto as TSMSimpleRTTIProtoObject).GetJSvalFromProp(cx, PI, Instance); if (propCache.DeterministicIndex>=0) then begin // all jsvals in reserved slots are rooted automatically this.ReservedSlot[propCache.DeterministicIndex] := rval; end; end; vp.rval := rval; Result := True; except on E: Exception do begin Result := False; JSError(cx, E); end; end; end; { TSMSimpleRTTIProtoObject } function TSMSimpleRTTIProtoObject.GetJSvalFromProp(cx: PJSContext; PI: PPropInfo; instance: PSMInstanceRecord): jsval; var FInst: PSMInstanceRecord; tmp: RawUTF8; obj: TObject; arr: TDynArray; begin case PI.PropType^.Kind of tkInteger, tkEnumeration, tkSet{$ifdef FPC},tkBool{$endif}: Result.asInteger := PI.GetOrdValue(Instance^.instance); tkInt64: Result.asInt64 := PI.GetInt64Value(Instance^.instance); tkFloat: Result.asDouble := PI.GetDoubleValue(Instance^.instance); tkLString,{$ifdef FPC}tkLStringOld,{$endif}tkWString{$ifdef HASVARUSTRING},tkUString{$endif}: begin PI.GetLongStrValue(Instance^.instance, tmp); Result.asJSString := cx.NewJSString(tmp); end; tkClass: begin new(FInst); obj := PI.GetObjProp(Instance^.instance); if obj <> nil then Result := FInst.CreateForObj(cx, obj, TSMSimpleRTTIProtoObject, Instance.Proto) else Result := JSVAL_NULL; end; tkDynArray: begin // MPV. WARNING. Every access to dyn array property will create a JS Array, so // I recommend avoiding use of the dynamic arrays, or use a temp variable arr := PI.GetDynArray(Instance^.instance); Result.asJson[cx] := arr.SaveToJSON(true); end; else raise ESMException.Create('NotImplemented'); end; end; procedure TSMSimpleRTTIProtoObject.InitObject(aParent: PJSRootedObject); var PI: PPropInfo; i: integer; idx: Integer; CT: TClass; n: integer; added: boolean; isReadonly: boolean; isDeterministic: boolean; exclude: boolean; methods: TPublishedMethodInfoDynArray; begin for i := 0 to GetPublishedMethods(nil, methods, fRttiCls) - 1 do begin idx := FMethodsDA.FindHashedForAdding(methods[i].Name, added); if added then with FMethods[idx] do begin ujsName := UTF8ToSynUnicode(methods[i].Name); method := methods[i].Method.Code; nargs := 0; isNativeCall := true; call := @JSRTTINativeMethodCall; flags := [jspEnumerate]; end; end; fDeterministicCnt := 0; CT := fRttiCls; repeat for i := 1 to InternalClassPropInfo(CT,PI) do begin idx := Length(FJSProps); exclude := PI^.PropType^.Kind = tkMethod; if not exclude then for n := 0 to idx - 1 do begin if StrLIComp(PAnsiChar(@PI.Name[1]), PAnsiChar(FRTTIPropsCache[n].jsName), length(PI.Name)) = 0 then begin exclude := true; break; end; end; if exclude or not GetPropertyAddInformation(fCx, PI, isReadonly, isDeterministic, aParent) then begin PI := PI^.Next; Continue; end; case PI^.PropType^{$IFNDEF FPC}^{$ENDIF}.Kind of tkChar, {$IFDEF FPC}tkLString{$ELSE}tkString{$ENDIF}, tkWChar, tkWString, tkVariant: begin raise ESMException.CreateUtf8('Unsupported class property %.%', [FjsObjName, PI^.Name]); end; // lazy create class type property prototypes on first read (in TSMSimpleRTTIProtoObject.GetJSvalFromProp) // tkClass: // defineClass(Cx, PI^.PropType^{$IFNDEF FPC}^{$ENDIF}.ClassType^.ClassType, TSMSimpleRTTIProtoObject, aParent); tkEnumeration: defineEnum(fCx, PI^.PropType{$IFNDEF FPC_OLDRTTI}^{$ENDIF}, aParent); end; SetLength(FJSProps, idx + 1); SetLength(FRTTIPropsCache, idx + 1); FRTTIPropsCache[idx].jsName := camelize(PI.Name); FRTTIPropsCache[idx].mbr := PI; FRTTIPropsCache[idx].typeInfo := PI^.PropType{$IFNDEF FPC}^{$ENDIF}; FRTTIPropsCache[idx].isReadOnly := isReadonly or isDeterministic; if isDeterministic then begin FRTTIPropsCache[idx].DeterministicIndex := fDeterministicCnt; Inc(fDeterministicCnt); end else FRTTIPropsCache[idx].DeterministicIndex := -1; FJSProps[idx].flags := JSPROP_ENUMERATE or JSPROP_PERMANENT or JSPROP_SHARED; FJSProps[idx].Name := PCChar(RTTIPropsCache[idx].jsName); // FJSProps[idx].tinyid := idx; FJSProps[idx].setter.native.info := nil; FJSProps[idx].setter.native.op := JSRTTIPropWrite; FJSProps[idx].getter.native.info := nil; FJSProps[idx].getter.native.op := JSRTTIPropRead; PI := PI^.Next; end; CT := CT.ClassParent; until CT=nil; inherited; //MPV !! do not use FMethodsDA.Add() end; function TSMSimpleRTTIProtoObject.GetPropertyAddInformation(cx: PJSContext; PI: PPropInfo; out isReadonly: boolean; out isDeterministic: boolean; aParent: PJSRootedObject): boolean; begin isReadonly := false; isDeterministic := false; result := true; end; function CreateJSInstanceObjForSimpleRTTI(cx: PJSContext; AInstance: TObject; aParent: PJSRootedObject=nil): jsval; var Inst: PSMInstanceRecord; eng: TSMEngine; begin new(Inst); if (aParent = nil) then begin eng := cx.PrivateData; Result := Inst.CreateForObj(cx, AInstance, TSMSimpleRTTIProtoObject, eng.GlobalObject); end else Result := Inst.CreateForObj(cx, AInstance, TSMSimpleRTTIProtoObject, aParent); end; end.