Tuesday, October 28, 2008

Inter-Process Communication (IPC) with Autodesk Map3d

Author: Jonio, Dennis

Prospective and Environment
Everything that follows was done on and with Windows XP SP2, Visual Studio 2005 Professional SP1, Oracle XE 10.2, Oracle ODP.NET 11g(2.111.6.20), Autodesk Map3d 2008 SP1, Autodesk ObjectARX 2008 and lots of reading and lots of research. Since I have not done it myself I cannot attest as to the viability of this solution in a vanilla AutoCAD 200x environment. I am however inclined to believe that it will work just fine. I make neither guarantees nor claims of fitness for any purpose. It is “as-is” so proceed at your own risk.

Overview
Simply stated, this was the problem, I wished to have a generic way in which to communicate with a DWG. All I wished to do was PUT an SDO_GEOMETRY and GET a selected entity back as an SDO_GEOMETRY. This seemed to me to be a not unreasonable request. Just think about this a second: AutoCAD as a COLUMN editor? I could build whatever user interface I wished, using whatever tools I wished. Include all the business logic needed and then just “plumb” this stand-alone application. I didn’t have to solve the world’s problems either. Points, lines and surfaces were all I was interested in. Well good luck! First off there was no .NET DWG to SDO_GEOMETRY translator that I could get my hands on. Second, this just is not how Autodesk has written their applications. Don’t misunderstand me here. This is not a flame against Autodesk. Their applications do what they were designed to do and do this very well. So this series is then about getting one of two major things accomplished: 1) A 2-dimensional, SDO_GEOMETRY translator that supported arcs and 2) A communications interface.

This is about the communications piece. In my mind this would be the most technically challenging and if I was going to fail it would be on this.

Of course the issue of multithreading arose numerous times but it always seemed, by its nature, to be somewhat limiting. Inter-process communication (IPC) was the answer but I found no ready made solution. I had to piece this together myself. I did a fair amount of investigating and found named pipes to be the most generic and robust approach to take.

This is not really a story about pipes but you must look at this article: “Inter-Process Communication in .NET Using Named Pipes” by Ivan Latunov.
link to article The author explains it all much better than I ever could and his implementation is the backbone of this implementation. By the way, I believe that Microsoft has now included Named Pipe support in the 3.5 .NET framework. Since MS pipes are a Client/Server architecture and I was totally intimidated by “overlapping” ,“bidirectional” and the other more complex implementations I decided to really simplify and incorporate, in each executable or dll, a pipe Server and a pipe Client. Thank you Ivan. The significant change that I incorporated was the use of Events on the Server side( the “owner” side if you will ). When you compare Ivan’s code to mine you will see some other minor changes. A simple piccy to illustrate the idea.


Since experience has taught me that the Autodesk API’s are not trivial especially when it comes to the MDI interface structure I really needed to isolate this application as much as possible. The solution was a simple data exchange mechanism, a data “bridge”, if you will. Let the bridge become the generic between the other executables! My NETLOADed module and the “bridge” will communicate with simple Events and Queue objects! The Autodesk API’s understands Forms, Forms are, by nature, multi-threaded! The bridge will then communicate to whatever is at the other end of the pipe. BINGO! We have it all!

Below is a clip where these ideas are put together. Very simple and straightforward but it should give you some "food for thought":

Full-size video clip is available for download at:


The Communications Data Definition

OK… so now we have settled on the communications “plumbing”, a full blown IO channel no less, what is it that we wish to communicate?
Well in reading Ivan’s work and the work of some others I became aware that pipes can move megabytes of data in sub-second times. Thusly then, the amount was not the issue the format was. Whatever that format was it had to be serializable and it had to be generic. I had to accomplish two(2) things: 1) construct a request that I wished my NETLOADed module to perform 2) provide for data interchange. The solution here is a plain’ol .NET dataset. In fact, all I really had to do was encapsulate a dataset inside a field in a “control” dataset. I could then pass one(1) serializabe object that had both a request/task and corresponding data. In my mind at least, very clean, very straightforward, and very generic.

The following is the structure I decided upon for my environment:


public static DataSet MaterializeNPControlDataSet()
{
DataSet _ds = new DataSet("NPCONTROLDATASET");
_ds.RemotingFormat = SerializationFormat.Binary;
_ds.SchemaSerializationMode = SchemaSerializationMode.IncludeSchema;

_ds.Tables.Add("NPCONTROLTABLE");
DataColumn dc1 = new DataColumn("ID", typeof(Int32));
DataColumn dc2 = new DataColumn("TEXT", typeof(string));
DataColumn dc3 = new DataColumn("PAYLOADTEXT", typeof(string));
DataColumn dc4 = new DataColumn("PAYLOAD", typeof(object));
_ds.Tables[0].Columns.Add(dc1);
_ds.Tables[0].Columns.Add(dc2);
_ds.Tables[0].Columns.Add(dc3);
_ds.Tables[0].Columns.Add(dc4);
return _ds;
}


To support the manipulation of this structure there is one(1) class. All of the serialization/de-seriaization and encapsulations are done with this class. Below are the “putter” and “getter” methods for the dataset that is encapsulated within the “PAYLOAD” field. In addition there are numerous other helper methods within the class.
”putter”

public static void SetNPControlTablePayLoad(ref DataSet _NPds, ref DataSet _payload)
{
byte[] ba = SerializeDataSet(ref _payload);
_NPds.Tables["NPCONTROLTABLE"].Rows[0]["PAYLOAD"] = (object)ba;
}

“getter”

public static DataSet ExtractNPCTablePayLoad(ref DataSet _NPds)
{
DataSet ads = null;
object o = (object)_NPds.Tables["NPCONTROLTABLE"].Rows[0]["PAYLOAD"];
if (o != System.DBNull.Value)
{
ads = ExtractSerializedDataSet((byte[])o);
if (ads != null)
return ads;
else
return null;
}
else
return null;
}

No, I did not implement any compression of the datasets, just the BinaryFormatter.

public static byte[] SerializeDataSet(ref DataSet _ds)
{
_ds.RemotingFormat = SerializationFormat.Binary;
_ds.SchemaSerializationMode = SchemaSerializationMode.IncludeSchema;
MemoryStream ms = new MemoryStream();
BinaryFormatter bf = new BinaryFormatter();
bf.Serialize(ms, _ds);
if (ms.Length > 0)
return ms.ToArray();
else
return null;
}

The Named Pipes
Ivan’s implementation is a work of art. I really hated to touch it. (Not only because it was well put together but I really did not fully comprehend his threading architecture). However, I really needed event hooks because this wasn’t going to be a “classic” Client/Server application. The applications that were going to be incorporating this were going to be operating in and supporting the user interface world. Since Ivan had architected this with interfaces I went about some touch up. (four lines of code with some big ideas ...)
Source code (C#):

using System;
namespace AppModule.InterProcessComm {
// Delegates for events that your component will use to communicate
// values to the form
public delegate void ReceiveCompleteHandler(byte[] ba);
public delegate void StatusActivityHandler(string info);

public interface IChannelManager {
// events that this (class)component will use to communicate with the app
event ReceiveCompleteHandler OnReceiveComplete;
event StatusActivityHandler OnStatusActivity;

void Initialize();
void Stop();
string HandleRequest(string request);
bool Listen {get; set;}
void WakeUp();
void RemoveServerChannel(object param);
}
}

Since I had decided to have both a Client and a Server incorporated within each app I made it easier on myself and just constructed an “A” and “B” dispatcher (I wanted to get rid of this Client/Server label). One would be owned by the “bridge” the other would be owned by the “other” application.
They are really nothing more than containers for Ivan’s public static IChannelManager PipeManager; I just added a few constants to make life easier and to set the “ReadOn” pipe and the “SendOn” pipe. I really didn’t want any confusion later on.

PipeManager’s most significant modifications had to do with the Events hooks:
Source code (C#):

if (numChannels <= NumberPipes) // =>
{
ServerNamedPipe pipe = new ServerNamedPipe(PipeName, OutBuffer, InBuffer, MAX_READ_BYTES, false);
// subscribe to the events raised when data has been gotten
pipe.GotNewByteArray += new OnNewByteArrayHandler(pipe_GotNewByteArray);
pipe.GotActivity += new OnActivityToReportHandler(pipe_GotActivity);
try
{
pipe.Connect();
pipe.LastAction = DateTime.Now;
System.Threading.Interlocked.Increment(ref numChannels);
pipe.Start();
Pipes.Add(pipe.PipeConnection.NativeHandle, pipe);
}
catch (InterProcessIOException ex) {
RemoveServerChannel(pipe.PipeConnection.NativeHandle);
// unsubscribe to the events raised when data has been gotten
pipe.GotNewByteArray -= new OnNewByteArrayHandler(pipe_GotNewByteArray);
pipe.GotActivity -= new OnActivityToReportHandler(pipe_GotActivity);
pipe.Dispose();
}
}
else {
Mre.Reset();
Mre.WaitOne(1000, false);
}

Now, I just pass it on up the chain to “whatever” has hooked up.
Source code (C#):

void pipe_GotActivity(object sender, string activity)
{
if (OnStatusActivity.Target is System.Windows.Forms.Control)
{
// make sure execution is done on the UI thread
System.Windows.Forms.Control t = OnStatusActivity.Target as System.Windows.Forms.Control;
t.BeginInvoke(OnStatusActivity, new Object[] { activity });
}
else
// object from the invocation list isn't a UI thread.
OnStatusActivity(activity);
}
}
void pipe_GotNewByteArray(object sender, byte[] ba)
{
string rcvszdttm = string.Format("Received: {0} bytes at {1}", ba.Length.ToString(), DateTime.Now);
// tell whoever that a worker has just sent us a byte array
pipe_GotActivity(this, rcvszdttm);
//
// determine if the object on which this delegate was invoked is a UI thread
// if the object is a control, then the object is a UI thread
//
if (OnReceiveComplete.Target is System.Windows.Forms.Control)
{
// make sure execution is done on the UI thread
System.Windows.Forms.Control t = OnReceiveComplete.Target as System.Windows.Forms.Control;
t.BeginInvoke(OnReceiveComplete, new Object[] { ba });
}
else // object from the invocation list isn't a UI thread.
OnReceiveComplete(ba);
}

Finally public sealed class ServerNamedPipe had to be tweaked. In this implementation I always send an acknowledgment string after a receipt. It is client/server after all. So it is a receive binary / send a string.
Source code (C#):

public event OnNewByteArrayHandler GotNewByteArray;
public event OnActivityToReportHandler GotActivity;
private void PipeListener() {
CheckIfDisposed();
try {
Listen = DspchrHandlerA.PipeManager.Listen;
string myName = "Pipe_" + this.PipeConnection.NativeHandle.ToString();
GotActivity(new object(), myName + " :new pipe started");
while (Listen)
{
LastAction = DateTime.Now;
byte[] request = PipeConnection.ReadBytes();
LastAction = DateTime.Now;

// pipe manager gets the data
GotNewByteArray(this, request);
// this goes to the sender's listener to signal OK
PipeConnection.Write("ACKNOWLEDGEMENT " + request.Length.ToString() + " bytes");
//pipe manager get more info
GotActivity(new object(), myName + " acknowledgement sent for receipt of " + request.Length.ToString() + " bytes");

LastAction = DateTime.Now;
PipeConnection.Disconnect();
if (Listen) {
GotActivity(new object(), myName + " :listening");
Connect();
}
//
DspchrHandlerA.PipeManager.WakeUp();
}
}
catch (System.Threading.ThreadAbortException ex) { }
catch (System.Threading.ThreadStateException ex) { }
catch (Exception ex) {
// Log exception
}
finally {
this.Close();
}
}

Believe me named pipes and threads are NOT my specialty. I just had to know and do just enough to make it work for me. Yes, I am sure there are different and probably better ways. But I am a pragmatist and was not doing this for fun.

So we now have our pipes and we have an A and a B configuration. It matters not who is who but obviously partners cannot have the same configuration. As a final note these configurations are coded to operate on one workstation. You can of course run across different machines but that is a whole different topic.

The Data Bridge
This is the Form that will be instantiated from our NETLOADed module. As I mentioned before I wanted this to be as simple and generic as possible. Just a couple public Queue objects and a few events. I did add some UI Controls to play with during debug and never took them out.

How it works:
  1. Receive an entry on “my” pipe
  2. EnQueue the entry into the Suspense Queue
  3. Fire off an OnReceivedQueueEntry event
OR
  1. Receive an OnCompletedTask event
  2. DeQueue the entry from the Process queue
  3. Send it on to the other “Server’
If there is a simpler approach I could not think of it. Since a picture is worth so many words…



And the entire source sans the UI controls for the bridge:

using System;
using System.Data;
using System.Drawing;
using System.ComponentModel;
using System.Windows.Forms;
using System.Collections;

using DspchrHandlers;
using Dspchr;

namespace DataBridge
{
public partial class DataBridge : Form
{
public delegate void ReceivedQueueEntryHandler(object sender);
public event ReceivedQueueEntryHandler OnReceivedQueueEntry;

public Queue SuspenseQ;
public Queue ProcessQ;

public SendVia SendViaX;
IDataBridge iDataBridgeClient;

public DataBridge(IDataBridge parent)
{
InitializeComponent();
iDataBridgeClient = parent;
DspchrHandlerA.StartServer();
SuspenseQ = new Queue();
ProcessQ = new Queue();
SendViaX = new SendVia();
DspchrHandlerA.PipeManager.OnReceiveComplete += new AppModule.InterProcessComm.ReceiveCompleteHandler(PipeManager_OnReceiveComplete);
DspchrHandlerA.PipeManager.OnStatusActivity += new AppModule.InterProcessComm.StatusActivityHandler(PipeManager_OnStatusActivity);
iDataBridgeClient.OnCompletedTask += new CompletedTaskHandler(iDataBridgeClient_OnCompletedTask);
}

void iDataBridgeClient_OnCompletedTask(object sender)
{
lblPQCnt.Text = ProcessQ.Count.ToString();
string rtn_result = null;
if (ProcessQ.Count > 0)
{
rtn_result = SendViaX.WriteBytes(DspchrHandlerA.SendOnPipeName, DspchrHandlerA.ServerName, (byte[])ProcessQ.Dequeue());
txtActivity.AppendText(rtn_result + Environment.NewLine);
}
lblPQCnt.Text = ProcessQ.Count.ToString();
lblSQCnt.Text = SuspenseQ.Count.ToString();
}
//
void PipeManager_OnStatusActivity(string info)
{
txtActivity.AppendText(info + Environment.NewLine);
}
void PipeManager_OnReceiveComplete(byte[] ba)
{
SuspenseQ.Enqueue(ba);
lblSQCnt.Text = SuspenseQ.Count.ToString();
Application.DoEvents();
OnReceivedQueueEntry(this);
}
private void DataBridge_FormClosing(object sender, FormClosingEventArgs e)
{
DspchrHandlerA.StopServer();
}
//
// Some form relate stuff to mess around with
//
private void btnRmvSQE_Click(object sender, EventArgs e)
{
if (SuspenseQ.Count > 0) SuspenseQ.Dequeue();
lblSQCnt.Text = SuspenseQ.Count.ToString();
}
private void btnRmvPQE_Click(object sender, EventArgs e)
{
if (ProcessQ.Count > 0) ProcessQ.Dequeue();
lblPQCnt.Text = ProcessQ.Count.ToString();
}
private void btnReturnSQE_Click(object sender, EventArgs e)
{
string rtn_result = null;
if (SuspenseQ.Count > 0)
{
rtn_result = SendViaX.WriteBytes(DspchrHandlerA.SendOnPipeName, DspchrHandlerA.ServerName, (byte[])SuspenseQ.Dequeue());
lblSQCnt.Text = SuspenseQ.Count.ToString();
txtActivity.AppendText(rtn_result + Environment.NewLine);
}
}
private void btnCLEARLOG_Click(object sender, EventArgs e)
{
txtActivity.Clear();
}
}
}
The last piece of the puzzle is this interface object for the DataBridge so it knows that there was something to be done.

using System;
using System.Collections.Generic;
using System.Text;

public delegate void CompletedTaskHandler(object sender);
public interface IDataBridge
{
event CompletedTaskHandler OnCompletedTask;
}
And a skeleton of a NETLOADable dll:

namespace ADSKDATABRIDGE
{
public class WRKR : IDataBridge
{
public event CompletedTaskHandler OnCompletedTask;
public DataBridge.DataBridge DataBridgeForm;

private static object TestForDuplication = null;
public WRKR() { }

[CommandMethod("DOPUTGET")]
public void dome()
{
if (TestForDuplication == null)
{
DataBridgeForm = new DataBridge.DataBridge(this);
TestForDuplication = DataBridgeForm;
DataBridgeForm.FormClosing += new FormClosingEventHandler(DataBridgeForm_FormClosing);
DataBridgeForm.OnReceivedQueueEntry += new DataBridge.DataBridge.ReceivedQueueEntryHandler(DataBridgeForm_OnReceivedQueueEntry);
DataBridgeForm.Show();
}
}
void DataBridgeForm_FormClosing(object sender, FormClosingEventArgs e)
{
DataBridgeForm.OnReceivedQueueEntry -= new DataBridge.DataBridge.ReceivedQueueEntryHandler(DataBridgeForm_OnReceivedQueueEntry);
TestForDuplication = null;
}
Finally … we receive our task and do it

void DataBridgeForm_OnReceivedQueueEntry(object sender)
{
Document doc = Autodesk.AutoCAD.ApplicationServices.Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
bool GONOGO = true;
DataSet CntrlDS;
DataSet ToDoDS;
byte[] RtnVal = null;
if (this.DataBridgeForm.SuspenseQ.Count > 0)
{
while (GONOGO && this.DataBridgeForm.SuspenseQ.Count > 0)
{
object o = DataBridgeForm.SuspenseQ.Dequeue();
try
{
CntrlDS = NPC.ExtractSerializedDataSet((byte[])o);
if (CntrlDS != null)
{
if (NPC.HasPayLoad(ref CntrlDS))
{
ToDoDS = NPC.ExtractNPCTablePayLoad(ref CntrlDS);
if (ToDoDS != null)
{
RtnVal = ProcessRequest(ref CntrlDS, ref ToDoDS);
}
}
}
if (RtnVal != null)
DataBridgeForm.ProcessQ.Enqueue(RtnVal);
OnCompletedTask(this);
RtnVal = null;
}
catch (System.Exception _Ex)
{
ed.WriteMessage("ERROR: " + _Ex.Message + "\r\n");
}
} // end while GONOGO
}// if count in queue
}// OnReceivedQueueEntry
}//eoc WRKR
} // eons

No comments:

Post a Comment