fprpcclient.pp 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  1. {
  2. This file is part of the Free Component Library
  3. Client-side JSON-RPC functionality using Invoke.
  4. Copyright (c) 2022 by Michael Van Canneyt [email protected]
  5. See the file COPYING.FPC, included in this distribution,
  6. for details about the copyright.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  10. **********************************************************************}
  11. {$IFNDEF FPC_DOTTEDUNITS}
  12. unit fprpcclient;
  13. {$ENDIF FPC_DOTTEDUNITS}
  14. {$mode ObjFPC}{$H+}
  15. interface
  16. {$IFDEF FPC_DOTTEDUNITS}
  17. uses
  18. System.TypInfo, System.Classes, System.SysUtils, FpJson.Data, FpWeb.Client, FpWeb.Client.Http, System.Rtti, FpJson.Value;
  19. {$ELSE FPC_DOTTEDUNITS}
  20. uses
  21. TypInfo, Classes, SysUtils, fpjson, fpwebclient, fphttpwebclient, rtti, fpjsonvalue;
  22. {$ENDIF FPC_DOTTEDUNITS}
  23. Type
  24. ERPCClient = Class(Exception);
  25. TRttiParameterArray = array of TRttiParameter;
  26. TFPRPCClient = Class;
  27. { TFPRPCVirtualInterface }
  28. TFPRPCVirtualInterface = Class(TVirtualInterface)
  29. private
  30. FClient: TFPRPCClient;
  31. FTypeInfo: PTypeInfo;
  32. FClassName : String;
  33. Protected
  34. procedure HandleInvoke(aMethod: TRttiMethod; const aArgs: TValueArray; out aResult: TValue);
  35. Public
  36. Constructor Create(aTypeInfo : PTypeInfo; const aClassName : String; aClient : TFPRPCClient);
  37. Property Client : TFPRPCClient Read FClient;
  38. Property IntfTypeInfo : PTypeInfo Read FTypeInfo;
  39. end;
  40. { TFPRPCClient }
  41. TRPCClientOption = (rcoObjectParam,rcoNotifications);
  42. TRPCClientOptions = set of TRPCClientOption;
  43. TFPRPCClient = Class(TComponent)
  44. Private
  45. FBaseURL: String;
  46. FClient : TAbstractWebClient;
  47. FInternalClient : TAbstractWebClient;
  48. FOptions: TRPCClientOptions;
  49. FRequestID : Int64;
  50. function GetClient : TAbstractWebClient;
  51. Protected
  52. // Override so we can query for all registered types
  53. function QueryInterface(constref aIID: TGuid; out aObj): LongInt;{$IFNDEF WINDOWS}cdecl{$ELSE}stdcall{$ENDIF}; override;
  54. // Create virtual interface. Override this if you want to return something other than TFPRPCVirtualInterface
  55. function CreateVirtualInterface(IntfType: TRttiInterfaceType; const aName: string): IInterface; virtual;
  56. // Encode parameters to method call.
  57. function EncodeParams(aMethod: TRttiMethod; const aArgs: TValueArray; out VarParamCount: Integer): TJSONData;
  58. // Decode JSON-RPC result to method call result and var/out params.
  59. function DecodeResult(Response: TJSONObject; aMethod: TRttiMethod; const aArgs: TValueArray; HaveReturnValues: Boolean): TValue;
  60. // Find registered interfacen return instance in aObj. Return true if successful.
  61. function DoCreateProxy(constref aIID: TGuid; out aObj): Boolean;
  62. function DoCreateProxy(const aName: String; out aObj): Boolean;
  63. // Called from TFPRPCVirtualInterface to actuall handle call.
  64. procedure HandleInvoke(const aClassName : String; aMethod: TRttiMethod; const aArgs: TValueArray; out aResult: TValue); virtual;
  65. // Do actual HTTP request.
  66. function DoRequest(aRequest : TJSONObject) : TJSONObject; virtual;
  67. // Create JSON-RPC request object.
  68. function CreateRPCRequest(const aClassName,aMethodName : String; IsNotification : Boolean): TJSONObject; virtual;
  69. // Client to do request with. If WebClient is set, that is used. Otherwise fallback using TFPHTTPClient is used.
  70. property Client : TAbstractWebClient Read GetClient;
  71. Public
  72. // Create a service by name. Use QueryInterface on the result to get your actual interface
  73. Function CreateService(const aName : string) : IInterface;
  74. // Create a service by name, directly return the interface.
  75. generic Function CreateService<T : IInterface>(const aName : string) : T;
  76. // Set this to use another webclient other than the default one.
  77. Property WebClient : TAbstractWebClient Read FClient Write FClient;
  78. // base URL for JSON-RPC requests
  79. property BaseURL : String Read FBaseURL Write FBaseURL;
  80. // Options.
  81. Property Options : TRPCClientOptions Read FOptions Write FOptions;
  82. end;
  83. { TFPRPCServiceRegistry }
  84. TFPRPCServiceRegistry = class
  85. Class var
  86. _instance : TFPRPCServiceRegistry;
  87. Private
  88. Type
  89. { TIntfEntry }
  90. TIntfEntry = record
  91. Name: String;
  92. IntfType : TRttiInterfaceType;
  93. end;
  94. Var
  95. FContext : TRTTIContext;
  96. fIntfs : Array of TIntfEntry;
  97. fIntfCount : Integer;
  98. Protected
  99. Public
  100. class var
  101. SizeDelta : Integer;
  102. Public
  103. class constructor Init;
  104. class destructor done;
  105. constructor create; virtual;
  106. procedure Add(aInterfaceInfo : PTypeInfo; const aName : string = '');
  107. generic procedure Add <T : IInterface>(const aName : string = '');
  108. function Find(const aName: string; out IntfType: TRttiInterfaceType): Boolean;
  109. function Find(const aGUID: TGUID; out IntfType: TRttiInterfaceType; out aName : String): Boolean;
  110. function Get(const aName: string) : TRttiInterfaceType;
  111. function Get(const aGUID: TGUID; out aName : String) : TRttiInterfaceType;
  112. class property Instance : TFPRPCServiceRegistry Read _Instance;
  113. end;
  114. Function RPCServiceRegistry : TFPRPCServiceRegistry;
  115. implementation
  116. {$IFDEF FPC_DOTTEDUNITS}
  117. uses FpWeb.JsonRpc.Strings;
  118. {$ELSE FPC_DOTTEDUNITS}
  119. uses fprpcstrings;
  120. {$ENDIF FPC_DOTTEDUNITS}
  121. function IsGUIDEqual(const guid1, guid2: tguid): boolean;
  122. begin
  123. IsGUIDEqual:=
  124. (guid1.D1=guid2.D1) and
  125. (PDWORD(@guid1.D2)^=PDWORD(@guid2.D2)^) and
  126. (PDWORD(@guid1.D4[0])^=PDWORD(@guid2.D4[0])^) and
  127. (PDWORD(@guid1.D4[4])^=PDWORD(@guid2.D4[4])^);
  128. end;
  129. Function RPCServiceRegistry : TFPRPCServiceRegistry;
  130. begin
  131. Result:=TFPRPCServiceRegistry.Instance;
  132. end;
  133. { TFPRPCVirtualInterface }
  134. procedure TFPRPCVirtualInterface.HandleInvoke(aMethod: TRttiMethod; const aArgs: TValueArray; out aResult: TValue);
  135. begin
  136. FClient.HandleInvoke(FClassName,aMethod,aArgs,aResult);
  137. end;
  138. constructor TFPRPCVirtualInterface.Create(aTypeInfo: PTypeInfo; const aClassName: String; aClient: TFPRPCClient);
  139. begin
  140. inherited Create(aTypeInfo, @HandleInvoke);
  141. FTypeInfo:=aTypeInfo;
  142. FClient:=aClient;
  143. FClassName:=aClassName;
  144. end;
  145. { TFPRPCServiceRegistry }
  146. class constructor TFPRPCServiceRegistry.Init;
  147. begin
  148. SizeDelta:=32;
  149. _Instance:=TFPRPCServiceRegistry.Create;
  150. end;
  151. class destructor TFPRPCServiceRegistry.done;
  152. begin
  153. FreeAndNil(_Instance);
  154. end;
  155. constructor TFPRPCServiceRegistry.create;
  156. begin
  157. SetLength(fIntfs,SizeDelta);
  158. fIntfCount:=0;
  159. end;
  160. procedure TFPRPCServiceRegistry.Add(aInterfaceInfo: PTypeInfo; const aName: string);
  161. var
  162. entry: TIntfEntry;
  163. begin
  164. if aName='' then
  165. entry.Name:=aInterfaceInfo^.Name
  166. else
  167. entry.Name:=aName;
  168. entry.IntfType := fContext.GetType(aInterfaceInfo) as TRttiInterfaceType;
  169. if fIntfCount>=Length(fIntfs) then
  170. SetLength(fIntfs,Length(fIntfs)+SizeDelta);
  171. fIntfs[fIntfCount]:=entry;
  172. Inc(fIntfCount);
  173. end;
  174. function TFPRPCServiceRegistry.Find(Const aName: string; out IntfType: TRttiInterfaceType): Boolean;
  175. Var
  176. Idx : integer;
  177. Entry : TIntfEntry;
  178. begin
  179. Result:=False;
  180. Idx:=fIntfCount-1;
  181. While (Idx>=0) and not Result do
  182. begin
  183. Result:=SameText(fIntfs[Idx].Name,aName);
  184. if Result then
  185. begin
  186. Entry:=fIntfs[Idx];
  187. IntfType:=Entry.IntfType;
  188. end;
  189. Dec(Idx);
  190. end;
  191. end;
  192. function TFPRPCServiceRegistry.Find(const aGUID: TGUID; out IntfType: TRttiInterfaceType; out aName: String): Boolean;
  193. Var
  194. Idx : integer;
  195. Entry : TIntfEntry;
  196. begin
  197. Result:=False;
  198. Idx:=fIntfCount-1;
  199. While (Idx>=0) and not Result do
  200. begin
  201. Result:=IsGUIDEqual(fIntfs[Idx].IntfType.GUID,aGUID);
  202. if Result then
  203. begin
  204. Entry:=fIntfs[Idx];
  205. IntfType:=Entry.IntfType;
  206. aName:=Entry.Name;
  207. end;
  208. Dec(Idx);
  209. end;
  210. end;
  211. function TFPRPCServiceRegistry.Get(Const aName: string): TRttiInterfaceType;
  212. begin
  213. if not Find(aName,Result) then
  214. Raise ERPCClient.CreateFmt(SErrUnknownServiceName ,[aName]);
  215. end;
  216. function TFPRPCServiceRegistry.Get(const aGUID: TGUID; out aName: String): TRttiInterfaceType;
  217. begin
  218. if not Find(aGuid,Result,aName) then
  219. raise ERPCClient.CreateFmt(SErrUnknownServiceGUID, [aGuid.ToString]);
  220. end;
  221. generic procedure TFPRPCServiceRegistry.Add <T>(const aName : string = '');
  222. begin
  223. Add(TypeInfo(T),aName);
  224. end;
  225. { TFPRPCClient }
  226. function TFPRPCClient.CreateVirtualInterface(IntfType : TRttiInterfaceType; const aName: string) : IInterface;
  227. begin
  228. Result:=TFPRPCVirtualInterface.Create(IntfType.Handle,aName,Self) as IInterface
  229. end;
  230. function TFPRPCClient.DoCreateProxy(constref aIID: TGuid; out aObj): Boolean;
  231. Var
  232. IntfType : TRttiInterfaceType;
  233. aName : string;
  234. aIntf : IInterface;
  235. begin
  236. Result:=RPCServiceRegistry.Find(aIID,IntfType,aName);
  237. if Result then
  238. begin
  239. aIntf:=CreateVirtualInterface(IntfType,aName);
  240. Result:=(aIntf.QueryInterface(aIID,aObj)=S_OK);
  241. end;
  242. end;
  243. function TFPRPCClient.DoCreateProxy(const aName: String; out aObj): Boolean;
  244. Var
  245. IntfType : TRttiInterfaceType;
  246. aIntf : IInterface;
  247. begin
  248. Result:=RPCServiceRegistry.Find(aName,IntfType);
  249. if Result then
  250. begin
  251. aIntf:=CreateVirtualInterface(IntfType,aName);
  252. Result:=(aIntf.QueryInterface(IntfType.GUID,aObj)=S_OK);
  253. end;
  254. end;
  255. function TFPRPCClient.QueryInterface(constref aIID: TGuid; out aObj): LongInt;{$IFNDEF WINDOWS}cdecl{$ELSE}stdcall{$ENDIF};
  256. begin
  257. Result:=Inherited QueryInterface(aIID,aObj);
  258. if (Result<>S_OK) then
  259. begin
  260. if DoCreateProxy(aIID,aObj) then
  261. Result:=S_OK
  262. else
  263. Result:=E_NOINTERFACE;
  264. end
  265. end;
  266. function TFPRPCClient.GetClient: TAbstractWebClient;
  267. begin
  268. Result:=FClient;
  269. if Result=Nil then
  270. begin
  271. if FInternalClient=Nil then
  272. FInternalClient:=TFPHTTPWebClient.Create(Self);
  273. Result:=FInternalClient;
  274. end;
  275. end;
  276. function TFPRPCClient.CreateRPCRequest(const aClassName, aMethodName: String; IsNotification: Boolean): TJSONObject;
  277. begin
  278. Result := TJSONObject.Create;
  279. try
  280. Result.Add('method', aMethodName);
  281. Result.Add('class', aClassName);
  282. Result.Add('jsonrpc','2.0');
  283. // In case of notification, do not send an ID
  284. if Not (IsNotification and (rcoNotifications in Options)) then
  285. begin
  286. inc(FRequestID);
  287. Result.Add('id',FRequestID);
  288. end;
  289. except
  290. Result.Free;
  291. Raise;
  292. end;
  293. end;
  294. function TFPRPCClient.CreateService(const aName: string): IInterface;
  295. begin
  296. if not DoCreateProxy(aName,Result) then
  297. Raise ERPCClient.CreateFmt(SErrUnknownServiceName,[aName]);
  298. end;
  299. generic function TFPRPCClient.CreateService<T>(const aName: string): T;
  300. Var
  301. II : IInterface;
  302. begin
  303. Result:=Nil;
  304. II:=CreateService(aName);
  305. if II.QueryInterface(RPCServiceRegistry.Get(aName).GUID,Result)<>S_OK then
  306. Raise ERPCClient.CreateFmt(SErrSupportedServiceName,[aName]);
  307. end;
  308. Function TFPRPCClient.EncodeParams(aMethod: TRttiMethod; const aArgs: TValueArray; out VarParamCount : Integer) : TJSONData;
  309. var
  310. UseObj : Boolean;
  311. args: TRttiParameterArray;
  312. arg: TRttiParameter;
  313. I,argIdx: Integer;
  314. argVal : TJSONData;
  315. begin
  316. varParamCount:=0;
  317. UseObj:=rcoObjectParam in Options;
  318. if UseObj then
  319. Result := TJSONObject.Create
  320. else
  321. Result := TJSONArray.Create;
  322. try
  323. argIdx:=1;
  324. args := aMethod.GetParameters;
  325. for I:=0 to length(args)-1 do
  326. begin
  327. Arg:=args[i];
  328. if [pfHidden,pfSelf] * arg.Flags <> [] then
  329. Continue
  330. else if ([pfVar,pfOut] * arg.Flags)<>[] then
  331. Inc(VarParamCount);
  332. argVal:=ValueToJSON(aArgs[argidx], arg.ParamType);
  333. if UseObj then
  334. TJSONObject(Result).Add(arg.Name, argVal)
  335. else
  336. TJSONArray(Result).Add(argVal);
  337. Inc(argidx);
  338. end;
  339. except
  340. Result.Free;
  341. Raise;
  342. end;
  343. end;
  344. Function TFPRPCClient.DecodeResult(Response : TJSONObject; aMethod: TRttiMethod; const aArgs: TValueArray; HaveReturnValues : Boolean): TValue;
  345. Var
  346. i,argIdx : Integer;
  347. args : TRttiParameterArray;
  348. arg : TRttiParameter;
  349. resobj : TJSONObject;
  350. value: TValue;
  351. begin
  352. Result:=Default(TValue);
  353. if Assigned(aMethod.ReturnType) or HaveReturnValues then
  354. if not Assigned(Response) then
  355. raise ERPCClient.CreateFmt(SErrExpectedReturnButNoServerReturn,[aMethod.Name]);
  356. if Not HaveReturnValues then
  357. begin
  358. if Assigned(aMethod.ReturnType) then
  359. Result := JSONToValue(response.Elements['result'], aMethod.ReturnType);
  360. end
  361. else
  362. begin
  363. resObj:=response.Objects['result'];
  364. if Assigned(aMethod.ReturnType) then
  365. Result := JSONToValue(resObj.Elements['$result'], aMethod.ReturnType);
  366. argidx := 1;
  367. args:=aMethod.GetParameters;
  368. for i := 0 to High(args) do
  369. begin
  370. arg := Args[i];
  371. if pfHidden in arg.Flags then
  372. Continue;
  373. if arg.Flags * [pfOut, pfVar] = [] then
  374. begin
  375. Inc(argidx);
  376. Continue;
  377. end;
  378. value := JSONToValue(resObj.Elements[arg.Name], arg.ParamType);
  379. value.ExtractRawData(aArgs[argidx].GetReferenceToRawData);
  380. Inc(argidx);
  381. end;
  382. end;
  383. end;
  384. procedure TFPRPCClient.HandleInvoke(const aClassName : String; aMethod: TRttiMethod; const aArgs: TValueArray; out aResult: TValue);
  385. var
  386. request, response: TJSONObject;
  387. argobj: TJSONData;
  388. VarParamCount:Integer;
  389. begin
  390. aResult:=Default(TValue);
  391. response:=nil;
  392. Request:=CreateRPCRequest(aClassName,aMethod.Name,Not Assigned(aMethod.ReturnType));
  393. try
  394. { skip Self argument }
  395. argObj:=EncodeParams(aMethod,aArgs,VarParamCount);
  396. request.Add('params', argobj);
  397. response := DoRequest(request) as TJSONObject;
  398. aResult:=DecodeResult(Response,aMethod,aArgs,VarParamCount>0);
  399. finally
  400. response.Free;
  401. request.Free;
  402. end;
  403. end;
  404. function TFPRPCClient.DoRequest(aRequest: TJSONObject): TJSONObject;
  405. var
  406. aClient: TAbstractWebClient;
  407. Req : TWebClientRequest;
  408. Resp: TWebClientResponse;
  409. S : TJSONStringType;
  410. Res : TJSONData;
  411. begin
  412. Result:=Nil;
  413. aClient := GetClient;
  414. Resp:=Nil;
  415. Req:=aClient.CreateRequest;
  416. try
  417. S:=aRequest.AsJSON;
  418. // Writeln('Request : ',S);
  419. Req.Content.WriteBuffer(S[1],Length(S));
  420. Resp:=aClient.ExecuteRequest('POST',FBaseURL,Req);
  421. // Writeln('Response : ',Resp.GetContentAsString);
  422. // For notification methods, there is no return !
  423. if (resp.Content.Size>0) then
  424. begin
  425. resp.Content.Position:=0;
  426. Res:=GetJSON(resp.Content,True);
  427. if (Res is TJSONObject) then
  428. Result:=Res as TJSONObject
  429. else
  430. begin
  431. Res.Free;
  432. Raise ERPCClient.Create(SErrInvalidServerResponse);
  433. end;
  434. end;
  435. finally
  436. Req.Free;
  437. Resp.Free;
  438. end;
  439. end;
  440. end.