每次您使用 CSharpScript.Run 或 Evaluate 方法时,您实际上是在加载一个恰好相当大的新脚本(一个 .dll)。为了避免这种情况,您需要缓存您正在执行的脚本:
_script = CSharpScript.Create<TR>(code, opts, typeof(Globals<T>)); // Other options may be needed here
缓存了 _script 您现在可以通过以下方式执行它:
_script.RunAsync(new Globals<T> {A = a, B = b}); // The script will compile here in the first execution
如果您每次都需要为应用程序加载一些脚本,那么这是最简单的做法。然而,更好的解决方案是使用单独的 AppDomain 并加载隔离的脚本。这是一种方法:
创建一个作为 MarshalByRefObject 的脚本执行器代理:
public class ScriptExecutor<TP, TR> : CrossAppDomainObject, IScriptExecutor<TP, TR>
{
private readonly Script<TR> _script;
private int _currentClients;
public DateTime TimeStamp { get; }
public int CurrentClients => _currentClients;
public string Script => _script.Code;
public ScriptExecutor(string script, DateTime? timestamp = null, bool eagerCompile = false)
{
if (string.IsNullOrWhiteSpace(script))
throw new ArgumentNullException(nameof(script));
var opts = ScriptOptions.Default.AddImports("System");
_script = CSharpScript.Create<TR>(script, opts, typeof(Host<TP>)); // Other options may be needed here
if (eagerCompile)
{
var diags = _script.Compile();
Diagnostic firstError;
if ((firstError = diags.FirstOrDefault(d => d.Severity == DiagnosticSeverity.Error)) != null)
{
throw new ArgumentException($"Provided script can't compile: {firstError.GetMessage()}");
}
}
if (timestamp == null)
timestamp = DateTime.UtcNow;
TimeStamp = timestamp.Value;
}
public void Execute(TP parameters, RemoteCompletionSource<TR> completionSource)
{
Interlocked.Increment(ref _currentClients);
_script.RunAsync(new Host<TP> {Args = parameters}).ContinueWith(t =>
{
if (t.IsFaulted && t.Exception != null)
{
completionSource.SetException(t.Exception.InnerExceptions.ToArray());
Interlocked.Decrement(ref _currentClients);
}
else if (t.IsCanceled)
{
completionSource.SetCanceled();
Interlocked.Decrement(ref _currentClients);
}
else
{
completionSource.SetResult(t.Result.ReturnValue);
Interlocked.Decrement(ref _currentClients);
}
});
}
}
public class Host<T>
{
public T Args { get; set; }
}
创建一个代理对象,在脚本执行应用域和主域之间共享数据:
public class RemoteCompletionSource<T> : CrossAppDomainObject
{
private readonly TaskCompletionSource<T> _tcs = new TaskCompletionSource<T>();
public void SetResult(T result) { _tcs.SetResult(result); }
public void SetException(Exception[] exception) { _tcs.SetException(exception); }
public void SetCanceled() { _tcs.SetCanceled(); }
public Task<T> Task => _tcs.Task;
}
创建所有其他远程需要继承的辅助抽象类型:
public abstract class CrossAppDomainObject : MarshalByRefObject, IDisposable
{
private bool _disposed;
/// <summary>
/// Gets an enumeration of nested <see cref="MarshalByRefObject"/> objects.
/// </summary>
protected virtual IEnumerable<MarshalByRefObject> NestedMarshalByRefObjects
{
get { yield break; }
}
~CrossAppDomainObject()
{
Dispose(false);
}
/// <summary>
/// Disconnects the remoting channel(s) of this object and all nested objects.
/// </summary>
private void Disconnect()
{
RemotingServices.Disconnect(this);
foreach (var tmp in NestedMarshalByRefObjects)
RemotingServices.Disconnect(tmp);
}
public sealed override object InitializeLifetimeService()
{
//
// Returning null designates an infinite non-expiring lease.
// We must therefore ensure that RemotingServices.Disconnect() is called when
// it's no longer needed otherwise there will be a memory leak.
//
return null;
}
public void Dispose()
{
GC.SuppressFinalize(this);
Dispose(true);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
Disconnect();
_disposed = true;
}
}
我们是这样使用它的:
public static IScriptExecutor<T, R> CreateExecutor<T, R>(AppDomain appDomain, string script)
{
var t = typeof(ScriptExecutor<T, R>);
var executor = (ScriptExecutor<T, R>)appDomain.CreateInstanceAndUnwrap(t.Assembly.FullName, t.FullName, false, BindingFlags.CreateInstance, null,
new object[] {script, null, true}, CultureInfo.CurrentCulture, null);
return executor;
}
public static AppDomain CreateSandbox()
{
var setup = new AppDomainSetup { ApplicationBase = AppDomain.CurrentDomain.SetupInformation.ApplicationBase };
var appDomain = AppDomain.CreateDomain("Sandbox", null, setup, AppDomain.CurrentDomain.PermissionSet);
return appDomain;
}
string script = @"int Square(int number) {
return number*number;
}
Square(Args)";
var domain = CreateSandbox();
var executor = CreateExecutor<int, int>(domain, script);
using (var src = new RemoteCompletionSource<int>())
{
executor.Execute(5, src);
Console.WriteLine($"{src.Task.Result}");
}
注意在 using 块中 RemoteCompletionSource 的使用。如果您忘记处置它,您将有内存泄漏,因为此对象在另一个域(不是调用者)上的实例将永远不会被 GC。
免责声明:我从 RemoteCompletionSource 的想法
here,也是来自公共领域的 CrossAppDomainObject 的想法。