Table of Contents
R(D)COM .NET Wrapper
by Igor Velikorossov of Insurance Australia Group, ©2004-2007.
An interaction between R and .NET is made possible by R(D)COM and the following article will discuss the implementation of managed wrapper for R(D)COM and IStatConnectorCharacterDevice.
- Adding references
- Character Device
- Manager wrapper
- Basic wrapper
- Extending the wrapper with async calls
- Using the wrapper
Step 1: Adding references
First of all you need to include references to
- StatConnectorCommon 1.1 Type Library (StatConnectorSrv.tlb),
- StatConnectorSrv 1.1 Type Library (StatConnLib.tlb)
both of these you should find at the COM tab when adding project's references. The tlb's are normally located at C:\Program Files\R\(D)COM Server\tlb
Step 2: Character Device
You need to implement IStatConnectorCharacterDevice interface to be able to capture output from R.
The interface is pretty simple and defines only 3 methods.
/// <summary> /// Managed wrapper for <see cref="IStatConnectorCharacterDevice"/> interface. /// </summary> public class CharacterDevice : IStatConnectorCharacterDevice, IDisposable { void IStatConnectorCharacterDevice.Clear() { } void IStatConnectorCharacterDevice.WriteString(string bstrLine) { } void IStatConnectorCharacterDevice.WriteStringLevel(string bstrLine, int lLevel) { } }
The current implementation of the character device is based on RichTextBox control, and provides ability to color-code and indent output. For more details on implmentation please refer to CharacterDevice.cs.
Step 3: Manager wrapper
Now we are ready to implement the actual R(D)COM wrapper.
Basic wrapper
The proposed skeleton for the wrapper will look similar to the following:
/// <summary> /// Managed R(D)Com wrapper. /// </summary> public class Rcom : IDisposable { private StatConnector rsrv = null; private CharacterDevice chardev = new CharacterDevice(); #region public Rcom() /// <summary> /// Initializes a new instance of the <see cref="Rcom"/> class. /// </summary> public Rcom() { this.chardev.DefaultColor = Color.DarkGreen; this.chardev.DefaultIndent = 30; this.rsrv = new StatConnectorClass(); this.rsrv.Init("R"); this.chardev.WriteString("R initialised\r\n\r\n", Color.Navy); this.rsrv.SetCharacterOutputDevice(this.chardev); } #endregion // public Rcom() }
It is important to remember that we need to implement IDisposable interface to ensure that all unmanaged resources are released when the wrapper is no longer used and/or exists:
protected virtual void Dispose(bool disposing) { // Check to see if Dispose has already been called. if (!this.disposed) { this.disposed = true; // If disposing equals true, dispose all managed // and unmanaged resources. if (disposing) { // Dispose managed resources. if (this.chardev != null) { this.chardev.Dispose(); this.chardev = null; } } // Release unmanaged resources. If disposing is false, // only the following code is executed. if (this.rsrv != null) { this.rsrv.Close(); Marshal.ReleaseComObject(this.rsrv); this.rsrv = null; } } }
Extending the wrapper with async calls
It is quite likely that some of operations may take a while to compute and it may be desirable not to lock the application until all of the computations are finished.
To do this let's allow calls to R be made in either sync or async modes:
#region public JobStatus Submit(bool async) /// <summary> /// Submits the job for execution. /// </summary> /// <param name="async"><see langword="true"/> to run job in asynchronous mode; /// <see langword="false"/> otherwise.</param> /// <returns>Job state.</returns> public JobStatus Submit(bool async) { if (this.rsrv == null) return JobStatus.Unknown; this.executionResult = JobStatus.Unknown; this.running = true; if (async) { // you can implement this call if desired //this.OnSubmitting(); ThreadStart start = new ThreadStart(this.Execute); Thread thread = new Thread(start); thread.IsBackground = true; thread.Start(); } else { this.Execute(); } return this.executionResult; } #endregion // public JobStatus Submit(bool async) #region private void Execute() private void Execute() { lock (this.codeLines) { string line = string.Empty; try { this.chardev.StartCapture(); // processing the code lines this.executionResult = JobStatus.Success; } catch (Exception ex) { this.chardev.WriteString("\r\n> " + line + "\r\n", Color.Red); this.chardev.WriteString(ex.ToString(), Color.Red); this.executionResult = JobStatus.Error; } finally { this.chardev.EndCapture(); this.OnFinished(new FinishedEventArgs(this.executionResult, this.chardev.GetCapture(DataFormat.Rtf))); } } } #endregion // private void Execute()
For more details on implmentation please refer to Rcom.cs.
Step 4: Using the wrapper
Using the wrapper is pretty much straight forward:
try { // create teh R server this.rcom = new Rcom(); // wire events this.rcom.LineEvaluated += new LineEvaluatedHandler(rcom_LineEvaluated); this.rcom.Finished += new FinishedEventHandler(rcom_Finished); #region R code // R code this.rcom.CodeLines.AddRange(false, new string[] { "some_r_code_no_return;", "more_of_r_code_no_return;", }); this.rcom.CodeLines.Add("some_r_code_returning_value();", true); this.rcom.CodeLines.AddRange(false, new string[] { // open bmp device "bmp(file=\"c:/pic.bmp\", width=100, height=100);", // plot "par(col=3);", "plot(some_value, some_function(value));", // save "dev.off();", }); } #endregion // R code // execute the code this.rcom.Submit(true); } catch (Exception ex) { MessageBox.Show(this, ex.ToString(), "R call failed", MessageBoxButtons.OK, MessageBoxIcon.Error); }