Wrap C# code as ActiveX - c#

I write a C# class library and want to use it in vbscript.
Here is my code:
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.PointOfService;
using System.Runtime.InteropServices;
using Microsoft.Win32;
using System.Reflection;
using System.ComponentModel;
namespace IndigoDynamic
{
#region class implements IAsyncResult
[ProgId("IndigoDynamic.VirtualManager")]
[ClassInterface(ClassInterfaceType.AutoDispatch)]
[ComVisible(true)]
public class AsyncResult : IAsyncResult
{
object _state;
private bool m_completed;
private System.Threading.ManualResetEvent m_handle;
private Exception m_exception;
public bool IsCompleted
{
get { return m_completed; }
set { m_completed = value; }
}
public System.Threading.WaitHandle AsyncWaitHandle
{
get { return m_handle; }
set { m_handle = (System.Threading.ManualResetEvent)value; }
}
public object AsyncState
{
get
{
if (Exception != null)
{
throw Exception;
}
return _state;
}
internal set
{
_state = value;
}
}
public bool CompletedSynchronously { get { return IsCompleted; } }
internal Exception Exception
{
get { return m_exception; }
set { m_exception = value; }
}
}
#endregion
#region extends CashDrawer
[ProgId("IndigoDynamic.VirtualManager")]
[ClassInterface(ClassInterfaceType.AutoDispatch)]
[ComVisible(true)]
public class MyCashDrawer
{
private CashDrawer me;
public delegate void status_callback(int newstatus);
private status_callback myF;
public MyCashDrawer(CashDrawer param)
{
me = param;
me.StatusUpdateEvent += new StatusUpdateEventHandler(this.StatusUpdate);
}
[ComVisible(true)]
public void Claim(int timeout) { me.Claim(timeout); }
[ComVisible(true)]
public void Close() { me.Close(); }
[ComVisible(true)]
public void Open() { me.Open(); }
[ComVisible(true)]
public void OpenDrawer() { me.OpenDrawer(); }
[ComVisible(true)]
public void Release() { me.Release(); }
[ComVisible(true)]
public void Release(int timeout, int freq, int duration, int delay)
{
me.WaitForDrawerClose(timeout, freq, duration, delay);
}
[ComVisible(true)]
public int StatusClosed() { return CashDrawer.StatusClosed; }
[ComVisible(true)]
public int StatusOpen() { return CashDrawer.StatusOpen; }
[ComVisible(true)]
public bool Claimed() { return me.Claimed; }
[ComVisible(true)]
public bool DeviceEnabled() { return me.DeviceEnabled; }
[ComVisible(true)]
public bool DrawerOpened() { return me.DrawerOpened; }
[ComVisible(true)]
public ControlState State() { return me.State; }
[ComVisible(true)]
public void addStatusCallback(status_callback f)
{
myF = f;
}
[ComVisible(true)]
public void removeStatusCallback(status_callback f)
{
if (myF == f)
myF = null;
}
[ComVisible(true)]
private void StatusUpdate(object sender, StatusUpdateEventArgs arg)
{
if (myF != null)
myF(arg.Status);
}
}
#endregion
[ProgId("IndigoDynamic.VirtualManager")]
[ClassInterface(ClassInterfaceType.AutoDispatch)]
[ComVisible(true)]
class VirtualManager : ISynchronizeInvoke
{
private readonly object _sync;
// Constructor
public VirtualManager()
{
_sync = new object();
}
#region implements methods of ISynchronizeInvoke
public IAsyncResult BeginInvoke(Delegate method, object[] args) {
AsyncResult result = new AsyncResult();
System.Threading.ThreadPool.QueueUserWorkItem(delegate {
result.AsyncWaitHandle = new System.Threading.ManualResetEvent(false);
try {
result.AsyncState = Invoke(method, args);
} catch (Exception exception) {
result.Exception = exception;
}
result.IsCompleted = true;
});
return result;
}
public object EndInvoke(IAsyncResult result) {
if (!result.IsCompleted) {
result.AsyncWaitHandle.WaitOne();
}
return result.AsyncState;
}
public object Invoke(Delegate method, object[] args) {
lock (_sync) {
return method.DynamicInvoke(args);
}
}
public bool InvokeRequired {
get { return true; }
}
#endregion
[ComVisible(true)]
public MyCashDrawer getCashDrawer()
{
PosExplorer posExplorer = new PosExplorer(this);
DeviceInfo deviceInfo = posExplorer.GetDevice(DeviceType.CashDrawer);
if (deviceInfo == null)
{
//<report failure >
return null;
}
else
{
CashDrawer cd = posExplorer.CreateInstance(deviceInfo) as CashDrawer;
return new MyCashDrawer(cd);
}
}
[ComVisible(true)]
public MyCashDrawer getCashDrawer(String name)
{
PosExplorer posExplorer = new PosExplorer(this);
DeviceInfo deviceInfo = posExplorer.GetDevice(DeviceType.CashDrawer, name);
if (deviceInfo == null)
{
//<report failure >
return null;
}
else
{
CashDrawer cd = posExplorer.CreateInstance(deviceInfo) as CashDrawer;
return new MyCashDrawer(cd);
}
}
[ComRegisterFunction()]
public static void RegisterClass(string key)
{
StringBuilder sb = new StringBuilder(key);
sb.Replace(#"HKEY_CLASSES_ROOT\", "");
RegistryKey k = Registry.ClassesRoot.OpenSubKey(sb.ToString(), true);
RegistryKey ctrl = k.CreateSubKey("Control");
ctrl.Close();
RegistryKey inprocServer32 = k.OpenSubKey("InprocServer32", true);
inprocServer32.SetValue("CodeBase", Assembly.GetExecutingAssembly().CodeBase);
inprocServer32.Close();
k.Close();
}
[ComUnregisterFunction()]
public static void UnregisterClass(string key)
{
StringBuilder sb = new StringBuilder(key);
sb.Replace(#"HKEY_CLASSES_ROOT\", "");
RegistryKey k = Registry.ClassesRoot.OpenSubKey(sb.ToString(), true);
if (k == null)
{
return;
}
k.DeleteSubKey("Control", false);
RegistryKey inprocServer32 = k.OpenSubKey("InprocServer32", true);
inprocServer32.DeleteSubKey("CodeBase", false);
inprocServer32.Close();
k.Close();
}
}
}
After building, I use RegAsm but it threw Warning No types are registered.
Then I write a sample code in vbs but it says ActiveX can not create Object.
Sub main
set objTest = CreateObject("IndigoDynamic.VirtualManager")
end sub
call main
Somebody said that I have to check AssemblyInfo.cs and make sure I have
[assembly: ComVisible(true)]
Of course I have, but problem is still not solved.
Can anybody tell me a solution?
I change my code like that. More simple, no threading, no interface, all is public.
But it still doesn't work.
Please, I really need help.
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.PointOfService;
using System.Runtime.InteropServices;
using Microsoft.Win32;
using System.Reflection;
using System.ComponentModel;
namespace IndigoDynamic
{
[ProgId("IndigoDynamic.VirtualManager")]
[ClassInterface(ClassInterfaceType.AutoDispatch)]
[ComVisible(true)]
public class VirtualManager
{
public VirtualManager()
{
}
[ComVisible(true)]
public CashDrawer getCashDrawer()
{
PosExplorer posExplorer = new PosExplorer();
DeviceInfo deviceInfo = posExplorer.GetDevice(DeviceType.CashDrawer);
if (deviceInfo == null)
{
//<report failure >
return null;
}
else
{
CashDrawer cd = posExplorer.CreateInstance(deviceInfo) as CashDrawer;
return cd;
}
}
[ComVisible(true)]
public CashDrawer getCashDrawer(String name)
{
PosExplorer posExplorer = new PosExplorer();
DeviceInfo deviceInfo = posExplorer.GetDevice(DeviceType.CashDrawer, name);
if (deviceInfo == null)
{
//<report failure >
return null;
}
else
{
CashDrawer cd = posExplorer.CreateInstance(deviceInfo) as CashDrawer;
return cd;
}
}
[ComRegisterFunction()]
public static void RegisterClass(string key)
{
StringBuilder sb = new StringBuilder(key);
sb.Replace(#"HKEY_CLASSES_ROOT\", "");
RegistryKey k = Registry.ClassesRoot.OpenSubKey(sb.ToString(), true);
RegistryKey ctrl = k.CreateSubKey("Control");
ctrl.Close();
RegistryKey inprocServer32 = k.OpenSubKey("InprocServer32", true);
inprocServer32.SetValue("CodeBase", Assembly.GetExecutingAssembly().CodeBase);
inprocServer32.Close();
k.Close();
}
[ComUnregisterFunction()]
public static void UnregisterClass(string key)
{
StringBuilder sb = new StringBuilder(key);
sb.Replace(#"HKEY_CLASSES_ROOT\", "");
RegistryKey k = Registry.ClassesRoot.OpenSubKey(sb.ToString(), true);
if (k == null)
{
return;
}
k.DeleteSubKey("Control", false);
RegistryKey inprocServer32 = k.OpenSubKey("InprocServer32", true);
inprocServer32.DeleteSubKey("CodeBase", false);
inprocServer32.Close();
k.Close();
}
}
}

You are unfortunately very far removed from a workable solution. The warning is accurate, none of the classes you made [ComVisible] at creatable by a COM client. MyCashDrawer is missing the required default constructor, a COM client app cannot pass arguments to a constructor. VirtualManager isn't public and derives from an interface that's not [ComVisible]
The code is also missing the required implementations for interfaces that makes an ActiveX component work on a ActiveX host window, like IOleObject, IOleInPlaceObject, IOleInplaceActiveObject, IOleWindow, IViewObject, etcetera. Furthermore, you are exposing implementation details that an ActiveX object can't take care of, the threading model in COM is very different from the one in .NET.
You are going to need a seriously different approach. Consider deriving the visible object from System.Windows.Forms.Control, it takes care of the minimum ActiveX interface implementation requirements. And make threading your problem, don't leave it up to the client to sort it out.

The regasm warning isn't the problem.
Maybe it's beacause VirtualManager class isn't public. Try to expose your class as public.

Related

How to patch public int "set" method though Harmony Patches

I'm trying to patch public int "set" method taught Harmony Patches. The method I want to patch is a 7D2D function (ProgressionValue.Level). I wrote a little patch, but It doesn't work.
Method I want to patch:
public void set_Level(int value)
{
this.calculatedFrame = -1;
if (this.ProgressionClass == null)
{
this.level = value;
return;
}
if (this.ProgressionClass.IsSkill)
{
this.level = this.ProgressionClass.MaxLevel;
return;
}
this.level = value;
}
Method I wrote:
using HarmonyLib;
using UnityEngine;
namespace Undead_InstantSkillLeveling
{
[HarmonyPatch(typeof(ProgressionValue))]
[HarmonyPatch(nameof(ProgressionValue.Level))]
public class Udead_Harmony_InstantSkill
{
private static void Prefix(ProgressionValue __instance)
{
__instance.Level = __instance.ProgressionClass.MaxLevel;
}
}
}
Harmony used: Harmony

Method GetServer() returning null for unknown reason

I have a project and one of the main methods is returning null when it is pretty clear to see I have assigned a value to it.
Program.cs:
using System;
namespace Sahara
{
class Program
{
static void Main(string[] args)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.Title = "Loading Sahara...";
Console.CursorVisible = false;
Sahara.Initialize();
while (true)
{
Console.ReadKey();
}
}
}
}
Sahara.cs:
namespace Sahara
{
class Sahara
{
private static SaharaServer server;
public static void Initialize()
{
server = new SaharaServer();
}
public static SaharaServer GetServer()
{
return server;
}
}
}
SaharaServer:
using Sahara.Core.Config;
using Sahara.Core.Logging;
using Sahara.Core.Server;
using System;
using System.Diagnostics;
namespace Sahara
{
class SaharaServer
{
private readonly ServerStatusUpdater serverStatusUpdater;
private readonly LogManager logManager;
private readonly ServerInformation serverInformation;
private readonly DateTime startedTime;
private readonly ConfigManager configManager;
public SaharaServer()
{
logManager = new LogManager();
serverInformation = new ServerInformation();
foreach (string consoleOutputString in serverInformation.ConsoleLogo)
{
Console.WriteLine(consoleOutputString);
}
logManager.Log("Loading " + serverInformation.ServerName + "...", LogType.Information);
Stopwatch stopwatch = Stopwatch.StartNew();
configManager = new ConfigManager("Extra/Other/config.ini");
startedTime = DateTime.Now;
serverStatusUpdater = new ServerStatusUpdater();
stopwatch.Stop();
logManager.Log("Finished Loading! [" + stopwatch.ElapsedMilliseconds + "ms]", LogType.Warning);
Console.ForegroundColor = ConsoleColor.Black;
}
public LogManager GetLogManager()
{
return logManager;
}
public ServerInformation GetServerInformation()
{
return serverInformation;
}
public DateTime StartedTime
{
get { return startedTime; }
}
public ConfigManager GetConfigManager()
{
return configManager;
}
public void Dispose()
{
try
{
serverStatusUpdater.Dispose();
}
catch (Exception exception)
{
if (logManager != null)
{
logManager.Log("Error in disposing SaharaServer: " + exception.Message, LogType.Error);
logManager.Log(exception.StackTrace, LogType.Error);
}
}
finally
{
Environment.Exit(0);
}
}
}
}
But why is GetServer() in Sahara.cs returning null!!?!?!?!?
You call the ConfigManager constructor from within the SaharaServer constructor, so the constructor has not yet completed to set the server field and thus GetServer will return null.

How to use common trace id?

I want to use common trace id. I'm using following code.
public void method1(){
using (new Tracer(Guid.NewGuid().ToString()))
{
//my code
}
}
public void method2(){
using (new Tracer(Guid.NewGuid().ToString()))
{
//my code
}
}
Here guid is my trace id. But different trace id generating for every method call. I want keep it as unique. How to achieve this?. (note : I call method1,method2 from some different client)
If you need to get info about class name and/or your .NET <= 4.0, use StackFrame. You'll get some overhead with StackFrame. If you don't need to get the name of class and you use .NET >= 4.5, here is solution. It uses Caller Information. :
namespace Tracer
{
using System;
using System.Runtime.CompilerServices;
sealed class CallerInfoTracer : IDisposable
{
private readonly string _message;
private readonly string _memberName;
private readonly string _sourceFilePath;
private readonly int _lineNumber;
private bool _disposed;
public CallerInfoTracer(string message, [CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int lineNumber = 0)
{
_message = message;
_memberName = memberName;
_sourceFilePath = sourceFilePath;
_lineNumber = lineNumber;
}
public void Dispose()
{
if (_disposed) return;
Console.WriteLine("Message: {0}", _message);
Console.WriteLine("MemberName: {0}", _memberName);
Console.WriteLine("SourceFilePath: {0}", _sourceFilePath);
Console.WriteLine("LineNumber: {0}", _lineNumber);
_disposed = true;
}
}
public class Program
{
public static void Main(string[] args)
{
Method1();
Method2();
}
public static void Method1()
{
using (var tracer = new CallerInfoTracer("Desc1")) { }
}
public static void Method2()
{
using (var tracer = new CallerInfoTracer("Desc2")) { }
}
}
}

Use C++ Intellisense for custom ContentType

I'm trying to make a simple extension with MEF for GLSL (It is also my first one, I tried to understand the language services and other stuff but I got completely lost. It seems like way too much work compared to MEF for a simple non-professional thing). I've been following the tutorials on MSDN and the OOK example to create basic syntax coloring, brace matching and IntelliSense support. I created my content type to extend C/C++, so I have the default coloring for C/C++ keywords, however I don't have access to C/C++ IntelliSense, the completion set isn't even in the list. Is there a way to get the default IntelliSense as well as my own completion set?
Here's my completion source:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.VisualStudio.Language.Intellisense;
using System.Collections.ObjectModel;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Tagging;
using System.ComponentModel.Composition;
using Microsoft.VisualStudio.Utilities;
namespace GLSL_Extension {
[Export(typeof(ICompletionSourceProvider))]
[ContentType(GLSLConstants.ContentType)]
[Name(GLSLConstants.CompletionSourceProviderName)]
class GLSLCompletionSourceProvider : ICompletionSourceProvider {
public ICompletionSource TryCreateCompletionSource(ITextBuffer textBuffer) {
return new GLSLCompletionSource(textBuffer);
}
}
class GLSLCompletionSource : ICompletionSource, IDisposable {
private ITextBuffer buffer;
private bool disposed = false;
public GLSLCompletionSource(ITextBuffer buffer) {
this.buffer = buffer;
}
public void AugmentCompletionSession(ICompletionSession session, IList<CompletionSet> completionSets) {
if (this.disposed)
throw new ObjectDisposedException(this.ToString());
List<Completion> completions = new List<Completion>(){
new Completion(GLSLCompletionResources.uniformName, GLSLCompletionResources.uniformName, GLSLCompletionResources.uniformDesc, null, ""),
new Completion(GLSLCompletionResources.varyingName, GLSLCompletionResources.varyingName, GLSLCompletionResources.varyingDesc, null, ""),
new Completion(GLSLCompletionResources.inName, GLSLCompletionResources.inName, GLSLCompletionResources.inDesc, null, ""),
new Completion(GLSLCompletionResources.outName, GLSLCompletionResources.outName, GLSLCompletionResources.outDesc, null, ""),
new Completion(GLSLCompletionResources.vec2Name, GLSLCompletionResources.vec2Name, GLSLCompletionResources.vec2Desc, null, ""),
new Completion(GLSLCompletionResources.vec3Name, GLSLCompletionResources.vec3Name, GLSLCompletionResources.vec3Desc, null, ""),
new Completion(GLSLCompletionResources.vec4Name, GLSLCompletionResources.vec4Name, GLSLCompletionResources.vec4Desc, null, ""),
new Completion(GLSLCompletionResources.mat2Name, GLSLCompletionResources.mat2Name, GLSLCompletionResources.mat2Desc, null, ""),
new Completion(GLSLCompletionResources.mat3Name, GLSLCompletionResources.mat3Name, GLSLCompletionResources.mat3Desc, null, ""),
new Completion(GLSLCompletionResources.mat4Name, GLSLCompletionResources.mat4Name, GLSLCompletionResources.mat4Desc, null, "")
};
ITextSnapshot snapshot = this.buffer.CurrentSnapshot;
var triggerPoint = (SnapshotPoint)session.GetTriggerPoint(snapshot);
if (triggerPoint == null)
return;
var line = triggerPoint.GetContainingLine();
SnapshotPoint start = triggerPoint;
while (start > line.Start && !char.IsWhiteSpace((start - 1).GetChar())) {
start -= 1;
}
var applicableTo = snapshot.CreateTrackingSpan(new SnapshotSpan(start, triggerPoint), SpanTrackingMode.EdgeInclusive);
completionSets.Add(new CompletionSet("All", "All", applicableTo, completions, Enumerable.Empty<Completion>()));
}
public void Dispose() {
if (!this.disposed) {
GC.SuppressFinalize(this);
this.disposed = true;
}
}
}
}
And my completion controller:
using System;
using System.Collections.Generic;
using System.Linq;
using System.ComponentModel.Composition;
using System.Diagnostics;
using Microsoft.VisualStudio.Editor;
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.OLE.Interop;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.TextManager.Interop;
using Microsoft.VisualStudio.Utilities;
using Microsoft.VisualStudio;
using System.Windows;
using System.Runtime.InteropServices;
namespace GLSL_Extension {
[Export(typeof(IVsTextViewCreationListener))]
[ContentType(GLSLConstants.ContentType)]
[TextViewRole(PredefinedTextViewRoles.Interactive)]
internal sealed class VsTextViewCreationListener : IVsTextViewCreationListener {
[Import]
IVsEditorAdaptersFactoryService AdaptersFactory = null;
[Import]
ICompletionBroker CompletionBroker = null;
public void VsTextViewCreated(IVsTextView textViewAdapter) {
IWpfTextView view = AdaptersFactory.GetWpfTextView(textViewAdapter);
Debug.Assert(view != null);
CommandFilter filter = new CommandFilter(view, CompletionBroker);
IOleCommandTarget next;
textViewAdapter.AddCommandFilter(filter, out next);
filter.Next = next;
}
}
internal sealed class CommandFilter : IOleCommandTarget {
private ICompletionSession currentSession;
public IWpfTextView TextView { get; private set; }
public ICompletionBroker Broker { get; private set; }
public IOleCommandTarget Next { get; set; }
public CommandFilter(IWpfTextView textView, ICompletionBroker broker) {
this.currentSession = null;
TextView = textView;
Broker = broker;
}
private char GetTypeChar(IntPtr pvaIn) {
return (char)(ushort)Marshal.GetObjectForNativeVariant(pvaIn);
}
public int Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut) {
bool handled = false;
int hresult = VSConstants.S_OK;
if (pguidCmdGroup == VSConstants.VSStd2K) {
switch ((VSConstants.VSStd2KCmdID)nCmdID) {
case VSConstants.VSStd2KCmdID.AUTOCOMPLETE:
case VSConstants.VSStd2KCmdID.COMPLETEWORD:
handled = StartSession();
break;
case VSConstants.VSStd2KCmdID.RETURN:
handled = Complete(false);
break;
case VSConstants.VSStd2KCmdID.TAB:
handled = Complete(true);
break;
case VSConstants.VSStd2KCmdID.CANCEL:
handled = Cancel();
break;
}
}
if (!handled)
hresult = Next.Exec(pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut);
if (ErrorHandler.Succeeded(hresult)) {
if (pguidCmdGroup == VSConstants.VSStd2K) {
switch ((VSConstants.VSStd2KCmdID)nCmdID) {
case VSConstants.VSStd2KCmdID.TYPECHAR:
char ch = GetTypeChar(pvaIn);
if (ch == ' ')
StartSession();
else if (this.currentSession != null)
Filter();
break;
case VSConstants.VSStd2KCmdID.BACKSPACE:
Filter();
break;
}
}
}
return hresult;
}
private void Filter() {
if (this.currentSession == null)
return;
this.currentSession.SelectedCompletionSet.SelectBestMatch();
this.currentSession.SelectedCompletionSet.Recalculate();
}
bool Cancel() {
if (this.currentSession == null)
return false;
this.currentSession.Dismiss();
return true;
}
bool Complete(bool force) {
if(this.currentSession == null)
return false;
if (!this.currentSession.SelectedCompletionSet.SelectionStatus.IsSelected && !force) {
this.currentSession.Dismiss();
return false;
} else {
this.currentSession.Commit();
return true;
}
}
bool StartSession() {
if (this.currentSession != null)
return false;
SnapshotPoint caret = TextView.Caret.Position.BufferPosition;
ITextSnapshot snapshot = caret.Snapshot;
if (!Broker.IsCompletionActive(TextView))
this.currentSession = Broker.CreateCompletionSession(TextView, snapshot.CreateTrackingPoint(caret, PointTrackingMode.Positive), true);
else
this.currentSession = Broker.GetSessions(TextView).First();
this.currentSession.Dismissed += (sender, args) => this.currentSession = null;
this.currentSession.Start();
return true;
}
public int QueryStatus(ref Guid pguidCmdGroup, uint cCmds, OLECMD[] prgCmds, IntPtr pCmdText) {
if (pguidCmdGroup == VSConstants.VSStd2K) {
switch ((VSConstants.VSStd2KCmdID)prgCmds[0].cmdID) {
case VSConstants.VSStd2KCmdID.AUTOCOMPLETE:
case VSConstants.VSStd2KCmdID.COMPLETEWORD:
prgCmds[0].cmdf = (uint)OLECMDF.OLECMDF_ENABLED | (uint)OLECMDF.OLECMDF_SUPPORTED;
return VSConstants.S_OK;
}
}
return Next.QueryStatus(pguidCmdGroup, cCmds, prgCmds, pCmdText);
}
}
}

Memory leaks when using MarshalByRef in .Net

I've come across an interesting issue, which is really two-fold I guess. I'll try and keep this focused though. I have an environment set up in which an assembly is programmatically compiled and loaded into a child app domain. A class from that child app domain's assembly is instantiated (it's actually marshaled back to the parent domain and a proxy is used there), and methods are executed against it.
The following resides in a satellite assembly:
namespace ScriptingSandbox
{
public interface ISandbox
{
object Invoke(string method, object[] parameters);
void Disconnect();
}
public class SandboxLoader : MarshalByRefObject, IDisposable
{
#region Properties
public bool IsDisposed { get; private set; }
public bool IsDisposing { get; private set; }
#endregion
#region Finalization/Dispose Methods
~SandboxLoader()
{
DoDispose();
}
public void Dispose()
{
DoDispose();
GC.SuppressFinalize(this);
}
private void DoDispose()
{
if (IsDisposing) return;
if (IsDisposed) return;
IsDisposing = true;
Disconnect();
IsDisposed = true;
IsDisposing = false;
}
#endregion
[SecurityPermissionAttribute(SecurityAction.Demand, Flags = SecurityPermissionFlag.Infrastructure)]
public override object InitializeLifetimeService()
{
// We don't want this to ever expire.
// We will disconnect it when we're done.
return null;
}
public void Disconnect()
{
// Close all the remoting channels so that this can be garbage
// collected later and we don't leak memory.
RemotingServices.Disconnect(this);
}
public ISandbox Create(string assemblyFileName, string typeName, object[] arguments)
{
// Using CreateInstanceFromAndUnwrap and then casting to the interface so that types in the
// child AppDomain won't be loaded into the parent AppDomain.
BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.CreateInstance;
object instance = AppDomain.CurrentDomain.CreateInstanceFromAndUnwrap(assemblyFileName, typeName, true, bindingFlags, null, arguments, null, null);
ISandbox sandbox = instance as ISandbox;
return sandbox;
}
}
}
The class that is unwrapped from the child app domain is expected to implement the interface above. The SandboxLoader in the code above also runs in the child app domain, and serves the role of creating the target class. This is all tied in by the ScriptingHost class below, which runs in the parent domain in the main assembly.
namespace ScriptingDemo
{
internal class ScriptingHost : IDisposable
{
#region Declarations
private AppDomain _childAppDomain;
private string _workingDirectory;
#endregion
#region Properties
public bool IsDisposed { get; private set; }
public bool IsDisposing { get; private set; }
public string WorkingDirectory
{
get
{
if (string.IsNullOrEmpty(_workingDirectory))
{
_workingDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "bin");
}
return _workingDirectory;
}
}
#endregion
public ScriptingHost() { }
#region Finalization/Dispose Methods
~ScriptingHost()
{
DoDispose(false);
}
public void Dispose()
{
DoDispose(true);
GC.SuppressFinalize(this);
}
private void DoDispose(bool isFromDispose)
{
if (IsDisposing) return;
if (IsDisposed) return;
IsDisposing = true;
if (isFromDispose)
{
UnloadChildAppDomain();
}
IsDisposed = true;
IsDisposing = false;
}
private void UnloadChildAppDomain()
{
if (_childAppDomain == null) return;
try
{
bool isFinalizing = _childAppDomain.IsFinalizingForUnload();
if (!isFinalizing)
{
AppDomain.Unload(_childAppDomain);
}
}
catch { }
_childAppDomain = null;
}
#endregion
#region Compile
public List<string> Compile()
{
CreateDirectory(WorkingDirectory);
CreateChildAppDomain(WorkingDirectory);
CompilerParameters compilerParameters = GetCompilerParameters(WorkingDirectory);
using (VBCodeProvider codeProvider = new VBCodeProvider())
{
string sourceFile = GetSourceFilePath();
CompilerResults compilerResults = codeProvider.CompileAssemblyFromFile(compilerParameters, sourceFile);
List<string> compilerErrors = GetCompilerErrors(compilerResults);
return compilerErrors;
}
}
private string GetSourceFilePath()
{
DirectoryInfo dir = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory);
// This points a test VB.net file in the solution.
string sourceFile = Path.Combine(dir.Parent.Parent.FullName, #"Classes\Scripting", "ScriptingDemo.vb");
return sourceFile;
}
private void CreateDirectory(string path)
{
if (Directory.Exists(path))
{
Directory.Delete(path, true);
}
Directory.CreateDirectory(path);
}
private void CreateChildAppDomain(string workingDirectory)
{
AppDomainSetup appDomainSetup = new AppDomainSetup()
{
ApplicationBase = AppDomain.CurrentDomain.SetupInformation.ApplicationBase,
PrivateBinPath = "bin",
LoaderOptimization = LoaderOptimization.MultiDomainHost,
ApplicationTrust = AppDomain.CurrentDomain.ApplicationTrust
};
Evidence evidence = new Evidence(AppDomain.CurrentDomain.Evidence);
_childAppDomain = AppDomain.CreateDomain(Guid.NewGuid().ToString(), evidence, appDomainSetup);
_childAppDomain.InitializeLifetimeService();
}
private CompilerParameters GetCompilerParameters(string workingDirectory)
{
CompilerParameters compilerParameters = new CompilerParameters()
{
GenerateExecutable = false,
GenerateInMemory = false,
IncludeDebugInformation = true,
OutputAssembly = Path.Combine(workingDirectory, "GeneratedAssembly.dll")
};
// Add GAC/System Assemblies
compilerParameters.ReferencedAssemblies.Add("System.dll");
compilerParameters.ReferencedAssemblies.Add("System.Xml.dll");
compilerParameters.ReferencedAssemblies.Add("System.Data.dll");
compilerParameters.ReferencedAssemblies.Add("Microsoft.VisualBasic.dll");
// Add Custom Assemblies
compilerParameters.ReferencedAssemblies.Add(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ScriptingSandbox.dll"));
compilerParameters.ReferencedAssemblies.Add(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ScriptingInterfaces.dll"));
return compilerParameters;
}
private List<string> GetCompilerErrors(CompilerResults compilerResults)
{
List<string> errors = new List<string>();
if (compilerResults == null) return errors;
if (compilerResults.Errors == null) return errors;
if (compilerResults.Errors.Count == 0) return errors;
foreach (CompilerError error in compilerResults.Errors)
{
string errorText = string.Format("[{0}, {1}] :: {2}", error.Line, error.Column, error.ErrorText);
errors.Add(errorText);
}
return errors;
}
#endregion
#region Execute
public object Execute(string method, object[] parameters)
{
using (SandboxLoader sandboxLoader = CreateSandboxLoader())
{
ISandbox sandbox = CreateSandbox(sandboxLoader);
try
{
object result = sandbox.Invoke(method, parameters);
return result;
}
finally
{
if (sandbox != null)
{
sandbox.Disconnect();
sandbox = null;
}
}
}
}
private SandboxLoader CreateSandboxLoader()
{
object sandboxLoader = _childAppDomain.CreateInstanceAndUnwrap("ScriptingSandbox", "ScriptingSandbox.SandboxLoader", true, BindingFlags.CreateInstance, null, null, null, null);
return sandboxLoader as SandboxLoader;
}
private ISandbox CreateSandbox(SandboxLoader sandboxLoader)
{
string assemblyPath = Path.Combine(WorkingDirectory, "GeneratedAssembly.dll");
ISandbox sandbox = sandboxLoader.Create(assemblyPath, "ScriptingDemoSource.SandboxClass", null);
return sandbox;
}
#endregion
}
}
For reference, the ScriptingDemo.vb file that gets compiled:
Imports System
Imports System.Collections
Imports System.Collections.Generic
Imports System.Globalization
Imports Microsoft.VisualBasic
Imports System.Data
Imports System.Text
Imports System.Text.RegularExpressions
Imports System.Xml
Imports System.Net
Imports System.ComponentModel
Imports System.Reflection
Imports System.Runtime.Remoting
Imports System.Runtime.Remoting.Lifetime
Imports System.Security.Permissions
Imports ScriptingSandbox
Imports ScriptingInterfaces
Namespace ScriptingDemoSource
Public Class SandboxClass
Inherits MarshalByRefObject
Implements ISandbox
Public Sub Disconnect() Implements ISandbox.Disconnect
RemotingServices.Disconnect(Me)
End Sub
Public Function Invoke(ByVal methodName As String, methodParameters As Object()) As Object Implements ScriptingSandbox.ISandbox.Invoke
'Return Nothing
Dim type As System.Type = Me.GetType()
Dim returnValue As Object = type.InvokeMember(methodName, Reflection.BindingFlags.InvokeMethod + Reflection.BindingFlags.Default, Nothing, Me, methodParameters)
type = Nothing
Return returnValue
End Function
<SecurityPermissionAttribute(SecurityAction.Demand, Flags:=SecurityPermissionFlag.Infrastructure)> _
Public Overrides Function InitializeLifetimeService() As Object
Return Nothing
End Function
Function ExecuteWithNoParameters() As Object
Return Nothing
End Function
Function ExecuteWithSimpleParameters(a As Integer, b As Integer) As Object
Return a + b
End Function
Function ExecuteWithComplexParameters(o As ScriptingInterfaces.IMyInterface) As Object
Return o.Execute()
End Function
End Class
End Namespace
The first issue I ran into was that even after cleaning up the sandbox, memory leaked. This was resolved by keeping an instance of the sandbox around and not destroying it after executing methods from the script. This added/changed the following to the ScriptingHost class:
private ISandbox _sandbox;
private string _workingDirectory;
private void DoDispose(bool isFromDispose)
{
if (IsDisposing) return;
if (IsDisposed) return;
IsDisposing = true;
if (isFromDispose)
{
Cleanup();
}
IsDisposed = true;
IsDisposing = false;
}
private void CleanupSandboxLoader()
{
try
{
if (_sandboxLoader == null) return;
_sandboxLoader.Disconnect();
_sandboxLoader = null;
}
catch { }
}
private void CleanupSandbox()
{
try
{
if (_sandbox == null) return;
_sandbox.Disconnect();
}
catch { }
}
public void Cleanup()
{
CleanupSandbox();
CleanupSandboxLoader();
UnloadChildAppDomain();
}
public object Execute(string method, object[] parameters)
{
if (_sandboxLoader == null)
{
_sandboxLoader = CreateSandboxLoader();
}
if (_sandbox == null)
{
_sandbox = CreateSandbox(_sandboxLoader);
}
object result = _sandbox.Invoke(method, parameters);
return result;
}
This really didn't resolve the underlying issue (destroying the sandbox and loader didn't release memory as expected). As I have more control over that behavior, though, it did allow me to move on to the next issue.
The code that uses ScriptingHost looks like the following:
private void Execute()
{
try
{
List<MyClass> originals = CreateList();
for (int i = 0; i < 4000; i++)
{
List<MyClass> copies = MyClass.MembersClone(originals);
foreach (MyClass copy in copies)
{
object[] args = new object[] { copy };
try
{
object results = _scriptingHost.Execute("ExecuteWithComplexParameters", args);
}
catch (Exception ex)
{
_logManager.LogException("executing the script", ex);
}
finally
{
copy.Disconnect();
args.SetValue(null, 0);
args = null;
}
}
MyClass.ShallowCopy(copies, originals);
MyClass.Cleanup(copies);
copies = null;
}
MyClass.Cleanup(originals);
originals = null;
}
catch (Exception ex)
{
_logManager.LogException("executing the script", ex);
}
MessageBox.Show("done");
}
private List<MyClass> CreateList()
{
List<MyClass> myClasses = new List<MyClass>();
for (int i = 0; i < 300; i++)
{
MyClass myClass = new MyClass();
myClasses.Add(myClass);
}
return myClasses;
}
And the code for MyClass:
namespace ScriptingDemo
{
internal sealed class MyClass : MarshalByRefObject, IMyInterface, IDisposable
{
#region Properties
public int ID { get; set; }
public string Name { get; set; }
public bool IsDisposed { get; private set; }
public bool IsDisposing { get; private set; }
#endregion
public MyClass() { }
#region Finalization/Dispose Methods
~MyClass()
{
DoDispose();
}
public void Dispose()
{
DoDispose();
GC.SuppressFinalize(this);
}
private void DoDispose()
{
if (IsDisposing) return;
if (IsDisposed) return;
IsDisposing = true;
Disconnect();
IsDisposed = true;
IsDisposing = false;
}
#endregion
[SecurityPermissionAttribute(SecurityAction.Demand, Flags = SecurityPermissionFlag.Infrastructure)]
public override object InitializeLifetimeService()
{
// We don't want this to ever expire.
// We will disconnect it when we're done.
return null;
}
public void Disconnect()
{
// Close all the remoting channels so that this can be garbage
// collected later and we don't leak memory.
RemotingServices.Disconnect(this);
}
public object Execute()
{
return "Hello, World!";
}
public MyClass MembersClone()
{
MyClass copy = new MyClass();
copy.ShallowCopy(this);
return copy;
}
public void ShallowCopy(MyClass source)
{
if (source == null) return;
ID = source.ID;
Name = source.Name;
}
#region Static Members
public static void ShallowCopy(List<MyClass> sources, List<MyClass> targets)
{
if (sources == null) return;
if (targets == null) return;
int minCount = Math.Min(sources.Count, targets.Count);
for (int i = 0; i < minCount; i++)
{
MyClass source = sources[i];
MyClass target = targets[i];
target.ShallowCopy(source);
}
}
public static List<MyClass> MembersClone(List<MyClass> originals)
{
if (originals == null) return null;
List<MyClass> copies = new List<MyClass>();
foreach (MyClass original in originals)
{
MyClass copy = original.MembersClone();
copies.Add(copy);
}
return copies;
}
public static void Disconnect(List<MyClass> myClasses)
{
if (myClasses == null) return;
myClasses.ForEach(c => c.Disconnect());
}
public static void Cleanup(List<MyClass> myClasses)
{
if (myClasses == null) return;
myClasses.ForEach(c => c.Dispose());
myClasses.Clear();
myClasses.TrimExcess();
myClasses = null;
}
#endregion
}
}
As the code stands, memory slowly leaks the more iterations run and GCHandles soar through the roof. I've played with adding a finite lease instead of setting up the leases to never expire, but that caused wild fluctuations in memory that would eventually drop but not completely and ultimately still consumed more memory overall than the current solution (by a margin of dozens of megabytes).
I fully understand that creating a large number of classes like that and dropping them shortly there after is undesirable, but it simulates a much larger system. We may or may not address that issue, but for me I would like to better understand why the memory is leaking in the current system.
EDIT:
I just wanted to note that the memory leaking doesn't appear to be managed memory. Using various profiling tools, it appears that that the managed heaps tend to stay within a pretty set range whereas the unmanaged memory is what seems to grow.
EDIT #2
Rewriting the code to keep the list of classes around rather than dumping them every iteration does seem to alleviate the issues (my assumption is that this works because we're reusing everything we've already allocated), but I'd like to keep this open if only for an academic exercise. The root issue still is unresolved.

Categories

Resources