mirror of
https://github.com/StarBeat/JsonRpc.git
synced 2026-03-08 03:55:29 +08:00
init repo
This commit is contained in:
parent
2bfce66fb5
commit
5c8c6a82bb
4
.gitignore
vendored
4
.gitignore
vendored
@ -23,6 +23,10 @@ bld/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
.vs
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
|
||||
36
CodeGenerator/AttributeChecker.cs
Normal file
36
CodeGenerator/AttributeChecker.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using Mono.Cecil;
|
||||
|
||||
namespace CodeGenerator
|
||||
{
|
||||
public static class AttributeChecker
|
||||
{
|
||||
public static CustomAttribute? GetTargetttribute(this ICustomAttributeProvider definition)
|
||||
{
|
||||
var customAttributes = definition.CustomAttributes;
|
||||
|
||||
return customAttributes.FirstOrDefault(_ => _.AttributeType.Name == nameof(JsonRPCAttribute));
|
||||
}
|
||||
|
||||
public static bool ContainsTargetAttribute(this ICustomAttributeProvider definition) =>
|
||||
GetTargetttribute(definition) != null;
|
||||
|
||||
public static bool IsCompilerGenerated(this ICustomAttributeProvider definition)
|
||||
{
|
||||
var customAttributes = definition.CustomAttributes;
|
||||
|
||||
return customAttributes.Any(_ => _.AttributeType.Name == "CompilerGeneratedAttribute");
|
||||
}
|
||||
|
||||
public static void RemoveTargetAttribute(this ICustomAttributeProvider definition)
|
||||
{
|
||||
var customAttributes = definition.CustomAttributes;
|
||||
|
||||
var targetAttribute = customAttributes.FirstOrDefault(_ => _.AttributeType.Name == nameof(JsonRPCAttribute));
|
||||
|
||||
if (targetAttribute != null)
|
||||
{
|
||||
customAttributes.Remove(targetAttribute);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
145
CodeGenerator/CecilExtensions.cs
Normal file
145
CodeGenerator/CecilExtensions.cs
Normal file
@ -0,0 +1,145 @@
|
||||
using Mono.Cecil;
|
||||
using Mono.Cecil.Cil;
|
||||
using Mono.Collections.Generic;
|
||||
|
||||
namespace CodeGenerator
|
||||
{
|
||||
public static class CecilExtensions
|
||||
{
|
||||
public static bool IsBoxingRequired(this TypeReference typeReference, TypeReference expectedType)
|
||||
{
|
||||
if (expectedType.IsValueType && string.Equals(typeReference.FullName, expectedType.FullName))
|
||||
{
|
||||
// Boxing is never required if type is expected
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeReference.IsValueType ||
|
||||
typeReference.IsGenericParameter)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static IEnumerable<MethodDefinition> AbstractMethods(this TypeDefinition type) =>
|
||||
type.Methods.Where(_ => _.IsAbstract).ToList();
|
||||
|
||||
public static IEnumerable<MethodDefinition> ConcreteMethods(this TypeDefinition type) =>
|
||||
type.Methods.Where(x => !x.IsAbstract &&
|
||||
x.HasBody &&
|
||||
!IsEmptyConstructor(x)).ToList();
|
||||
|
||||
static bool IsEmptyConstructor(this MethodDefinition method) =>
|
||||
method.Name == ".ctor" &&
|
||||
method.Body.Instructions.Count(_ => _.OpCode != OpCodes.Nop) == 3;
|
||||
|
||||
public static bool IsInstanceConstructor(this MethodDefinition methodDefinition) =>
|
||||
methodDefinition.IsConstructor && !methodDefinition.IsStatic;
|
||||
|
||||
public static void InsertBefore(this MethodBody body, Instruction target, Instruction instruction) =>
|
||||
body.Instructions.InsertBefore(target, instruction);
|
||||
|
||||
public static void InsertBefore(this Collection<Instruction> instructions, Instruction target, Instruction instruction)
|
||||
{
|
||||
var index = instructions.IndexOf(target);
|
||||
instructions.Insert(index, instruction);
|
||||
}
|
||||
|
||||
public static string MethodName(this MethodDefinition method)
|
||||
{
|
||||
if (method.IsConstructor)
|
||||
{
|
||||
return $"{method.DeclaringType.Name}{method.Name} ";
|
||||
}
|
||||
|
||||
return $"{method.DeclaringType.Name}.{method.Name} ";
|
||||
}
|
||||
|
||||
public static void Insert(this MethodBody body, int index, IEnumerable<Instruction> instructions)
|
||||
{
|
||||
instructions = instructions.Reverse();
|
||||
foreach (var instruction in instructions)
|
||||
{
|
||||
body.Instructions.Insert(index, instruction);
|
||||
}
|
||||
}
|
||||
|
||||
public static void Add(this MethodBody body, params Instruction[] instructions)
|
||||
{
|
||||
foreach (var instruction in instructions)
|
||||
{
|
||||
body.Instructions.Add(instruction);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsYield(this MethodDefinition method)
|
||||
{
|
||||
if (method.ReturnType is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!method.ReturnType.Name.StartsWith("IEnumerable"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var stateMachinePrefix = $"<{method.Name}>";
|
||||
var nestedTypes = method.DeclaringType.NestedTypes;
|
||||
return nestedTypes.Any(_ => _.Name.StartsWith(stateMachinePrefix));
|
||||
}
|
||||
|
||||
public static CustomAttribute GetAsyncStateMachineAttribute(this MethodDefinition method) =>
|
||||
method.CustomAttributes.FirstOrDefault(_ => _.AttributeType.Name == "AsyncStateMachineAttribute");
|
||||
|
||||
public static bool IsAsync(this MethodDefinition method) =>
|
||||
GetAsyncStateMachineAttribute(method) != null;
|
||||
|
||||
public static bool IsLeaveInstruction(this Instruction instruction) =>
|
||||
instruction.OpCode == OpCodes.Leave ||
|
||||
instruction.OpCode == OpCodes.Leave_S;
|
||||
|
||||
public static MethodDefinition Method(this TypeDefinition type, string name)
|
||||
{
|
||||
var method = type.Methods.FirstOrDefault(_ => _.Name == name);
|
||||
|
||||
if (method is null)
|
||||
{
|
||||
throw new($"Could not find method '{name}' on type {type.FullName}.");
|
||||
}
|
||||
|
||||
return method;
|
||||
}
|
||||
|
||||
public static MethodDefinition Method(this TypeDefinition type, string name, params string[] parameters)
|
||||
{
|
||||
var method = type.Methods
|
||||
.FirstOrDefault(x =>
|
||||
{
|
||||
return x.Name == name &&
|
||||
parameters.Length == x.Parameters.Count &&
|
||||
x.Parameters.Select(y => y.ParameterType.Name).SequenceEqual(parameters);
|
||||
});
|
||||
|
||||
if (method is null)
|
||||
{
|
||||
throw new($"Could not find method '{name}' on type {type.FullName}.");
|
||||
}
|
||||
|
||||
return method;
|
||||
}
|
||||
|
||||
public static TypeDefinition Type(this List<TypeDefinition> types, string name)
|
||||
{
|
||||
var type = types.FirstOrDefault(_ => _.Name == name);
|
||||
if (type is null)
|
||||
{
|
||||
throw new($"Could not find type '{name}'.");
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
CodeGenerator/CodeGenerator.csproj
Normal file
18
CodeGenerator/CodeGenerator.csproj
Normal file
@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
|
||||
<PackageReference Include="Mono.Cecil" Version="0.11.6" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\JsonRPC\JsonRPC.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
8
CodeGenerator/JsonRPCAttribute.cs
Normal file
8
CodeGenerator/JsonRPCAttribute.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace CodeGenerator
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
|
||||
public class JsonRPCAttribute : Attribute
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
506
CodeGenerator/JsonRPCCodeGenerator.cs
Normal file
506
CodeGenerator/JsonRPCCodeGenerator.cs
Normal file
@ -0,0 +1,506 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.Emit;
|
||||
using Mono.Cecil;
|
||||
using Mono.Cecil.Cil;
|
||||
using Mono.Cecil.Rocks;
|
||||
using System.Text;
|
||||
|
||||
namespace CodeGenerator
|
||||
{
|
||||
public class JsonRPCCodeGenerator
|
||||
{
|
||||
private readonly Action<string> logger;
|
||||
private ModuleDefinition moduleDefinition = null!;
|
||||
private List<TypeDefinition> types = null!;
|
||||
private string filePath = null!;
|
||||
private MethodDefinition moduleeStaticConstructor = null;
|
||||
public JsonRPCCodeGenerator(Action<string> logFn)
|
||||
{
|
||||
this.logger = logFn;
|
||||
}
|
||||
|
||||
public void LoadAssembly(string filePath)
|
||||
{
|
||||
this.filePath = filePath;
|
||||
var readerParameters = new ReaderParameters
|
||||
{
|
||||
InMemory = true
|
||||
};
|
||||
|
||||
moduleDefinition = ModuleDefinition.ReadModule(filePath, readerParameters);
|
||||
}
|
||||
|
||||
public void WriteModule()
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(filePath, $"WriteModule call but {filePath} is null.");
|
||||
|
||||
moduleDefinition.Write(filePath);
|
||||
}
|
||||
|
||||
private void AddModuleStaticConstructor(TypeDefinition type)
|
||||
{
|
||||
var sctor = type.GetStaticConstructor();
|
||||
if (sctor == null)
|
||||
{
|
||||
sctor = new MethodDefinition(".cctor", MethodAttributes.Static | MethodAttributes.Private | MethodAttributes.HideBySig, moduleDefinition.ImportReference(typeof(void)));
|
||||
sctor.IsRuntimeSpecialName = true;
|
||||
sctor.IsSpecialName = true;
|
||||
|
||||
//var cw = moduleDefinition.ImportReference(typeof(Console).GetMethods().Where(m => m.GetParameters().Length == 1 && m.Name == "WriteLine" && m.GetParameters()[0].ParameterType == typeof(string)).First());
|
||||
|
||||
// sctor.Body.Instructions.Add(Instruction.Create(OpCodes.Nop));
|
||||
// sctor.Body.Instructions.Add(Instruction.Create(OpCodes.Ldstr, "AddModuleStaticConstructor"));
|
||||
// sctor.Body.Instructions.Add(Instruction.Create(OpCodes.Call, cw));
|
||||
|
||||
sctor.Body.Instructions.Add(Instruction.Create(OpCodes.Nop));
|
||||
sctor.Body.Instructions.Add(Instruction.Create(OpCodes.Ret));
|
||||
type.Methods.Add(sctor);
|
||||
}
|
||||
|
||||
moduleeStaticConstructor = sctor;
|
||||
}
|
||||
|
||||
private void AddModuleInitializerMethod(MethodReference methodReference)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(moduleeStaticConstructor, $"moduleeStaticConstructor is null.");
|
||||
if (moduleeStaticConstructor.Body.Instructions[0].OpCode.Code == Code.Nop)
|
||||
{
|
||||
moduleeStaticConstructor.Body.Instructions.Insert(1, Instruction.Create(OpCodes.Call, methodReference));
|
||||
}
|
||||
else
|
||||
{
|
||||
moduleeStaticConstructor.Body.Instructions.Insert(0, Instruction.Create(OpCodes.Call, methodReference));
|
||||
}
|
||||
}
|
||||
|
||||
public void ProcessAssembly()
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(moduleDefinition, $"moduleDefinition is null.");
|
||||
|
||||
types = moduleDefinition.GetTypes().ToList();
|
||||
foreach (var type in types)
|
||||
{
|
||||
if (type.Name.Equals("<Module>"))
|
||||
{
|
||||
AddModuleStaticConstructor(type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (moduleDefinition.Assembly.ContainsTargetAttribute())
|
||||
{
|
||||
foreach (var type in types)
|
||||
{
|
||||
if (type.IsCompilerGenerated())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var method in type.ConcreteMethods())
|
||||
{
|
||||
ProcessMethod(type, method);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var type in types)
|
||||
{
|
||||
if (type.IsCompilerGenerated())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (type.ContainsTargetAttribute())
|
||||
{
|
||||
foreach (var method in type.ConcreteMethods())
|
||||
{
|
||||
ProcessMethod(type, method);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var method in type.ConcreteMethods().Where(_ => _.ContainsTargetAttribute()))
|
||||
{
|
||||
ProcessMethod(type, method);
|
||||
}
|
||||
}
|
||||
|
||||
RemoveAttributes();
|
||||
}
|
||||
|
||||
private MethodDefinition CompileFunc(MemoryStream ms, string funcBody, string returnType)
|
||||
{
|
||||
returnType = returnType.EndsWith("Void") ? "void" : returnType;
|
||||
string code = @$"
|
||||
using System;
|
||||
public class DynamicClass
|
||||
{{
|
||||
public static {returnType} HelloWorld {funcBody}
|
||||
}}";
|
||||
|
||||
// 创建语法树
|
||||
SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(code);
|
||||
|
||||
// 引用必要的程序集
|
||||
var references = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.Where(a => !a.IsDynamic)
|
||||
.Select(a => MetadataReference.CreateFromFile(a.Location))
|
||||
.Cast<MetadataReference>()
|
||||
.ToList();
|
||||
|
||||
// 创建编译选项
|
||||
CSharpCompilationOptions options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, allowUnsafe: true);
|
||||
|
||||
// 创建编译对象
|
||||
CSharpCompilation compilation = CSharpCompilation.Create(
|
||||
"DynamicAssembly",
|
||||
new[] { syntaxTree },
|
||||
references,
|
||||
options
|
||||
);
|
||||
|
||||
// 编译代码
|
||||
EmitResult result = compilation.Emit(ms);
|
||||
|
||||
// 检查编译错误
|
||||
if (!result.Success)
|
||||
{
|
||||
foreach (var diagnostic in result.Diagnostics)
|
||||
{
|
||||
Console.WriteLine(diagnostic.ToString());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// 加载编译后的程序集
|
||||
ms.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
var md = ModuleDefinition.ReadModule(ms);
|
||||
var m = md.GetType("DynamicClass").Method("HelloWorld");
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
private string GetMethodSignature(MethodDefinition method)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.Append(method.DeclaringType.FullName.Replace('.', '_'));
|
||||
sb.Append("__");
|
||||
sb.Append(method.Name);
|
||||
sb.Append('_');
|
||||
for (int i = 0; i < method.Parameters.Count; i++)
|
||||
{
|
||||
var param = method.Parameters[i];
|
||||
sb.Append('_');
|
||||
sb.Append(param.ParameterType.Name);
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private MethodDefinition AddRPCRealMethod(TypeDefinition type, MethodDefinition method)
|
||||
{
|
||||
var methodSignature = GetMethodSignature(method);
|
||||
MethodBody body = method.Body;
|
||||
|
||||
MethodDefinition realMethod = new MethodDefinition(methodSignature, MethodAttributes.Public | MethodAttributes.Static, method.ReturnType);
|
||||
realMethod.Body.InitLocals = body.InitLocals;
|
||||
realMethod.Body.Instructions.Clear();
|
||||
realMethod.Body.Variables.Clear();
|
||||
|
||||
for (int i = 0; i < body.Instructions.Count; i++)
|
||||
{
|
||||
var ins = body.Instructions[i];
|
||||
realMethod.Body.Instructions.Add(ins);
|
||||
}
|
||||
|
||||
|
||||
for (int i = 0; i < body.Variables.Count; i++)
|
||||
{
|
||||
realMethod.Body.Variables.Add(body.Variables[i]);
|
||||
}
|
||||
|
||||
for (int i = 0; i < method.Parameters.Count; i++)
|
||||
{
|
||||
var param = method.Parameters[i];
|
||||
realMethod.Parameters.Add(param);
|
||||
}
|
||||
type.Methods.Add(realMethod);
|
||||
|
||||
|
||||
return realMethod;
|
||||
}
|
||||
|
||||
private void AddRPCHandleMethod(TypeDefinition type, MethodDefinition method)
|
||||
{
|
||||
var name = method.Name +"_Handle";
|
||||
var handleMethod = new MethodDefinition(name, MethodAttributes.Static | MethodAttributes.Public, moduleDefinition.ImportReference(typeof(JsonRPC.Protocol.JsonRPCResponse)));
|
||||
handleMethod.Parameters.Add(new("req", ParameterAttributes.In, moduleDefinition.ImportReference(typeof(JsonRPC.Protocol.JsonRPCRequest))));
|
||||
using var ms = new MemoryStream();
|
||||
StringBuilder bodyBuilder = new();
|
||||
StringBuilder parameterBuilder = new();
|
||||
type.Methods.Add(handleMethod);
|
||||
bodyBuilder.AppendLine(@$"
|
||||
(JsonRPC.Protocol.JsonRPCRequest req)
|
||||
{{
|
||||
|
||||
if(req.Params != null && req.Params.Count != {method.Parameters.Count})
|
||||
{{
|
||||
return new JsonRPC.Protocol.JsonRPCResponse()
|
||||
{{
|
||||
Id = req.Id,
|
||||
Error = new JsonRPC.Protocol.JsonRPCError()
|
||||
{{
|
||||
Code = (int)JsonRPC.Protocol.EErrorCode.InvalidParam,
|
||||
Message = ""req Parameters {method.Parameters.Count}"",
|
||||
}}
|
||||
}};
|
||||
}}
|
||||
else
|
||||
{{
|
||||
|
||||
");
|
||||
|
||||
bool returnVoid = method.ReturnType.Name.EndsWith("Void");
|
||||
if (!returnVoid)
|
||||
{
|
||||
bodyBuilder.Append("var ret =");
|
||||
}
|
||||
bodyBuilder.Append($"{method.Name}(");
|
||||
for (int i = 0; i < method.Parameters.Count; i++)
|
||||
{
|
||||
var param = method.Parameters[i];
|
||||
bodyBuilder.Append($"({param.ParameterType.FullName})req.Params[{i}]");
|
||||
parameterBuilder.Append(param.ParameterType.FullName);
|
||||
parameterBuilder.Append(' ');
|
||||
parameterBuilder.Append(param.Name);
|
||||
if (param != method.Parameters[^1])
|
||||
{
|
||||
parameterBuilder.Append(',');
|
||||
bodyBuilder.Append(',');
|
||||
}
|
||||
}
|
||||
bodyBuilder.Append(");");
|
||||
if (returnVoid)
|
||||
{
|
||||
bodyBuilder.AppendLine(@"
|
||||
return new JsonRPC.Protocol.JsonRPCResponse()
|
||||
{
|
||||
Id = req.Id,
|
||||
};
|
||||
");
|
||||
}
|
||||
else
|
||||
{
|
||||
bodyBuilder.AppendLine(@$"
|
||||
return new JsonRPC.Protocol.JsonRPCResponse<{method.ReturnType.FullName}>()
|
||||
{{
|
||||
Id = req.Id,
|
||||
Result = ret,
|
||||
}};
|
||||
");
|
||||
}
|
||||
bodyBuilder.AppendLine($@"
|
||||
}}
|
||||
}}
|
||||
|
||||
public static {(returnVoid ? "void" : method.ReturnType.FullName)} {method.Name}({parameterBuilder.ToString()}){{ return {(returnVoid ? "" : "default")};}}
|
||||
");
|
||||
|
||||
|
||||
var tempFunc = CompileFunc(ms, bodyBuilder.ToString(), "JsonRPC.Protocol.JsonRPCResponse");
|
||||
ReplaceMethodBody(tempFunc, handleMethod);
|
||||
for (int i = 0; i < handleMethod.Body.Instructions.Count; i++)
|
||||
{
|
||||
var ins = handleMethod.Body.Instructions[i];
|
||||
if (ins.OpCode.Code == Code.Call && ins.Operand is MethodReference mr)
|
||||
{
|
||||
if (mr.Name.Equals(method.Name))
|
||||
{
|
||||
ins.Operand = moduleDefinition.ImportReference(method);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AddRPCRegMethod(type, handleMethod);
|
||||
}
|
||||
|
||||
private void AddRPCRegMethod(TypeDefinition type, MethodDefinition method)
|
||||
{
|
||||
var name = method.Name +"_Reg";
|
||||
|
||||
var regMethod = new MethodDefinition(name, MethodAttributes.Static | MethodAttributes.Public, moduleDefinition.ImportReference(typeof(void)));
|
||||
type.Methods.Add(regMethod);
|
||||
|
||||
var body = regMethod.Body;
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
StringBuilder bodyBuilder = new();
|
||||
|
||||
bodyBuilder.AppendLine(@$"
|
||||
()
|
||||
{{
|
||||
var fn = new Func<JsonRPC.Protocol.JsonRPCRequest, JsonRPC.Protocol.JsonRPCResponse>({method.Name});
|
||||
JsonRPC.RPC.Receiver.Instance.RegHandler(""{method.Name.Replace("_Handle", "")}"", fn);
|
||||
}}
|
||||
public static JsonRPC.Protocol.JsonRPCResponse {method.Name}(JsonRPC.Protocol.JsonRPCRequest req){{ return null;}}
|
||||
");
|
||||
|
||||
var tempFunc = CompileFunc(ms, bodyBuilder.ToString(), "Void");
|
||||
ReplaceMethodBody(tempFunc, regMethod);
|
||||
for (int i = 0; i < regMethod.Body.Instructions.Count; i++)
|
||||
{
|
||||
var ins = regMethod.Body.Instructions[i];
|
||||
if (ins.OpCode.Code == Code.Ldftn && ins.Operand is MethodReference mr)
|
||||
{
|
||||
if (mr.Name.Equals(method.Name))
|
||||
{
|
||||
ins.Operand = moduleDefinition.ImportReference(method);
|
||||
}
|
||||
}
|
||||
}
|
||||
AddModuleInitializerMethod(regMethod);
|
||||
}
|
||||
|
||||
private void ReplaceMethodBody(MethodDefinition srcMethod, MethodDefinition dstMethod)
|
||||
{
|
||||
var body = dstMethod.Body;
|
||||
body.Instructions.Clear();
|
||||
body.Variables.Clear();
|
||||
|
||||
srcMethod.Body.SimplifyMacros();
|
||||
|
||||
body.MaxStackSize = srcMethod.Body.MaxStackSize;
|
||||
body.InitLocals = srcMethod.Body.InitLocals;
|
||||
|
||||
body.LocalVarToken = srcMethod.Body.LocalVarToken;
|
||||
|
||||
foreach (var item in srcMethod.Body.Instructions)
|
||||
{
|
||||
if (item.OpCode.Code == Code.Castclass && item.Operand is TypeReference tr)
|
||||
{
|
||||
var instruction = Instruction.Create(item.OpCode, moduleDefinition.ImportReference(tr));
|
||||
instruction.Offset = item.Offset;
|
||||
body.Instructions.Add(instruction);
|
||||
}
|
||||
else if (item.Operand is MethodReference mr)
|
||||
{
|
||||
var newMr = moduleDefinition.ImportReference(mr);
|
||||
var instruction = Instruction.Create(item.OpCode, newMr);
|
||||
instruction.Offset = item.Offset;
|
||||
body.Instructions.Add(instruction);
|
||||
}
|
||||
else
|
||||
{
|
||||
body.Instructions.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < srcMethod.Body.Variables.Count; i++)
|
||||
{
|
||||
var localVar = srcMethod.Body.Variables[i];
|
||||
localVar.VariableType = moduleDefinition.ImportReference(localVar.VariableType);
|
||||
body.Variables.Add(localVar);
|
||||
}
|
||||
body.Optimize();
|
||||
}
|
||||
|
||||
private void ProcessMethod(TypeDefinition type, MethodDefinition method)
|
||||
{
|
||||
bool hasTargetAttribute = method.ContainsTargetAttribute();
|
||||
|
||||
if (method.IsYield())
|
||||
{
|
||||
if (hasTargetAttribute)
|
||||
{
|
||||
logger.Invoke("Could not process '" + method.FullName + "' since methods that yield are currently not supported. Please remove the [Time] attribute from that method.");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.Invoke("Skipping '" + method.FullName + "' since methods that yield are not supported.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!method.IsStatic)
|
||||
{
|
||||
logger.Invoke("Skipping '" + method.FullName + "' non static methods are not supported.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (method.IsAsync() && hasTargetAttribute)
|
||||
{
|
||||
logger.Invoke("Could not process '" + method.FullName + "' async methods are not supported.");
|
||||
return;
|
||||
}
|
||||
|
||||
var realMethod = AddRPCRealMethod(type, method);
|
||||
AddRPCHandleMethod(type, realMethod);
|
||||
StringBuilder parameterBuilder = new();
|
||||
StringBuilder bodyBuilder = new();
|
||||
bool returnVoid = method.ReturnType.Name.EndsWith("Void");
|
||||
|
||||
parameterBuilder.Append('(');
|
||||
bodyBuilder.AppendLine("{");
|
||||
bodyBuilder.AppendLine($"JsonRPC.Protocol.JsonRPCRequest req = new(\"{realMethod.Name}\", JsonRPC.RPC.Sender.Instance.GetId());");
|
||||
foreach (var item in method.Parameters)
|
||||
{
|
||||
parameterBuilder.Append(item.ParameterType.FullName);
|
||||
parameterBuilder.Append(' ');
|
||||
parameterBuilder.Append(item.Name);
|
||||
if (item != method.Parameters[^1])
|
||||
{
|
||||
parameterBuilder.Append(",");
|
||||
}
|
||||
bodyBuilder.AppendLine($"req.AddParam({item.Name});");
|
||||
}
|
||||
parameterBuilder.Append(')');
|
||||
if(returnVoid)
|
||||
{
|
||||
bodyBuilder.AppendLine(@$"
|
||||
var json = JsonRPC.RPC.Sender.Instance.Send(req);
|
||||
var res = JsonRPC.RPC.Sender.Instance.JsonDeserialize<JsonRPC.Protocol.JsonRPCResponse>(json);
|
||||
if(!JsonRPC.RPC.Sender.Instance.HnadleResponseError(res))
|
||||
{{
|
||||
}}
|
||||
return;
|
||||
}}");
|
||||
}
|
||||
else
|
||||
{
|
||||
bodyBuilder.AppendLine(@$"
|
||||
var json = JsonRPC.RPC.Sender.Instance.Send(req);
|
||||
var res = JsonRPC.RPC.Sender.Instance.JsonDeserialize<JsonRPC.Protocol.JsonRPCResponse<{method.ReturnType.FullName}>>(json);
|
||||
if(!JsonRPC.RPC.Sender.Instance.HnadleResponseError(res))
|
||||
{{
|
||||
return default;
|
||||
}}
|
||||
|
||||
return res.Result;
|
||||
}}");
|
||||
}
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
var sigFunc = CompileFunc(ms, parameterBuilder.AppendLine(bodyBuilder.ToString()).ToString(), method.ReturnType.FullName);
|
||||
ReplaceMethodBody(sigFunc, method);
|
||||
}
|
||||
|
||||
void RemoveAttributes()
|
||||
{
|
||||
moduleDefinition.Assembly.RemoveTargetAttribute();
|
||||
foreach (var typeDefinition in types)
|
||||
{
|
||||
typeDefinition.RemoveTargetAttribute();
|
||||
foreach (var method in typeDefinition.Methods)
|
||||
{
|
||||
method.RemoveTargetAttribute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
43
JsonRPC.sln
Normal file
43
JsonRPC.sln
Normal file
@ -0,0 +1,43 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.14.36518.9
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeGenerator", "CodeGenerator\CodeGenerator.csproj", "{35FC52B4-AB3D-4879-ADAA-F90A63A78F8D}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Test", "Test\Test.csproj", "{FA0F37D3-770B-42F4-A53D-36CB8161A6F4}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestInterceptor", "TestInterceptor\TestInterceptor.csproj", "{0B65E76A-4435-4D6B-864A-097475739B42}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonRPC", "JsonRPC\JsonRPC.csproj", "{55CEBB38-910A-413E-80F3-35774E12A396}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{35FC52B4-AB3D-4879-ADAA-F90A63A78F8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{35FC52B4-AB3D-4879-ADAA-F90A63A78F8D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{35FC52B4-AB3D-4879-ADAA-F90A63A78F8D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{35FC52B4-AB3D-4879-ADAA-F90A63A78F8D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{FA0F37D3-770B-42F4-A53D-36CB8161A6F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FA0F37D3-770B-42F4-A53D-36CB8161A6F4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FA0F37D3-770B-42F4-A53D-36CB8161A6F4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FA0F37D3-770B-42F4-A53D-36CB8161A6F4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0B65E76A-4435-4D6B-864A-097475739B42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0B65E76A-4435-4D6B-864A-097475739B42}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0B65E76A-4435-4D6B-864A-097475739B42}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0B65E76A-4435-4D6B-864A-097475739B42}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{55CEBB38-910A-413E-80F3-35774E12A396}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{55CEBB38-910A-413E-80F3-35774E12A396}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{55CEBB38-910A-413E-80F3-35774E12A396}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{55CEBB38-910A-413E-80F3-35774E12A396}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {D497A5D8-3095-4DF8-A63B-C381551EF746}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
18
JsonRPC/JsonRPC.csproj
Normal file
18
JsonRPC/JsonRPC.csproj
Normal file
@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<BaseOutputPath></BaseOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Http\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
15
JsonRPC/Protocol/EJsonRPCVersion.cs
Normal file
15
JsonRPC/Protocol/EJsonRPCVersion.cs
Normal file
@ -0,0 +1,15 @@
|
||||
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
|
||||
namespace JsonRPC.Protocol
|
||||
{
|
||||
public enum EJsonRPCVersion
|
||||
{
|
||||
[EnumMember(Value = "1.0")]
|
||||
Version1 = 1,
|
||||
|
||||
[EnumMember(Value = "2.0")]
|
||||
Version2 = 2
|
||||
}
|
||||
}
|
||||
26
JsonRPC/Protocol/JsonRPCError.cs
Normal file
26
JsonRPC/Protocol/JsonRPCError.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace JsonRPC.Protocol
|
||||
{
|
||||
public enum EErrorCode
|
||||
{
|
||||
ParseError = -32700,// 解析错误,服务器收到无效的 JSON。
|
||||
InvalidRequest = -32600, // 无效请求,发送的 JSON 不是有效的请求对象。
|
||||
MethodNotFound = -32601, // 方法未找到,方法不存在或无效。
|
||||
InvalidParam = -32602, // 无效参数,提供的参数无效。
|
||||
InternalError = -32603, // 内部错误,JSON-RPC 内部错误。
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class JsonRPCError
|
||||
{
|
||||
[JsonPropertyName("data")]
|
||||
public string? Data { get; set; }
|
||||
|
||||
[JsonPropertyName("code")]
|
||||
public int Code { get; set; }
|
||||
|
||||
[JsonPropertyName("message")]
|
||||
public string Message { get; set; } = null!;
|
||||
}
|
||||
}
|
||||
38
JsonRPC/Protocol/JsonRPCRequest.cs
Normal file
38
JsonRPC/Protocol/JsonRPCRequest.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace JsonRPC.Protocol
|
||||
{
|
||||
[Serializable]
|
||||
public class JsonRPCRequest
|
||||
{
|
||||
[JsonPropertyName("method")]
|
||||
public string Method { get; private set; }
|
||||
|
||||
[JsonPropertyName("params")]
|
||||
public List<object> Params { get; private set; } = new();
|
||||
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; private set; }
|
||||
|
||||
[JsonPropertyName("jsonrpc")]
|
||||
public EJsonRPCVersion Version => EJsonRPCVersion.Version2;
|
||||
|
||||
public JsonRPCRequest(string method, int id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(method))
|
||||
{
|
||||
throw new ArgumentNullException(method);
|
||||
}
|
||||
|
||||
this.Method = method;
|
||||
this.Id = id;
|
||||
}
|
||||
|
||||
public JsonRPCRequest AddParam(object param)
|
||||
{
|
||||
this.Params.Add(param);
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
24
JsonRPC/Protocol/JsonRPCResponse.cs
Normal file
24
JsonRPC/Protocol/JsonRPCResponse.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace JsonRPC.Protocol
|
||||
{
|
||||
[Serializable]
|
||||
public class JsonRPCResponse
|
||||
{
|
||||
[JsonPropertyName("jsonrpc")]
|
||||
public EJsonRPCVersion Version { get; set; } = EJsonRPCVersion.Version2;
|
||||
|
||||
[JsonPropertyName("error")]
|
||||
public JsonRPCError? Error { get; set; }
|
||||
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class JsonRPCResponse<T> : JsonRPCResponse
|
||||
{
|
||||
[JsonPropertyName("result")]
|
||||
public T? Result { get; set; }
|
||||
}
|
||||
}
|
||||
19
JsonRPC/RPC/AbsSingle.cs
Normal file
19
JsonRPC/RPC/AbsSingle.cs
Normal file
@ -0,0 +1,19 @@
|
||||
public abstract class AbsSingle<T> where T : class, new()
|
||||
{
|
||||
static public T Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
return SingleHolder.instance;
|
||||
}
|
||||
}
|
||||
|
||||
private static class SingleHolder
|
||||
{
|
||||
public static T instance;
|
||||
static SingleHolder()
|
||||
{
|
||||
instance = new T();
|
||||
}
|
||||
}
|
||||
}
|
||||
71
JsonRPC/RPC/Receiver.cs
Normal file
71
JsonRPC/RPC/Receiver.cs
Normal file
@ -0,0 +1,71 @@
|
||||
using JsonRPC.Protocol;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace JsonRPC.RPC
|
||||
{
|
||||
public class Receiver : AbsSingle<Receiver>
|
||||
{
|
||||
Dictionary<string, Func<JsonRPCRequest, JsonRPCResponse>> handlers = new();
|
||||
|
||||
public void RegHandler(string methodSig, Func<JsonRPCRequest, JsonRPCResponse> func)
|
||||
{
|
||||
handlers.Add(methodSig, func);
|
||||
}
|
||||
|
||||
public string OnReceive(string msg)
|
||||
{
|
||||
JsonRPCResponse response;
|
||||
var req = JsonConvert.DeserializeObject<JsonRPCRequest>(msg);
|
||||
if(req == null)
|
||||
{
|
||||
response = new JsonRPCResponse()
|
||||
{
|
||||
Error = new JsonRPCError()
|
||||
{
|
||||
Code = (int)EErrorCode.ParseError,
|
||||
Message = msg,
|
||||
Data = "invalid json."
|
||||
}
|
||||
};
|
||||
|
||||
return JsonConvert.SerializeObject(response);
|
||||
}
|
||||
|
||||
if (handlers.TryGetValue(req.Method, out var handler))
|
||||
{
|
||||
try
|
||||
{
|
||||
response = handler!(req);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
response = new JsonRPCResponse()
|
||||
{
|
||||
Id = req.Id,
|
||||
Error = new JsonRPCError()
|
||||
{
|
||||
Code = (int)EErrorCode.InternalError,
|
||||
Message = msg,
|
||||
Data = e.ToString()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
response = new JsonRPCResponse()
|
||||
{
|
||||
Id = req.Id,
|
||||
Error = new JsonRPCError()
|
||||
{
|
||||
Code = (int)EErrorCode.MethodNotFound,
|
||||
Message = msg,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return JsonConvert.SerializeObject(response);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
45
JsonRPC/RPC/Sender.cs
Normal file
45
JsonRPC/RPC/Sender.cs
Normal file
@ -0,0 +1,45 @@
|
||||
using JsonRPC.Protocol;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace JsonRPC.RPC
|
||||
{
|
||||
public class Sender : AbsSingle<Sender>
|
||||
{
|
||||
Func<string, string> sendFunc = null!;
|
||||
Action<JsonRPCError> errorFunc = null!;
|
||||
int id;
|
||||
|
||||
public int GetId()
|
||||
{
|
||||
return Interlocked.Increment(ref id);
|
||||
}
|
||||
|
||||
public void SetSendFunction(Func<string, string> send)
|
||||
{
|
||||
sendFunc = send;
|
||||
}
|
||||
|
||||
public bool HnadleResponseError(JsonRPCResponse response)
|
||||
{
|
||||
if(response.Error != null)
|
||||
{
|
||||
errorFunc?.Invoke(response.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public string Send(JsonRPCRequest request)
|
||||
{
|
||||
var reqJs = JsonConvert.SerializeObject(request);
|
||||
|
||||
return sendFunc.Invoke(reqJs);
|
||||
}
|
||||
|
||||
public T JsonDeserialize<T>(string json) where T : JsonRPCResponse
|
||||
{
|
||||
return JsonConvert.DeserializeObject<T>(json);
|
||||
}
|
||||
}
|
||||
}
|
||||
17
Test/Program.cs
Normal file
17
Test/Program.cs
Normal file
@ -0,0 +1,17 @@
|
||||
// See https://aka.ms/new-console-template for more information
|
||||
using CodeGenerator;
|
||||
using JsonRPC.Protocol;
|
||||
Console.WriteLine("Hello, World!");
|
||||
|
||||
JsonRPCCodeGenerator jsonRPCCodeGenerator = new JsonRPCCodeGenerator((s) => Console.WriteLine(s));
|
||||
jsonRPCCodeGenerator.LoadAssembly("G:\\Misc\\JsonRPC\\TestInterceptor\\bin\\Debug\\net8.0\\TestInterceptor.dll");
|
||||
jsonRPCCodeGenerator.ProcessAssembly();
|
||||
jsonRPCCodeGenerator.WriteModule();
|
||||
JsonRPC.RPC.Sender.Instance.SetSendFunction((s) =>
|
||||
{
|
||||
Console.WriteLine(s);
|
||||
return JsonRPC.RPC.Receiver.Instance.OnReceive(s);
|
||||
});
|
||||
TestInterceptor.Class1.Echo("ss");
|
||||
Console.WriteLine(TestInterceptor.Class1.Test1());
|
||||
//typeof(TestInterceptor.Class1).GetMethod("TestInterceptor_Class1__Test_", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public)?.Invoke(null, null);
|
||||
16
Test/Test.csproj
Normal file
16
Test/Test.csproj
Normal file
@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CodeGenerator\CodeGenerator.csproj" />
|
||||
<ProjectReference Include="..\JsonRPC\JsonRPC.csproj" />
|
||||
<ProjectReference Include="..\TestInterceptor\TestInterceptor.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
36
TestInterceptor/Class1.cs
Normal file
36
TestInterceptor/Class1.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using CodeGenerator;
|
||||
using JsonRPC.Protocol;
|
||||
using JsonRPC.RPC;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace TestInterceptor
|
||||
{
|
||||
public class Class1
|
||||
{
|
||||
static Class1() { Console.WriteLine("Class1 Class1"); }
|
||||
|
||||
[JsonRPC]
|
||||
public static void Test()
|
||||
{
|
||||
Console.WriteLine("static TestFunc");
|
||||
}
|
||||
|
||||
[JsonRPC]
|
||||
public static void Echo(string msg)
|
||||
{
|
||||
Console.WriteLine(msg);
|
||||
}
|
||||
|
||||
[JsonRPC]
|
||||
public static string Test1()
|
||||
{
|
||||
return "aaaaaaa";
|
||||
}
|
||||
|
||||
|
||||
public static string Test11()
|
||||
{
|
||||
return "aaaaaaa";
|
||||
}
|
||||
}
|
||||
}
|
||||
14
TestInterceptor/TestInterceptor.csproj
Normal file
14
TestInterceptor/TestInterceptor.csproj
Normal file
@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CodeGenerator\CodeGenerator.csproj" />
|
||||
<ProjectReference Include="..\JsonRPC\JsonRPC.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Loading…
x
Reference in New Issue
Block a user