Jump to content

Sky Slate Blueberry Blackcurrant Watermelon Strawberry Orange Banana Apple Emerald Chocolate
Photo

.NET Framework Interop


  • Please log in to reply
150 replies to this topic
Lexikos
  • Administrators
  • 9442 posts
  • Last active:
  • Joined: 17 Oct 2006

Your workarounds involve starting a separate instance of AutoHotkey

I don't think so, but I don't want to argue.

What I meant was that the "ActiveX" DLL would be loaded into whatever process, but AutoHotkey.exe would also be running. I guess I don't see the point in communicating with/using AutoHotkey when the DLL is written in .NET. (I may not see the point, but it still interests me; read on...)

Do you actually have a use for generating .NET DLLs?

Well, it can be there just for the sake of compliteness then :), especialy if there isn't much to be done about it, like you already noted :)

I've added a few new features to the script:
[*:20hrbuay]CLR_StartDomain() now accepts an optional BaseDirectory, which is used to search for and load assemblies. (I've had problems with CLR looking for assemblies in %A_ProgramFiles%\AutoHotkey even though they're already loaded. :x BaseDirectory=A_WorkingDir solves that.)
[*:20hrbuay]CLR_CompileC# & 'VB now accept two additional optional parameters: FileName and CompilerOptions. If FileName is specified, a DLL or EXE is generated, but not loaded. Additional command-line arguments may be passed to the compiler via CompilerOptions. Of particular use, specify "/target:winexe" to generate a "windows" application rather than a (Windows) console application.
Since majkinetor mentioned inter-process communication earlier...
Remoting
.NET Remoting can be used to communicate with .NET objects in other processes (or even other computers) as though they are in the same process. The following demonstration is mostly C# based, but shows that the "proxy" for the remote object can be COM_Invoke'd the same as any other object. Requires .NET 2.0+ (but should run on earlier versions with minor modifications; see below.)
#NoEnv

; Base dll, defines the "interface" used to communicate.
c#_dll_file = AhkDotNetRemoteDemoBase.dll
c#_dll_refs = System.Windows.Forms.dll
c#_dll =
(
    public abstract class RemoteDemoBase : System.MarshalByRefObject {
        public abstract void StdOut(string msg);
        public abstract void Exit();
    }
)

; Server application.
c#_app_file = AhkDotNetRemoteDemo.exe
c#_app_refs = System.dll|System.Windows.Forms.dll|System.Runtime.Remoting.dll|%c#_dll_file%
c#_app =
(
    using System;
    using System.Windows.Forms;
    using Remoting = System.Runtime.Remoting;

    class RemoteDemo : RemoteDemoBase
    {
        static string Caption = "";
        
        // Entry-point for the application.
        static void Main()
        {
            //MessageBox.Show("Press OK to set up remoting.");
            Caption = "PID: " + System.Diagnostics.Process.GetCurrentProcess().Id;
            
            Remoting.Channels.ChannelServices.RegisterChannel(
                new Remoting.Channels.Ipc.IpcChannel("%A_ScriptName%"));
            
            Remoting.RemotingConfiguration.RegisterWellKnownServiceType(
                typeof(RemoteDemo), "RemoteDemo", Remoting.WellKnownObjectMode.Singleton);
            
            // Wait for exit message.
            Application.Run();
        }
        
        // Methods accessible by client.
        //
        public override void StdOut(string msg)
        {
            Console.WriteLine(msg);
        }
    
        public override void Exit()
        {
            Application.Exit();
        }
    }
)

; Client assembly.
c#_client_refs = System.Runtime.Remoting.dll | %c#_dll_file%
c#_client =
(
    using System;
    using Remoting = System.Runtime.Remoting;
    
    class RemoteDemoClient
    {
        RemoteDemoBase mRemoteDemo;
        
        public RemoteDemoClient()
        {
            Remoting.Channels.ChannelServices.RegisterChannel(
                new Remoting.Channels.Ipc.IpcChannel());
            
            // Get a proxy for the remote object.
            mRemoteDemo = (RemoteDemoBase) Activator.GetObject(
                typeof(RemoteDemoBase), "ipc://%A_ScriptName%/RemoteDemo");
        }
    
        public RemoteDemoBase RemoteDemo { get { return mRemoteDemo; } }
    
        public void Test()
        {
            try {
                mRemoteDemo.StdOut("test message.");
            } catch (Exception ex) {
                Console.WriteLine(ex.GetType().Name + " -- " + ex.Message);
            }
        }
    }
)

OnExit, CloseAndDeleteServer

CLR_Start()
; Start a new AppDomain so we can define the base path to load assemblies from.
CLR_StartDomain(pApp, A_WorkingDir)

; Compile the interface assembly to be used by both server and client.
CLR_CompileC#(c#_dll, c#_dll_refs, pApp, c#_dll_file)
; Compile the server application.
CLR_CompileC#(c#_app, c#_app_refs, pApp, c#_app_file)
; Compile and load the client assembly.
asm := CLR_CompileC#(c#_client, c#_client_refs, pApp)

; Start the server.
Run, %c#_app_file%,, UseErrorLevel, c#_app_pid
if !c#_app_pid {
    MsgBox Failed to launch %c#_app_pid%.
    ExitApp
}

; TODO: Wait for a signal from EXE that it is ready.
;   (The next two lines will work regardless, but any attempts to Invoke
;    methods of 'remote' will fail until the EXE finishes initializing.)

client := CLR_CreateObject(asm, "RemoteDemoClient")
remote := COM_Invoke(client, "RemoteDemo")
COM_Release(client)

Loop {
    InputBox, text,,,,, 100
    if ErrorLevel
        break
    COM_Invoke(remote, "StdOut", text)
    if (_hResult_)
        MsgBox Error: %_hResult_%
}
ExitApp


CloseAndDeleteServer:
    if (remote) {
        COM_Invoke(remote, "Exit")
        COM_Release(remote)
    }
    CLR_StopDomain(pApp)  ; unload c#_dll_file
    if c#_app_pid
        Process, WaitClose, %c#_app_pid%
    FileDelete, %c#_app_file%
    FileDelete, %c#_dll_file%
ExitApp
A basic summary of the script:
[*:20hrbuay]Generate a DLL which defines the abstract base class (RemoteDemoBase) of the object; i.e. the Invoke'able functions. This DLL is later loaded into the script and the server exe. This means the client and server need only load code that is relevant.
[*:20hrbuay]Generate an EXE which hosts the object (RemoteDemo) and its actual implementation. This is referred to as the server.
[*:20hrbuay]Generate and load an assembly which will be used to retrieve a proxy to the RemoteDemo object (as type RemoteDemoBase.) This is referred to as the client.
[*:20hrbuay]Start the generated EXE, and wait for the IpcChannel to initialize. Since IpcChannel uses a named pipe internally, the script simply waits for "\\.\pipe\channelname" to exist (Loop, IfExist, Sleep.)
[*:20hrbuay]Get a proxy to the RemoteDemo object. Note that only the metadata for the RemoteDemoBase abstract class is loaded into the script's process.
[*:20hrbuay]Receive input from the user, and Invoke the StdOut method of RemoteDemo(Base). Note that the COM-Callable Wrapper generated by .NET does not allow interface methods to be Invoked via IDispatch (hence the abstract base class rather than a true interface.)
[*:20hrbuay]When the user cancels, RemoteDemo.Exit() is Invoked. The AppDomain containing the client assembly is then unloaded so that the interface DLL can be deleted. The script waits for the server process to close, then deletes the generated EXE and DLL files.To interact with remote objects over a network, TcpChannel or HttpChannel must be used in place of IpcChannel. Using TcpChannel should make the script .NET 1.0-compatible, but IpcChannel has less overhead (i.e. is more efficient for Inter-process communication.)

majkinetor
  • Moderators
  • 4512 posts
  • Last active: Oct 02 2013 02:33 PM
  • Joined: 24 May 2006
U see, so much talk about few lines of code.

Now we have that funky thing, which can't be bad, eh ?

About example, I just quicly glanced and I can't run it again, it shows "too many parameters passed to function..". I reloaded CLR.ahk several times (perhaps something about my corporation proxy.... once reloading didn't help at all, I even tried in 3 different browsers and I always got the old image displayed)

Lexikos
  • Administrators
  • 9442 posts
  • Last active:
  • Joined: 17 Oct 2006
Sorry about that, I forgot to upload the file. :oops: Try again now.

U see, so much talk about few lines of code.

I wasn't against it, just wanted to know "why."

I just noticed the script occasionally fails to "connect" to the server app. :( Even worse, the HRESULT code is simply "0x80020009" (Exception). :x Useful? Ha!

Edit: The problem was apparently...

RemotingException -- Port is Busy: All pipe instances are busy.

...caused by:
Loop {  ; IpcChannel() uses a named pipe internally.
    ifExist, \\.\pipe\%A_ScriptName%  ; <-- doesn't always close the pipe..!?
        break
    Sleep, 10
}
The above was intended to make the script more reliable, but instead removing it made the script more reliable. :x

It turns out the server need not be ready (or even exist) until a method of RemoteDemo is called. Calling a method too soon results in an Exception, but doesn't prevent later calls from succeeding.

AHKnow*
  • Guests
  • Last active:
  • Joined: --
Ahhh.... Wow! This is looking really great. :D

polyethene
  • Administrators
  • 5517 posts
  • Last active: Jun 02 2014 02:21 AM
  • Joined: 26 Oct 2012
I'm curious to whether an ngen'd assembly could be embedded and run directly as machine code. This way your code can remain hidden and execution will be a lot faster. You can still program dynamic code with System.Reflection or by even reusing compilation services.

Lexikos
  • Administrators
  • 9442 posts
  • Last active:
  • Joined: 17 Oct 2006

I'm curious to whether an ngen'd assembly could be embedded and run directly as machine code. This way your code can remain hidden and execution will be a lot faster. You can still program dynamic code with System.Reflection or by even reusing compilation services.

Yes, using Assembly.Load(Byte[]). I will consider adding a friendly wrapper for it. :)

polyethene
  • Administrators
  • 5517 posts
  • Last active: Jun 02 2014 02:21 AM
  • Joined: 26 Oct 2012
I think you misunderstood. I'm talking about an embedded pre-compiled assembly to do reflection, totally independent of COM and all other ahk functions apart from DllCall. You can ignore this idea if you want because it's not that important.

Lexikos
  • Administrators
  • 9442 posts
  • Last active:
  • Joined: 17 Oct 2006
You're right. I misunderstood, mostly since my mind skipped over the word "ngen'd". :lol: It is definitely possible, as demonstrated by Loading a DLL from memory.

  • Guests
  • Last active:
  • Joined: --
I'm playing around with this, but I can't seem to get it to read from a simple hello world dll (the Class1.dll file is in the same directory as the script).

Here's the code:

#Include CLR.ahk
#Include COM.ahk

;Below is the code in Class1.cs, which I've compiled
;using "csc /target:library Class1.cs" into Class1.dll
;c# =
;(
;using System;

;namespace HW
;{
;    public class HelloWorldClass
;    {
;        public string SayHello(string name)
;        {
;            return ("Hello " + name);
;        }
;    }
;}
;)

CLR_Start()

asm := CLR_LoadLibrary("Class1.dll")

;asm := CLR_CompileC#(c#, "System.dll") 
;works fine when I use this instead of the line above it 
;and compile the C# code on-the-fly

obj := CLR_CreateObject(asm, "HW.HelloWorldClass")

string := COM_Invoke(obj, "SayHello", "Handsome")

COM_Release(obj)
COM_Release(asm)

MsgBox % string

If I compile the C# code on the fly, the script works fine. But when I try to access the dll, absolutely nothing happens. The MsgBox doesn't even bother to pop up (with or without a message).

I'm very confused - I feel as though there's something obvious I'm overlooking, but for the life of me I can't figure it out. I would greatly appreciate any assistance.

Lexikos
  • Administrators
  • 9442 posts
  • Last active:
  • Joined: 17 Oct 2006

If I compile the C# code on the fly, the script works fine. But when I try to access the dll, absolutely nothing happens. The MsgBox doesn't even bother to pop up (with or without a message).

It works for me on Vista (.NET v2.0.50727). At first when I tried it on XP with v1.1.4322, it would give me a blank MsgBox if I loaded Class1.dll from file, but it would work if I compiled on the fly. I then added an explicit public HelloWorldClass constructor, and suddenly it worked either way..:!: Now when I try it with or without the constructor, it works. :? Talk about confusing...

Which version of .NET are you running?
CLR_GetVersion(sver)
MsgBox % sver
CLR_GetVersion(ByRef sVer)
{
    VarSetCapacity(wsVer,40)
    hr:=DllCall("MSCorEE\GetCORVersion","uint",&wsVer,"uint",20,"uint*",0)
    sVer := COM_Ansi4Unicode(&wsVer)
    return hr
}


AHKnow
  • Members
  • 121 posts
  • Last active: May 17 2009 09:11 PM
  • Joined: 03 Jul 2004
Wondering if it would not be useful for lexikos to take a look at CS-Script- <!-- m -->http://www.members.o... ... index.html<!-- m --> .

I also mentioned this a while go in the utility section- <!-- m -->http://www.autohotke....php?t=4996<!-- m -->

Basically, that outrageous idea is here thanks to lexikos, :D . Curiously, there is an AutoHotkey member named olegbl that did AHKArray.... wondering if there is any relation...

Anyway the source code for CS-Script is available at <!-- m -->http://www.members.o... ... lease.html<!-- m -->.

Seeing how advanced the CLR script has become and how it can already be integrated into AutoHotkey, it might be worth taking a look at CS-Script for ideas.

Also bouncing some questions at the author may be useful as well.

pokercurious
  • Members
  • 48 posts
  • Last active: Dec 03 2008 07:22 PM
  • Joined: 16 Dec 2007
@lexikos, thanks for the quick reply.

I had a long response planned, because it still wasn't working, when I had an insight - if the script wasn't displaying the message box, it wasn't getting TO the messagebox. Now why would it do that?

I found the reason. I had originally commented out the C# code block with /*...*/. What I didn't realize was this (from the manual):

In addition, the /* and */ symbols can be used to comment out an entire section, but only if the symbols appear at the beginning of a line


And obviously, my C# code was indented, which was simply commenting out the entire script.

I've now typed out RTFM, noob at the top of my script. (to be fair, it's actually a little difficult to find that little tidbit about comment syntax on the site).

Final result - I switched to semicolon comments, and the script works beautifully now, with the call to the dll. Thanks very much for your work.

Hope my nick appears in this post, as the previous post called me "Guest" even though I was logged in.

Sean
  • Members
  • 2462 posts
  • Last active: Feb 07 2012 04:00 AM
  • Joined: 12 Feb 2007

I had a long response planned, because it still wasn't working

I suggest to try CreateInstanceFrom to see if it helps:
papp :=	CLR_GetDefaultDomain()
hobj :=	COM_Invoke(papp, "CreateInstanceFrom", A_ScriptDir . "\Class1.dll", "HW.HelloWorldClass")
pobj :=	COM_Invoke(hobj, "Unwrap"), COM_Release(hobj)
str  :=	COM_Invoke(pobj, "SayHello", "Handsome")
PS. Added missing "" in "\Class1.dll".

pokercurious
  • Members
  • 48 posts
  • Last active: Dec 03 2008 07:22 PM
  • Joined: 16 Dec 2007
@Sean

Thanks, that works too.

Don't know if you read the rest of my previous post, but the problems were caused by my misuse of /**/. I didn't know all the commented-out lines had to be at the beginning of the line.

I've been reading through your posts while trying this stuff out - they scare me a little, because there's so much to learn. :)

Thanks for the tip - now I just need to tinker and read a lot more to really understand what's going on.

Sean
  • Members
  • 2462 posts
  • Last active: Feb 07 2012 04:00 AM
  • Joined: 12 Feb 2007

Don't know if you read the rest of my previous post, but the problems were caused by my misuse of /**/.

Sorry, I missed that. I think my sight has been really becoming weaker lately.

I've been reading through your posts while trying this stuff out - they scare me a little, because there's so much to learn.

Take your time, and I'm sure you'll grasp them all in the end.