unit TestMethodCallFromRecordParams; interface // We need RTTI here! {$M+} uses System.Rtti, System.SysUtils, System.TypInfo, System.Classes, Myc.Data.Records; type TIndicatorProc = reference to function(const Value: TValue): TResult; TIndicatorFactoryProc = reference to function(const Params: TParams): TIndicatorProc; IndicatorFactoryAttribute = class(TCustomAttribute); TGenericIndicatorFactory = class private FParameterLayout: TDataRecord.TLayout; FFactoryProc: TIndicatorFactoryProc; public constructor Create( const AParameterLayout: TDataRecord.TLayout; const AFactoryProc: TIndicatorFactoryProc ); class function CreateFromTemplate: TGenericIndicatorFactory; function CreateIndicator(const Params: TDataRecord): TIndicatorProc; property ParameterLayout: TDataRecord.TLayout read FParameterLayout; end; TMyWorker = class published type TParams = record Log: Int64; text: String; end; TArgs = record xyz: Double; end; TResult = record val: Int64; desc: String; end; [IndicatorFactory] class function CreateFactory: TIndicatorFactoryProc; static; end; procedure Test1(const Log: TStrings); implementation class function TMyWorker.CreateFactory: TIndicatorFactoryProc; begin Result := function(const Params: TParams): TIndicatorProc begin var Log := TStrings(Params.Log); var text := Params.text; Result := function(const Args: TArgs): TResult begin // Use Format to avoid locale issues with float conversion Log.Add(Format('Val=%f (...%s)', [Args.xyz, text])); Result.desc := 'done'; end; end; end; constructor TGenericIndicatorFactory.Create( const AParameterLayout: TDataRecord.TLayout; const AFactoryProc: TIndicatorFactoryProc ); begin inherited Create; FParameterLayout := AParameterLayout; FFactoryProc := AFactoryProc; end; class function TGenericIndicatorFactory.CreateFromTemplate: TGenericIndicatorFactory; var Ctx: TRttiContext; rttiType: TRttiType; templateFactoryMethod: TRttiMethod; parameterLayout: TDataRecord.TLayout; factoryProc: TIndicatorFactoryProc; begin // This function creates a generic factory from a template class (like TMyWorker). // It uses RTTI to find the necessary types (TParams, TValue, TResult) and the // factory method, and then constructs a set of wrappers to adapt the specific // types of the template to the generic TDataRecord used by this factory. Ctx := TRttiContext.Create; rttiType := Ctx.GetType(TypeInfo(T)); // Find nested record types for parameters, values, and results. var paramsType := Ctx.FindType(rttiType.QualifiedName + '.' + 'TParams'); if not Assigned(paramsType) then raise EArgumentException.CreateFmt('TParams not found in "%s"', [rttiType.Name]); var argsType := Ctx.FindType(rttiType.QualifiedName + '.' + 'TArgs'); if not Assigned(paramsType) then raise EArgumentException.CreateFmt('TArgs not found in "%s"', [rttiType.Name]); var resultType := Ctx.FindType(rttiType.QualifiedName + '.' + 'TResult'); if not Assigned(paramsType) then raise EArgumentException.CreateFmt('TResult not found in "%s"', [rttiType.Name]); // Find the static factory method marked with the [Factory] attribute. templateFactoryMethod := nil; for var method in rttiType.GetMethods do begin if method.HasAttribute then begin templateFactoryMethod := method; break; end; end; if not Assigned(templateFactoryMethod) then raise EArgumentException.CreateFmt('[Factory] attribute not found on any method in "%s"', [rttiType.Name]); // Create the layout for the parameters, which will be exposed by this factory. parameterLayout := TDataRecord.TLayout.FromRecord(paramsType.Handle); // Create the main factory procedure. This is a double-nested anonymous method // that wraps the template's specific factory and worker functions. factoryProc := function(const Params: TDataRecord): TIndicatorProc var // Capture the created worker proc from the original factory as a TValue to manage its lifetime. workerProcAsValue: TValue; begin // Outer anonymous method: This is the factory proc. // It gets called with a TDataRecord of parameters. // 1. Invoke the template's static factory method (e.g., TMyWorker.CreateFactory) var factoryProcAsValue := templateFactoryMethod.Invoke(TValue.Empty, []); // 2. Invoke the factory proc itself to get the actual worker proc. var rttiFactoryProc := Ctx.GetType(factoryProcAsValue.TypeInfo) as TRttiInterfaceType; var factoryInvokeMethod := rttiFactoryProc.GetMethod('Invoke'); // The parameter for this 'Invoke' call is the TParams record. // Wrap the incoming TDataRecord 'Params' into a TValue for the call. var factoryParamTypeInfo := factoryInvokeMethod.GetParameters[0].ParamType.Handle; Assert(factoryParamTypeInfo = paramsType.Handle); var factoryArg: array[0..0] of TValue; TValue.Make(Params.RawData, factoryParamTypeInfo, factoryArg[0]); // This call returns the worker proc (e.g., a TIndicatorProc) as a TValue. workerProcAsValue := factoryInvokeMethod.Invoke(factoryProcAsValue, factoryArg); // 3. Return a new anonymous method that wraps the worker proc. // This wrapper conforms to the generic TIndicatorProc signature. var rttiWorkerProc := Ctx.GetType(workerProcAsValue.TypeInfo); var indicatorInvokeMethod := rttiWorkerProc.GetMethod('Invoke'); var indicatorParamTypeInfo := indicatorInvokeMethod.GetParameters[0].ParamType.Handle; var indicatorResultTypeInfo := indicatorInvokeMethod.ReturnType.Handle; Assert(indicatorParamTypeInfo = argsType.Handle); Assert(indicatorResultTypeInfo = resultType.Handle); var valueLayout := TDataRecord.TLayout.FromRecord(indicatorParamTypeInfo); var resultLayout := TDataRecord.TLayout.FromRecord(indicatorResultTypeInfo); Result := function(const Value: TDataRecord): TDataRecord begin // Inner anonymous method: This is the actual indicator proc wrapper. Assert(Value.Layout = valueLayout); // Prepare argument for the worker proc invocation. var args: array[0..0] of TValue; TValue.MakeWithoutCopy(Value.RawData, indicatorParamTypeInfo, args[0], true); // Invoke the actual worker proc. var resultAsTValue := indicatorInvokeMethod.Invoke(workerProcAsValue, args); // The result is a TValue containing the result record (e.g., TMyWorker.TResult). // Copy its contents into a new TDataRecord to return it. Assert(TDataRecord.TLayout.FromRecord(resultAsTValue.TypeInfo) = resultLayout); Result := TDataRecord.Create(resultLayout); var src := resultAsTValue.GetReferenceToRawData; resultLayout.Copy(src, Result.RawData); // Erase all raw data in the TValue, leaving it as an empty capsule. Ownership it taken // over to the resulting TDataRecord. FillChar(src^, resultLayout.Size, 0); end; end; // Create the final factory instance with the generated layout and wrapper procedure. Result := TGenericIndicatorFactory.Create(parameterLayout, factoryProc); end; function TGenericIndicatorFactory.CreateIndicator(const Params: TDataRecord): TIndicatorProc; begin // The factory proc does all the heavy lifting. We just need to call it. // A real implementation might add checks, e.g., that the layout of Params matches. Assert( (not Assigned(FParameterLayout.Fields)) or (Params.Layout = FParameterLayout), 'Invalid parameter layout for indicator creation' ); Result := FFactoryProc(Params); end; procedure Test1(const Log: TStrings); begin var fact := TGenericIndicatorFactory.CreateFromTemplate; var params := TDataRecord.FromRecord; params.SetValue('Log', Int64(Log)); params.SetValue('text', 'The quick brown fox jumps...'); var indi := fact.CreateIndicator(params); var args := TDataRecord.FromRecord; args.SetValue('xyz', 3.14159); var res := indi(args); var desc := res.GetValue('desc'); Log.Add('Final result description: ' + desc); Assert(desc = 'done'); end; end.