16 changed files with 1034 additions and 17 deletions
@ -0,0 +1,35 @@ |
|||
namespace Lion.AbpPro.CAP; |
|||
|
|||
public static class AbpProCAPMessageExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// 尝试获取消息标头中的租户标识
|
|||
/// </summary>
|
|||
public static bool TryGetTenantId(this Message message, out Guid? tenantId) |
|||
{ |
|||
if (message.Headers.TryGetValue(AbpProCapConsts.Tenant, out string tenantStr)) |
|||
{ |
|||
if (Guid.TryParse(tenantStr, out Guid id)) |
|||
{ |
|||
tenantId = id; |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
tenantId = null; |
|||
return false; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取消息标头中的租户标识
|
|||
/// </summary>
|
|||
public static Guid? GetTenantIdOrNull(this Message message) |
|||
{ |
|||
if (message.TryGetTenantId(out Guid? tenantId)) |
|||
{ |
|||
return tenantId; |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
} |
|||
@ -0,0 +1,165 @@ |
|||
using System.ComponentModel; |
|||
using DotNetCore.CAP.Filter; |
|||
using Lion.AbpPro.CAP.Internal; |
|||
using Microsoft.Extensions.Internal; |
|||
|
|||
namespace Lion.AbpPro.CAP; |
|||
|
|||
public class AbpProCAPSubscribeInvoker |
|||
{ |
|||
private readonly ConcurrentDictionary<string, ObjectMethodExecutor> _executors; |
|||
private readonly ISerializer _serializer; |
|||
private readonly IServiceProvider _serviceProvider; |
|||
private readonly ICurrentTenant _currentTenant; |
|||
public AbpProCAPSubscribeInvoker(IServiceProvider serviceProvider, ISerializer serializer, ICurrentTenant currentTenant) |
|||
{ |
|||
_serviceProvider = serviceProvider; |
|||
_serializer = serializer; |
|||
_currentTenant = currentTenant; |
|||
_executors = new ConcurrentDictionary<string, ObjectMethodExecutor>(); |
|||
} |
|||
|
|||
public async Task<ConsumerExecutedResult> InvokeAsync(ConsumerContext context, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
cancellationToken.ThrowIfCancellationRequested(); |
|||
|
|||
var methodInfo = context.ConsumerDescriptor.MethodInfo; |
|||
var reflectedTypeHandle = methodInfo.ReflectedType!.TypeHandle.Value; |
|||
var methodHandle = methodInfo.MethodHandle.Value; |
|||
var key = $"{reflectedTypeHandle}_{methodHandle}"; |
|||
|
|||
var executor = _executors.GetOrAdd(key, |
|||
_ => ObjectMethodExecutor.Create(methodInfo, context.ConsumerDescriptor.ImplTypeInfo)); |
|||
|
|||
await using var scope = _serviceProvider.CreateAsyncScope(); |
|||
|
|||
var provider = scope.ServiceProvider; |
|||
|
|||
var obj = GetInstance(provider, context); |
|||
|
|||
var message = context.DeliverMessage; |
|||
// 租户数据可能在消息标头中
|
|||
var tenantId = message.GetTenantIdOrNull(); |
|||
var parameterDescriptors = context.ConsumerDescriptor.Parameters; |
|||
var executeParameters = new object?[parameterDescriptors.Count]; |
|||
for (var i = 0; i < parameterDescriptors.Count; i++) |
|||
{ |
|||
var parameterDescriptor = parameterDescriptors[i]; |
|||
if (parameterDescriptor.IsFromCap) |
|||
{ |
|||
executeParameters[i] = GetCapProvidedParameter(parameterDescriptor, message, cancellationToken); |
|||
} |
|||
else |
|||
{ |
|||
if (message.Value != null) |
|||
{ |
|||
// use ISerializer when reading from storage, skip other objects if not Json
|
|||
if (_serializer.IsJsonType(message.Value)) |
|||
{ |
|||
executeParameters[i] = |
|||
_serializer.Deserialize(message.Value, parameterDescriptor.ParameterType); |
|||
} |
|||
else |
|||
{ |
|||
var converter = TypeDescriptor.GetConverter(parameterDescriptor.ParameterType); |
|||
if (converter.CanConvertFrom(message.Value.GetType())) |
|||
{ |
|||
executeParameters[i] = converter.ConvertFrom(message.Value); |
|||
} |
|||
else |
|||
{ |
|||
if (parameterDescriptor.ParameterType.IsInstanceOfType(message.Value)) |
|||
executeParameters[i] = message.Value; |
|||
else |
|||
executeParameters[i] = |
|||
Convert.ChangeType(message.Value, parameterDescriptor.ParameterType); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
var filter = provider.GetService<ISubscribeFilter>(); |
|||
object? resultObj = null; |
|||
try |
|||
{ |
|||
if (filter != null) |
|||
{ |
|||
var etContext = new ExecutingContext(context, executeParameters); |
|||
await filter.OnSubscribeExecutingAsync(etContext).ConfigureAwait(false); |
|||
executeParameters = etContext.Arguments; |
|||
} |
|||
|
|||
using (_currentTenant.Change(tenantId)) |
|||
{ |
|||
resultObj = await ExecuteWithParameterAsync(executor, obj, executeParameters).ConfigureAwait(false); |
|||
} |
|||
|
|||
if (filter != null) |
|||
{ |
|||
var edContext = new ExecutedContext(context, resultObj); |
|||
await filter.OnSubscribeExecutedAsync(edContext).ConfigureAwait(false); |
|||
resultObj = edContext.Result; |
|||
} |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
if (filter != null) |
|||
{ |
|||
var exContext = new ExceptionContext(context, e); |
|||
await filter.OnSubscribeExceptionAsync(exContext).ConfigureAwait(false); |
|||
if (!exContext.ExceptionHandled) exContext.Exception.ReThrow(); |
|||
|
|||
if (exContext.Result != null) resultObj = exContext.Result; |
|||
} |
|||
else |
|||
{ |
|||
throw; |
|||
} |
|||
} |
|||
|
|||
var callbackName = message.GetCallbackName(); |
|||
if (string.IsNullOrEmpty(callbackName)) |
|||
{ |
|||
return new ConsumerExecutedResult(resultObj, message.GetId(), null, null); |
|||
} |
|||
else |
|||
{ |
|||
var capHeader = executeParameters.FirstOrDefault(x => x is CapHeader) as AbpProCapHeader; |
|||
return new ConsumerExecutedResult(resultObj, message.GetId(), callbackName, capHeader?.ResponseHeader); |
|||
} |
|||
} |
|||
|
|||
private static object GetCapProvidedParameter(ParameterDescriptor parameterDescriptor, Message message, |
|||
CancellationToken cancellationToken) |
|||
{ |
|||
if (typeof(CancellationToken).IsAssignableFrom(parameterDescriptor.ParameterType)) return cancellationToken; |
|||
|
|||
if (parameterDescriptor.ParameterType.IsAssignableFrom(typeof(CapHeader))) |
|||
return new CapHeader(message.Headers); |
|||
|
|||
throw new ArgumentException(parameterDescriptor.Name); |
|||
} |
|||
|
|||
protected virtual object GetInstance(IServiceProvider provider, ConsumerContext context) |
|||
{ |
|||
var srvType = context.ConsumerDescriptor.ServiceTypeInfo?.AsType(); |
|||
var implType = context.ConsumerDescriptor.ImplTypeInfo.AsType(); |
|||
|
|||
object? obj = null; |
|||
if (srvType != null) obj = provider.GetServices(srvType).FirstOrDefault(o => o?.GetType() == implType); |
|||
|
|||
if (obj == null) obj = ActivatorUtilities.GetServiceOrCreateInstance(provider, implType); |
|||
|
|||
return obj; |
|||
} |
|||
|
|||
private async Task<object?> ExecuteWithParameterAsync(ObjectMethodExecutor executor, object @class, |
|||
object?[] parameter) |
|||
{ |
|||
if (executor.IsMethodAsync) return await executor.ExecuteAsync(@class, parameter); |
|||
|
|||
return executor.Execute(@class, parameter); |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
namespace Lion.AbpPro.CAP; |
|||
|
|||
public class AbpProCapConsts |
|||
{ |
|||
/// <summary>
|
|||
/// 租户字符串
|
|||
/// </summary>
|
|||
public const string Tenant = "__tenant"; |
|||
} |
|||
@ -0,0 +1,41 @@ |
|||
using System.Collections.ObjectModel; |
|||
|
|||
namespace Lion.AbpPro.CAP.Internal; |
|||
|
|||
|
|||
public class AbpProCapHeader : ReadOnlyDictionary<string, string> |
|||
{ |
|||
internal IDictionary<string, string?>? ResponseHeader { get; set; } |
|||
|
|||
public AbpProCapHeader(IDictionary<string, string?> dictionary) : base(dictionary) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// When a callbackName is specified from publish message, use this method to add an additional header.
|
|||
/// </summary>
|
|||
/// <param name="key">The response header key.</param>
|
|||
/// <param name="value">The response header value.</param>
|
|||
public void AddResponseHeader(string key, string? value) |
|||
{ |
|||
ResponseHeader ??= new Dictionary<string, string?>(); |
|||
ResponseHeader[key] = value; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// When a callbackName is specified from publish message, use this method to abort the callback.
|
|||
/// </summary>
|
|||
public void RemoveCallback() |
|||
{ |
|||
Dictionary.Remove(Headers.CallbackName); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// When a callbackName is specified from Publish message, use this method to rewrite the callback name.
|
|||
/// </summary>
|
|||
/// <param name="callbackName">The new callback name.</param>
|
|||
public void RewriteCallback(string callbackName) |
|||
{ |
|||
Dictionary[Headers.CallbackName] = callbackName; |
|||
} |
|||
} |
|||
@ -0,0 +1,118 @@ |
|||
// Licensed to the .NET Foundation under one or more agreements.
|
|||
// The .NET Foundation licenses this file to you under the MIT license.
|
|||
|
|||
#nullable disable |
|||
|
|||
using System; |
|||
using System.Linq; |
|||
using System.Reflection; |
|||
using System.Runtime.CompilerServices; |
|||
|
|||
namespace Microsoft.Extensions.Internal; |
|||
|
|||
internal readonly struct AwaitableInfo |
|||
{ |
|||
public Type AwaiterType { get; } |
|||
public PropertyInfo AwaiterIsCompletedProperty { get; } |
|||
public MethodInfo AwaiterGetResultMethod { get; } |
|||
public MethodInfo AwaiterOnCompletedMethod { get; } |
|||
public MethodInfo AwaiterUnsafeOnCompletedMethod { get; } |
|||
public Type ResultType { get; } |
|||
public MethodInfo GetAwaiterMethod { get; } |
|||
|
|||
public AwaitableInfo( |
|||
Type awaiterType, |
|||
PropertyInfo awaiterIsCompletedProperty, |
|||
MethodInfo awaiterGetResultMethod, |
|||
MethodInfo awaiterOnCompletedMethod, |
|||
MethodInfo awaiterUnsafeOnCompletedMethod, |
|||
Type resultType, |
|||
MethodInfo getAwaiterMethod) |
|||
{ |
|||
AwaiterType = awaiterType; |
|||
AwaiterIsCompletedProperty = awaiterIsCompletedProperty; |
|||
AwaiterGetResultMethod = awaiterGetResultMethod; |
|||
AwaiterOnCompletedMethod = awaiterOnCompletedMethod; |
|||
AwaiterUnsafeOnCompletedMethod = awaiterUnsafeOnCompletedMethod; |
|||
ResultType = resultType; |
|||
GetAwaiterMethod = getAwaiterMethod; |
|||
} |
|||
|
|||
public static bool IsTypeAwaitable(Type type, out AwaitableInfo awaitableInfo) |
|||
{ |
|||
// Based on Roslyn code: http://source.roslyn.io/#Microsoft.CodeAnalysis.Workspaces/Shared/Extensions/ISymbolExtensions.cs,db4d48ba694b9347
|
|||
|
|||
// Awaitable must have method matching "object GetAwaiter()"
|
|||
var getAwaiterMethod = type.GetRuntimeMethods().FirstOrDefault(m => |
|||
m.Name.Equals("GetAwaiter", StringComparison.OrdinalIgnoreCase) |
|||
&& m.GetParameters().Length == 0 |
|||
&& m.ReturnType != null); |
|||
if (getAwaiterMethod == null) |
|||
{ |
|||
awaitableInfo = default; |
|||
return false; |
|||
} |
|||
|
|||
var awaiterType = getAwaiterMethod.ReturnType; |
|||
|
|||
// Awaiter must have property matching "bool IsCompleted { get; }"
|
|||
var isCompletedProperty = awaiterType.GetRuntimeProperties().FirstOrDefault(p => |
|||
p.Name.Equals("IsCompleted", StringComparison.OrdinalIgnoreCase) |
|||
&& p.PropertyType == typeof(bool) |
|||
&& p.GetMethod != null); |
|||
if (isCompletedProperty == null) |
|||
{ |
|||
awaitableInfo = default; |
|||
return false; |
|||
} |
|||
|
|||
// Awaiter must implement INotifyCompletion
|
|||
var awaiterInterfaces = awaiterType.GetInterfaces(); |
|||
var implementsINotifyCompletion = awaiterInterfaces.Any(t => t == typeof(INotifyCompletion)); |
|||
if (!implementsINotifyCompletion) |
|||
{ |
|||
awaitableInfo = default; |
|||
return false; |
|||
} |
|||
|
|||
// INotifyCompletion supplies a method matching "void OnCompleted(Action action)"
|
|||
var onCompletedMethod = typeof(INotifyCompletion).GetRuntimeMethods().Single(m => |
|||
m.Name.Equals("OnCompleted", StringComparison.OrdinalIgnoreCase) |
|||
&& m.ReturnType == typeof(void) |
|||
&& m.GetParameters().Length == 1 |
|||
&& m.GetParameters()[0].ParameterType == typeof(Action)); |
|||
|
|||
// Awaiter optionally implements ICriticalNotifyCompletion
|
|||
var implementsICriticalNotifyCompletion = awaiterInterfaces.Any(t => t == typeof(ICriticalNotifyCompletion)); |
|||
MethodInfo unsafeOnCompletedMethod; |
|||
if (implementsICriticalNotifyCompletion) |
|||
// ICriticalNotifyCompletion supplies a method matching "void UnsafeOnCompleted(Action action)"
|
|||
unsafeOnCompletedMethod = typeof(ICriticalNotifyCompletion).GetRuntimeMethods().Single(m => |
|||
m.Name.Equals("UnsafeOnCompleted", StringComparison.OrdinalIgnoreCase) |
|||
&& m.ReturnType == typeof(void) |
|||
&& m.GetParameters().Length == 1 |
|||
&& m.GetParameters()[0].ParameterType == typeof(Action)); |
|||
else |
|||
unsafeOnCompletedMethod = null; |
|||
|
|||
// Awaiter must have method matching "void GetResult" or "T GetResult()"
|
|||
var getResultMethod = awaiterType.GetRuntimeMethods().FirstOrDefault(m => |
|||
m.Name.Equals("GetResult") |
|||
&& m.GetParameters().Length == 0); |
|||
if (getResultMethod == null) |
|||
{ |
|||
awaitableInfo = default; |
|||
return false; |
|||
} |
|||
|
|||
awaitableInfo = new AwaitableInfo( |
|||
awaiterType, |
|||
isCompletedProperty, |
|||
getResultMethod, |
|||
onCompletedMethod, |
|||
unsafeOnCompletedMethod, |
|||
getResultMethod.ReturnType, |
|||
getAwaiterMethod); |
|||
return true; |
|||
} |
|||
} |
|||
@ -0,0 +1,55 @@ |
|||
// Licensed to the .NET Foundation under one or more agreements.
|
|||
// The .NET Foundation licenses this file to you under the MIT license.
|
|||
|
|||
#nullable disable |
|||
|
|||
using System; |
|||
using System.Linq.Expressions; |
|||
|
|||
namespace Microsoft.Extensions.Internal; |
|||
|
|||
internal readonly struct CoercedAwaitableInfo |
|||
{ |
|||
public AwaitableInfo AwaitableInfo { get; } |
|||
public Expression CoercerExpression { get; } |
|||
public Type CoercerResultType { get; } |
|||
public bool RequiresCoercion => CoercerExpression != null; |
|||
|
|||
public CoercedAwaitableInfo(AwaitableInfo awaitableInfo) |
|||
{ |
|||
AwaitableInfo = awaitableInfo; |
|||
CoercerExpression = null; |
|||
CoercerResultType = null; |
|||
} |
|||
|
|||
public CoercedAwaitableInfo(Expression coercerExpression, Type coercerResultType, |
|||
AwaitableInfo coercedAwaitableInfo) |
|||
{ |
|||
CoercerExpression = coercerExpression; |
|||
CoercerResultType = coercerResultType; |
|||
AwaitableInfo = coercedAwaitableInfo; |
|||
} |
|||
|
|||
public static bool IsTypeAwaitable(Type type, out CoercedAwaitableInfo info) |
|||
{ |
|||
if (AwaitableInfo.IsTypeAwaitable(type, out var directlyAwaitableInfo)) |
|||
{ |
|||
info = new CoercedAwaitableInfo(directlyAwaitableInfo); |
|||
return true; |
|||
} |
|||
|
|||
// It's not directly awaitable, but maybe we can coerce it.
|
|||
// Currently we support coercing FSharpAsync<T>.
|
|||
if (ObjectMethodExecutorFSharpSupport.TryBuildCoercerFromFSharpAsyncToAwaitable(type, |
|||
out var coercerExpression, |
|||
out var coercerResultType)) |
|||
if (AwaitableInfo.IsTypeAwaitable(coercerResultType, out var coercedAwaitableInfo)) |
|||
{ |
|||
info = new CoercedAwaitableInfo(coercerExpression, coercerResultType, coercedAwaitableInfo); |
|||
return true; |
|||
} |
|||
|
|||
info = default; |
|||
return false; |
|||
} |
|||
} |
|||
@ -0,0 +1,329 @@ |
|||
// Licensed to the .NET Foundation under one or more agreements.
|
|||
// The .NET Foundation licenses this file to you under the MIT license.
|
|||
|
|||
#nullable enable |
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics; |
|||
using System.Linq.Expressions; |
|||
using System.Reflection; |
|||
|
|||
namespace Microsoft.Extensions.Internal; |
|||
|
|||
internal class ObjectMethodExecutor |
|||
{ |
|||
private static readonly ConstructorInfo _objectMethodExecutorAwaitableConstructor = |
|||
typeof(ObjectMethodExecutorAwaitable).GetConstructor(new[] |
|||
{ |
|||
typeof(object), // customAwaitable
|
|||
typeof(Func<object, object>), // getAwaiterMethod
|
|||
typeof(Func<object, bool>), // isCompletedMethod
|
|||
typeof(Func<object, object>), // getResultMethod
|
|||
typeof(Action<object, Action>), // onCompletedMethod
|
|||
typeof(Action<object, Action>) // unsafeOnCompletedMethod
|
|||
})!; |
|||
|
|||
private readonly MethodExecutor? _executor; |
|||
private readonly MethodExecutorAsync? _executorAsync; |
|||
private readonly object?[]? _parameterDefaultValues; |
|||
|
|||
private ObjectMethodExecutor(MethodInfo methodInfo, TypeInfo targetTypeInfo, object?[]? parameterDefaultValues) |
|||
{ |
|||
if (methodInfo == null) throw new ArgumentNullException(nameof(methodInfo)); |
|||
|
|||
MethodInfo = methodInfo; |
|||
MethodParameters = methodInfo.GetParameters(); |
|||
TargetTypeInfo = targetTypeInfo; |
|||
MethodReturnType = methodInfo.ReturnType; |
|||
|
|||
var isAwaitable = CoercedAwaitableInfo.IsTypeAwaitable(MethodReturnType, out var coercedAwaitableInfo); |
|||
|
|||
IsMethodAsync = isAwaitable; |
|||
AsyncResultType = isAwaitable ? coercedAwaitableInfo.AwaitableInfo.ResultType : null; |
|||
|
|||
// Upstream code may prefer to use the sync-executor even for async methods, because if it knows
|
|||
// that the result is a specific Task<T> where T is known, then it can directly cast to that type
|
|||
// and await it without the extra heap allocations involved in the _executorAsync code path.
|
|||
_executor = GetExecutor(methodInfo, targetTypeInfo); |
|||
|
|||
if (IsMethodAsync) _executorAsync = GetExecutorAsync(methodInfo, targetTypeInfo, coercedAwaitableInfo); |
|||
|
|||
_parameterDefaultValues = parameterDefaultValues; |
|||
} |
|||
|
|||
public MethodInfo MethodInfo { get; } |
|||
|
|||
public ParameterInfo[] MethodParameters { get; } |
|||
|
|||
public TypeInfo TargetTypeInfo { get; } |
|||
|
|||
public Type? AsyncResultType { get; } |
|||
|
|||
// This field is made internal set because it is set in unit tests.
|
|||
public Type MethodReturnType { get; internal set; } |
|||
|
|||
public bool IsMethodAsync { get; } |
|||
|
|||
public static ObjectMethodExecutor Create(MethodInfo methodInfo, TypeInfo targetTypeInfo) |
|||
{ |
|||
return new ObjectMethodExecutor(methodInfo, targetTypeInfo, null); |
|||
} |
|||
|
|||
public static ObjectMethodExecutor Create(MethodInfo methodInfo, TypeInfo targetTypeInfo, |
|||
object?[] parameterDefaultValues) |
|||
{ |
|||
if (parameterDefaultValues == null) throw new ArgumentNullException(nameof(parameterDefaultValues)); |
|||
|
|||
return new ObjectMethodExecutor(methodInfo, targetTypeInfo, parameterDefaultValues); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Executes the configured method on <paramref name="target" />. This can be used whether or not
|
|||
/// the configured method is asynchronous.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Even if the target method is asynchronous, it's desirable to invoke it using Execute rather than
|
|||
/// ExecuteAsync if you know at compile time what the return type is, because then you can directly
|
|||
/// "await" that value (via a cast), and then the generated code will be able to reference the
|
|||
/// resulting awaitable as a value-typed variable. If you use ExecuteAsync instead, the generated
|
|||
/// code will have to treat the resulting awaitable as a boxed object, because it doesn't know at
|
|||
/// compile time what type it would be.
|
|||
/// </remarks>
|
|||
/// <param name="target">The object whose method is to be executed.</param>
|
|||
/// <param name="parameters">Parameters to pass to the method.</param>
|
|||
/// <returns>The method return value.</returns>
|
|||
public object? Execute(object target, object?[]? parameters) |
|||
{ |
|||
Debug.Assert(_executor != null, "Sync execution is not supported."); |
|||
return _executor(target, parameters); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Executes the configured method on <paramref name="target" />. This can only be used if the configured
|
|||
/// method is asynchronous.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// If you don't know at compile time the type of the method's returned awaitable, you can use ExecuteAsync,
|
|||
/// which supplies an awaitable-of-object. This always works, but can incur several extra heap allocations
|
|||
/// as compared with using Execute and then using "await" on the result value typecasted to the known
|
|||
/// awaitable type. The possible extra heap allocations are for:
|
|||
/// 1. The custom awaitable (though usually there's a heap allocation for this anyway, since normally
|
|||
/// it's a reference type, and you normally create a new instance per call).
|
|||
/// 2. The custom awaiter (whether or not it's a value type, since if it's not, you need a new instance
|
|||
/// of it, and if it is, it will have to be boxed so the calling code can reference it as an object).
|
|||
/// 3. The async result value, if it's a value type (it has to be boxed as an object, since the calling
|
|||
/// code doesn't know what type it's going to be).
|
|||
/// </remarks>
|
|||
/// <param name="target">The object whose method is to be executed.</param>
|
|||
/// <param name="parameters">Parameters to pass to the method.</param>
|
|||
/// <returns>An object that you can "await" to get the method return value.</returns>
|
|||
public ObjectMethodExecutorAwaitable ExecuteAsync(object target, object?[]? parameters) |
|||
{ |
|||
Debug.Assert(_executorAsync != null, "Async execution is not supported."); |
|||
return _executorAsync(target, parameters); |
|||
} |
|||
|
|||
public object? GetDefaultValueForParameter(int index) |
|||
{ |
|||
if (_parameterDefaultValues == null) |
|||
throw new InvalidOperationException( |
|||
$"Cannot call {nameof(GetDefaultValueForParameter)}, because no parameter default values were supplied."); |
|||
|
|||
if (index < 0 || index > MethodParameters.Length - 1) throw new ArgumentOutOfRangeException(nameof(index)); |
|||
|
|||
return _parameterDefaultValues[index]; |
|||
} |
|||
|
|||
private static MethodExecutor GetExecutor(MethodInfo methodInfo, TypeInfo targetTypeInfo) |
|||
{ |
|||
// Parameters to executor
|
|||
var targetParameter = Expression.Parameter(typeof(object), "target"); |
|||
var parametersParameter = Expression.Parameter(typeof(object?[]), "parameters"); |
|||
|
|||
// Build parameter list
|
|||
var paramInfos = methodInfo.GetParameters(); |
|||
var parameters = new List<Expression>(paramInfos.Length); |
|||
for (var i = 0; i < paramInfos.Length; i++) |
|||
{ |
|||
var paramInfo = paramInfos[i]; |
|||
var valueObj = Expression.ArrayIndex(parametersParameter, Expression.Constant(i)); |
|||
var valueCast = Expression.Convert(valueObj, paramInfo.ParameterType); |
|||
|
|||
// valueCast is "(Ti) parameters[i]"
|
|||
parameters.Add(valueCast); |
|||
} |
|||
|
|||
// Call method
|
|||
var instanceCast = Expression.Convert(targetParameter, targetTypeInfo.AsType()); |
|||
var methodCall = Expression.Call(instanceCast, methodInfo, parameters); |
|||
|
|||
// methodCall is "((Ttarget) target) method((T0) parameters[0], (T1) parameters[1], ...)"
|
|||
// Create function
|
|||
if (methodCall.Type == typeof(void)) |
|||
{ |
|||
var lambda = Expression.Lambda<VoidMethodExecutor>(methodCall, targetParameter, parametersParameter); |
|||
var voidExecutor = lambda.Compile(); |
|||
return WrapVoidMethod(voidExecutor); |
|||
} |
|||
else |
|||
{ |
|||
// must coerce methodCall to match ActionExecutor signature
|
|||
var castMethodCall = Expression.Convert(methodCall, typeof(object)); |
|||
var lambda = Expression.Lambda<MethodExecutor>(castMethodCall, targetParameter, parametersParameter); |
|||
return lambda.Compile(); |
|||
} |
|||
} |
|||
|
|||
private static MethodExecutor WrapVoidMethod(VoidMethodExecutor executor) |
|||
{ |
|||
return delegate(object target, object?[]? parameters) |
|||
{ |
|||
executor(target, parameters); |
|||
return null; |
|||
}; |
|||
} |
|||
|
|||
private static MethodExecutorAsync GetExecutorAsync( |
|||
MethodInfo methodInfo, |
|||
TypeInfo targetTypeInfo, |
|||
CoercedAwaitableInfo coercedAwaitableInfo) |
|||
{ |
|||
// Parameters to executor
|
|||
var targetParameter = Expression.Parameter(typeof(object), "target"); |
|||
var parametersParameter = Expression.Parameter(typeof(object[]), "parameters"); |
|||
|
|||
// Build parameter list
|
|||
var paramInfos = methodInfo.GetParameters(); |
|||
var parameters = new List<Expression>(paramInfos.Length); |
|||
for (var i = 0; i < paramInfos.Length; i++) |
|||
{ |
|||
var paramInfo = paramInfos[i]; |
|||
var valueObj = Expression.ArrayIndex(parametersParameter, Expression.Constant(i)); |
|||
var valueCast = Expression.Convert(valueObj, paramInfo.ParameterType); |
|||
|
|||
// valueCast is "(Ti) parameters[i]"
|
|||
parameters.Add(valueCast); |
|||
} |
|||
|
|||
// Call method
|
|||
var instanceCast = Expression.Convert(targetParameter, targetTypeInfo.AsType()); |
|||
var methodCall = Expression.Call(instanceCast, methodInfo, parameters); |
|||
|
|||
// Using the method return value, construct an ObjectMethodExecutorAwaitable based on
|
|||
// the info we have about its implementation of the awaitable pattern. Note that all
|
|||
// the funcs/actions we construct here are precompiled, so that only one instance of
|
|||
// each is preserved throughout the lifetime of the ObjectMethodExecutor.
|
|||
|
|||
// var getAwaiterFunc = (object awaitable) =>
|
|||
// (object)((CustomAwaitableType)awaitable).GetAwaiter();
|
|||
var customAwaitableParam = Expression.Parameter(typeof(object), "awaitable"); |
|||
var awaitableInfo = coercedAwaitableInfo.AwaitableInfo; |
|||
var postCoercionMethodReturnType = coercedAwaitableInfo.CoercerResultType ?? methodInfo.ReturnType; |
|||
var getAwaiterFunc = Expression.Lambda<Func<object, object>>( |
|||
Expression.Convert( |
|||
Expression.Call( |
|||
Expression.Convert(customAwaitableParam, postCoercionMethodReturnType), |
|||
awaitableInfo.GetAwaiterMethod), |
|||
typeof(object)), |
|||
customAwaitableParam).Compile(); |
|||
|
|||
// var isCompletedFunc = (object awaiter) =>
|
|||
// ((CustomAwaiterType)awaiter).IsCompleted;
|
|||
var isCompletedParam = Expression.Parameter(typeof(object), "awaiter"); |
|||
var isCompletedFunc = Expression.Lambda<Func<object, bool>>( |
|||
Expression.MakeMemberAccess( |
|||
Expression.Convert(isCompletedParam, awaitableInfo.AwaiterType), |
|||
awaitableInfo.AwaiterIsCompletedProperty), |
|||
isCompletedParam).Compile(); |
|||
|
|||
var getResultParam = Expression.Parameter(typeof(object), "awaiter"); |
|||
Func<object, object> getResultFunc; |
|||
if (awaitableInfo.ResultType == typeof(void)) |
|||
// var getResultFunc = (object awaiter) =>
|
|||
// {
|
|||
// ((CustomAwaiterType)awaiter).GetResult(); // We need to invoke this to surface any exceptions
|
|||
// return (object)null;
|
|||
// };
|
|||
getResultFunc = Expression.Lambda<Func<object, object>>( |
|||
Expression.Block( |
|||
Expression.Call( |
|||
Expression.Convert(getResultParam, awaitableInfo.AwaiterType), |
|||
awaitableInfo.AwaiterGetResultMethod), |
|||
Expression.Constant(null) |
|||
), |
|||
getResultParam).Compile(); |
|||
else |
|||
// var getResultFunc = (object awaiter) =>
|
|||
// (object)((CustomAwaiterType)awaiter).GetResult();
|
|||
getResultFunc = Expression.Lambda<Func<object, object>>( |
|||
Expression.Convert( |
|||
Expression.Call( |
|||
Expression.Convert(getResultParam, awaitableInfo.AwaiterType), |
|||
awaitableInfo.AwaiterGetResultMethod), |
|||
typeof(object)), |
|||
getResultParam).Compile(); |
|||
|
|||
// var onCompletedFunc = (object awaiter, Action continuation) => {
|
|||
// ((CustomAwaiterType)awaiter).OnCompleted(continuation);
|
|||
// };
|
|||
var onCompletedParam1 = Expression.Parameter(typeof(object), "awaiter"); |
|||
var onCompletedParam2 = Expression.Parameter(typeof(Action), "continuation"); |
|||
var onCompletedFunc = Expression.Lambda<Action<object, Action>>( |
|||
Expression.Call( |
|||
Expression.Convert(onCompletedParam1, awaitableInfo.AwaiterType), |
|||
awaitableInfo.AwaiterOnCompletedMethod, |
|||
onCompletedParam2), |
|||
onCompletedParam1, |
|||
onCompletedParam2).Compile(); |
|||
|
|||
Action<object, Action>? unsafeOnCompletedFunc = null; |
|||
if (awaitableInfo.AwaiterUnsafeOnCompletedMethod != null) |
|||
{ |
|||
// var unsafeOnCompletedFunc = (object awaiter, Action continuation) => {
|
|||
// ((CustomAwaiterType)awaiter).UnsafeOnCompleted(continuation);
|
|||
// };
|
|||
var unsafeOnCompletedParam1 = Expression.Parameter(typeof(object), "awaiter"); |
|||
var unsafeOnCompletedParam2 = Expression.Parameter(typeof(Action), "continuation"); |
|||
unsafeOnCompletedFunc = Expression.Lambda<Action<object, Action>>( |
|||
Expression.Call( |
|||
Expression.Convert(unsafeOnCompletedParam1, awaitableInfo.AwaiterType), |
|||
awaitableInfo.AwaiterUnsafeOnCompletedMethod, |
|||
unsafeOnCompletedParam2), |
|||
unsafeOnCompletedParam1, |
|||
unsafeOnCompletedParam2).Compile(); |
|||
} |
|||
|
|||
// If we need to pass the method call result through a coercer function to get an
|
|||
// awaitable, then do so.
|
|||
var coercedMethodCall = coercedAwaitableInfo.RequiresCoercion |
|||
? Expression.Invoke(coercedAwaitableInfo.CoercerExpression, methodCall) |
|||
: (Expression)methodCall; |
|||
|
|||
// return new ObjectMethodExecutorAwaitable(
|
|||
// (object)coercedMethodCall,
|
|||
// getAwaiterFunc,
|
|||
// isCompletedFunc,
|
|||
// getResultFunc,
|
|||
// onCompletedFunc,
|
|||
// unsafeOnCompletedFunc);
|
|||
var returnValueExpression = Expression.New( |
|||
_objectMethodExecutorAwaitableConstructor, |
|||
Expression.Convert(coercedMethodCall, typeof(object)), |
|||
Expression.Constant(getAwaiterFunc), |
|||
Expression.Constant(isCompletedFunc), |
|||
Expression.Constant(getResultFunc), |
|||
Expression.Constant(onCompletedFunc), |
|||
Expression.Constant(unsafeOnCompletedFunc, typeof(Action<object, Action>))); |
|||
|
|||
var lambda = |
|||
Expression.Lambda<MethodExecutorAsync>(returnValueExpression, targetParameter, parametersParameter); |
|||
return lambda.Compile(); |
|||
} |
|||
|
|||
private delegate ObjectMethodExecutorAwaitable MethodExecutorAsync(object target, object?[]? parameters); |
|||
|
|||
private delegate object? MethodExecutor(object target, object?[]? parameters); |
|||
|
|||
private delegate void VoidMethodExecutor(object target, object?[]? parameters); |
|||
} |
|||
@ -0,0 +1,119 @@ |
|||
// Licensed to the .NET Foundation under one or more agreements.
|
|||
// The .NET Foundation licenses this file to you under the MIT license.
|
|||
|
|||
#nullable disable |
|||
|
|||
using System; |
|||
using System.Runtime.CompilerServices; |
|||
|
|||
namespace Microsoft.Extensions.Internal; |
|||
|
|||
/// <summary>
|
|||
/// Provides a common awaitable structure that <see cref="ObjectMethodExecutor.ExecuteAsync" /> can
|
|||
/// return, regardless of whether the underlying value is a System.Task, an FSharpAsync, or an
|
|||
/// application-defined custom awaitable.
|
|||
/// </summary>
|
|||
internal readonly struct ObjectMethodExecutorAwaitable |
|||
{ |
|||
private readonly object _customAwaitable; |
|||
private readonly Func<object, object> _getAwaiterMethod; |
|||
private readonly Func<object, bool> _isCompletedMethod; |
|||
private readonly Func<object, object> _getResultMethod; |
|||
private readonly Action<object, Action> _onCompletedMethod; |
|||
private readonly Action<object, Action> _unsafeOnCompletedMethod; |
|||
|
|||
// Perf note: since we're requiring the customAwaitable to be supplied here as an object,
|
|||
// this will trigger a further allocation if it was a value type (i.e., to box it). We can't
|
|||
// fix this by making the customAwaitable type generic, because the calling code typically
|
|||
// does not know the type of the awaitable/awaiter at compile-time anyway.
|
|||
//
|
|||
// However, we could fix it by not passing the customAwaitable here at all, and instead
|
|||
// passing a func that maps directly from the target object (e.g., controller instance),
|
|||
// target method (e.g., action method info), and params array to the custom awaiter in the
|
|||
// GetAwaiter() method below. In effect, by delaying the actual method call until the
|
|||
// upstream code calls GetAwaiter on this ObjectMethodExecutorAwaitable instance.
|
|||
// This optimization is not currently implemented because:
|
|||
// [1] It would make no difference when the awaitable was an object type, which is
|
|||
// by far the most common scenario (e.g., System.Task<T>).
|
|||
// [2] It would be complex - we'd need some kind of object pool to track all the parameter
|
|||
// arrays until we needed to use them in GetAwaiter().
|
|||
// We can reconsider this in the future if there's a need to optimize for ValueTask<T>
|
|||
// or other value-typed awaitables.
|
|||
|
|||
public ObjectMethodExecutorAwaitable( |
|||
object customAwaitable, |
|||
Func<object, object> getAwaiterMethod, |
|||
Func<object, bool> isCompletedMethod, |
|||
Func<object, object> getResultMethod, |
|||
Action<object, Action> onCompletedMethod, |
|||
Action<object, Action> unsafeOnCompletedMethod) |
|||
{ |
|||
_customAwaitable = customAwaitable; |
|||
_getAwaiterMethod = getAwaiterMethod; |
|||
_isCompletedMethod = isCompletedMethod; |
|||
_getResultMethod = getResultMethod; |
|||
_onCompletedMethod = onCompletedMethod; |
|||
_unsafeOnCompletedMethod = unsafeOnCompletedMethod; |
|||
} |
|||
|
|||
public Awaiter GetAwaiter() |
|||
{ |
|||
var customAwaiter = _getAwaiterMethod(_customAwaitable); |
|||
return new Awaiter(customAwaiter, _isCompletedMethod, _getResultMethod, _onCompletedMethod, |
|||
_unsafeOnCompletedMethod); |
|||
} |
|||
|
|||
public readonly struct Awaiter : ICriticalNotifyCompletion |
|||
{ |
|||
private readonly object _customAwaiter; |
|||
private readonly Func<object, bool> _isCompletedMethod; |
|||
private readonly Func<object, object> _getResultMethod; |
|||
private readonly Action<object, Action> _onCompletedMethod; |
|||
private readonly Action<object, Action> _unsafeOnCompletedMethod; |
|||
|
|||
public Awaiter( |
|||
object customAwaiter, |
|||
Func<object, bool> isCompletedMethod, |
|||
Func<object, object> getResultMethod, |
|||
Action<object, Action> onCompletedMethod, |
|||
Action<object, Action> unsafeOnCompletedMethod) |
|||
{ |
|||
_customAwaiter = customAwaiter; |
|||
_isCompletedMethod = isCompletedMethod; |
|||
_getResultMethod = getResultMethod; |
|||
_onCompletedMethod = onCompletedMethod; |
|||
_unsafeOnCompletedMethod = unsafeOnCompletedMethod; |
|||
} |
|||
|
|||
public bool IsCompleted => _isCompletedMethod(_customAwaiter); |
|||
|
|||
public object GetResult() |
|||
{ |
|||
return _getResultMethod(_customAwaiter); |
|||
} |
|||
|
|||
public void OnCompleted(Action continuation) |
|||
{ |
|||
_onCompletedMethod(_customAwaiter, continuation); |
|||
} |
|||
|
|||
public void UnsafeOnCompleted(Action continuation) |
|||
{ |
|||
// If the underlying awaitable implements ICriticalNotifyCompletion, use its UnsafeOnCompleted.
|
|||
// If not, fall back on using its OnCompleted.
|
|||
//
|
|||
// Why this is safe:
|
|||
// - Implementing ICriticalNotifyCompletion is a way of saying the caller can choose whether it
|
|||
// needs the execution context to be preserved (which it signals by calling OnCompleted), or
|
|||
// that it doesn't (which it signals by calling UnsafeOnCompleted). Obviously it's faster *not*
|
|||
// to preserve and restore the context, so we prefer that where possible.
|
|||
// - If a caller doesn't need the execution context to be preserved and hence calls UnsafeOnCompleted,
|
|||
// there's no harm in preserving it anyway - it's just a bit of wasted cost. That's what will happen
|
|||
// if a caller sees that the proxy implements ICriticalNotifyCompletion but the proxy chooses to
|
|||
// pass the call on to the underlying awaitable's OnCompleted method.
|
|||
|
|||
var underlyingMethodToUse = _unsafeOnCompletedMethod ?? _onCompletedMethod; |
|||
underlyingMethodToUse(_customAwaiter, continuation); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,140 @@ |
|||
// Licensed to the .NET Foundation under one or more agreements.
|
|||
// The .NET Foundation licenses this file to you under the MIT license.
|
|||
|
|||
#nullable disable |
|||
|
|||
using System; |
|||
using System.Linq; |
|||
using System.Linq.Expressions; |
|||
using System.Reflection; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Microsoft.Extensions.Internal; |
|||
|
|||
/// <summary>
|
|||
/// Helper for detecting whether a given type is FSharpAsync`1, and if so, supplying
|
|||
/// an <see cref="Expression" /> for mapping instances of that type to a C# awaitable.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// The main design goal here is to avoid taking a compile-time dependency on
|
|||
/// FSharp.Core.dll, because non-F# applications wouldn't use it. So all the references
|
|||
/// to FSharp types have to be constructed dynamically at runtime.
|
|||
/// </remarks>
|
|||
internal static class ObjectMethodExecutorFSharpSupport |
|||
{ |
|||
private static readonly object _fsharpValuesCacheLock = new(); |
|||
private static Assembly _fsharpCoreAssembly; |
|||
private static MethodInfo _fsharpAsyncStartAsTaskGenericMethod; |
|||
private static PropertyInfo _fsharpOptionOfTaskCreationOptionsNoneProperty; |
|||
private static PropertyInfo _fsharpOptionOfCancellationTokenNoneProperty; |
|||
|
|||
public static bool TryBuildCoercerFromFSharpAsyncToAwaitable( |
|||
Type possibleFSharpAsyncType, |
|||
out Expression coerceToAwaitableExpression, |
|||
out Type awaitableType) |
|||
{ |
|||
var methodReturnGenericType = possibleFSharpAsyncType.IsGenericType |
|||
? possibleFSharpAsyncType.GetGenericTypeDefinition() |
|||
: null; |
|||
|
|||
if (!IsFSharpAsyncOpenGenericType(methodReturnGenericType)) |
|||
{ |
|||
coerceToAwaitableExpression = null; |
|||
awaitableType = null; |
|||
return false; |
|||
} |
|||
|
|||
var awaiterResultType = possibleFSharpAsyncType.GetGenericArguments().Single(); |
|||
awaitableType = typeof(Task<>).MakeGenericType(awaiterResultType); |
|||
|
|||
// coerceToAwaitableExpression = (object fsharpAsync) =>
|
|||
// {
|
|||
// return (object)FSharpAsync.StartAsTask<TResult>(
|
|||
// (Microsoft.FSharp.Control.FSharpAsync<TResult>)fsharpAsync,
|
|||
// FSharpOption<TaskCreationOptions>.None,
|
|||
// FSharpOption<CancellationToken>.None);
|
|||
// };
|
|||
var startAsTaskClosedMethod = _fsharpAsyncStartAsTaskGenericMethod |
|||
.MakeGenericMethod(awaiterResultType); |
|||
var coerceToAwaitableParam = Expression.Parameter(typeof(object)); |
|||
coerceToAwaitableExpression = Expression.Lambda( |
|||
Expression.Convert( |
|||
Expression.Call( |
|||
startAsTaskClosedMethod, |
|||
Expression.Convert(coerceToAwaitableParam, possibleFSharpAsyncType), |
|||
Expression.MakeMemberAccess(null, _fsharpOptionOfTaskCreationOptionsNoneProperty), |
|||
Expression.MakeMemberAccess(null, _fsharpOptionOfCancellationTokenNoneProperty)), |
|||
typeof(object)), |
|||
coerceToAwaitableParam); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
private static bool IsFSharpAsyncOpenGenericType(Type possibleFSharpAsyncGenericType) |
|||
{ |
|||
var typeFullName = possibleFSharpAsyncGenericType?.FullName; |
|||
if (!string.Equals(typeFullName, "Microsoft.FSharp.Control.FSharpAsync`1", StringComparison.Ordinal)) |
|||
return false; |
|||
|
|||
lock (_fsharpValuesCacheLock) |
|||
{ |
|||
if (_fsharpCoreAssembly != null) |
|||
// Since we've already found the real FSharpAsync.Core assembly, we just have
|
|||
// to check that the supplied FSharpAsync`1 type is the one from that assembly.
|
|||
return possibleFSharpAsyncGenericType.Assembly == _fsharpCoreAssembly; |
|||
// We'll keep trying to find the FSharp types/values each time any type called
|
|||
// FSharpAsync`1 is supplied.
|
|||
return TryPopulateFSharpValueCaches(possibleFSharpAsyncGenericType); |
|||
} |
|||
} |
|||
|
|||
private static bool TryPopulateFSharpValueCaches(Type possibleFSharpAsyncGenericType) |
|||
{ |
|||
var assembly = possibleFSharpAsyncGenericType.Assembly; |
|||
var fsharpOptionType = assembly.GetType("Microsoft.FSharp.Core.FSharpOption`1"); |
|||
var fsharpAsyncType = assembly.GetType("Microsoft.FSharp.Control.FSharpAsync"); |
|||
|
|||
if (fsharpOptionType == null || fsharpAsyncType == null) return false; |
|||
|
|||
// Get a reference to FSharpOption<TaskCreationOptions>.None
|
|||
var fsharpOptionOfTaskCreationOptionsType = fsharpOptionType |
|||
.MakeGenericType(typeof(TaskCreationOptions)); |
|||
_fsharpOptionOfTaskCreationOptionsNoneProperty = fsharpOptionOfTaskCreationOptionsType |
|||
.GetRuntimeProperty("None"); |
|||
|
|||
// Get a reference to FSharpOption<CancellationToken>.None
|
|||
var fsharpOptionOfCancellationTokenType = fsharpOptionType |
|||
.MakeGenericType(typeof(CancellationToken)); |
|||
_fsharpOptionOfCancellationTokenNoneProperty = fsharpOptionOfCancellationTokenType |
|||
.GetRuntimeProperty("None"); |
|||
|
|||
// Get a reference to FSharpAsync.StartAsTask<>
|
|||
var fsharpAsyncMethods = fsharpAsyncType |
|||
.GetRuntimeMethods() |
|||
.Where(m => m.Name.Equals("StartAsTask", StringComparison.Ordinal)); |
|||
foreach (var candidateMethodInfo in fsharpAsyncMethods) |
|||
{ |
|||
var parameters = candidateMethodInfo.GetParameters(); |
|||
if (parameters.Length == 3 |
|||
&& TypesHaveSameIdentity(parameters[0].ParameterType, possibleFSharpAsyncGenericType) |
|||
&& parameters[1].ParameterType == fsharpOptionOfTaskCreationOptionsType |
|||
&& parameters[2].ParameterType == fsharpOptionOfCancellationTokenType) |
|||
{ |
|||
// This really does look like the correct method (and hence assembly).
|
|||
_fsharpAsyncStartAsTaskGenericMethod = candidateMethodInfo; |
|||
_fsharpCoreAssembly = assembly; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
return _fsharpCoreAssembly != null; |
|||
} |
|||
|
|||
private static bool TypesHaveSameIdentity(Type type1, Type type2) |
|||
{ |
|||
return type1.Assembly == type2.Assembly |
|||
&& string.Equals(type1.Namespace, type2.Namespace, StringComparison.Ordinal) |
|||
&& string.Equals(type1.Name, type2.Name, StringComparison.Ordinal); |
|||
} |
|||
} |
|||
@ -0,0 +1,2 @@ |
|||
这个目录下是cap的源码 |
|||
因为cap的源码的访问级别是internal,扩展的时候无法访问,所以直接把源码拷贝过来。 |
|||
Loading…
Reference in new issue