[I’ve now started pushing links to my posts out through Twitter, even if I haven’t gone quite so far as to abandon TypePad (yes, it was an April Fools’ joke, in case anyone missed the closing comment :-)].
Having spent some time looking into Python, I decided to give Ruby – another popular scripting language and one with an “Iron” implementation allowing you to work with .NET – the same treatment.
From what I can tell – and I’m really a newbie in both these languages – there is relatively little to separate the two: both Ruby and Python have their devotees but they ultimately to belong to different sects of the same faith (it’s hard to escape religious analogies when talking about programming languages, for some reason :-). That said, the fiercest arguments often seem to occur between people who very nearly agree. This is not something I know enough about to get involved in – even if I wished to – so I’m going to steer well clear of it, and simply show some simple Ruby code that does the same as in the equivalent post for IronPython. If you Google “python ruby comparison” you should find plenty of opinions out there.
To get started I installed the most recently released version of IronRuby at the time of writing, version 0.3.
As with IronPython, we’re going to use a C# loader inside AutoCAD to host the IronRuby runtime and use it to interpret a Ruby script. The code to do this is very similar to that needed for IronPython, and it should, in fact, be simple enough to templatize the code in some way and combine the two into a single implementation (something I expect Tim Riley will be doing with PyAcad.NET, when he gets the chance). I’m leaving the implementations separate, for now, to make it easier for people to work with the languages independently.
Here is the C# code defining our RBLOAD command. You’ll need to add project references to IronRuby.dll, IronRuby.Libraries.dll, Microsoft.Scripting.dll and Microsoft.Scripting.Core.dll, all of which can be found in IronRuby’s bin folder. You’ll clearly also need to reference the usual acmgd.dll and acdbmgd.dll.
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.EditorInput;
using IronRuby.Hosting;
using IronRuby;
using Microsoft.Scripting.Hosting;
using System;
namespace RubyLoader
{
public class CommandsAndFunctions
{
[CommandMethod("-RBLOAD")]
public static void RubyLoadCmdLine()
{
RubyLoad(true);
}
[CommandMethod("RBLOAD")]
public static void RubyLoadUI()
{
RubyLoad(false);
}
public static void RubyLoad(bool useCmdLine)
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
short fd =
(short)Application.GetSystemVariable("FILEDIA");
// As the user to select a .rb file
PromptOpenFileOptions pfo =
new PromptOpenFileOptions(
"Select Ruby script to load"
);
pfo.Filter = "Ruby script (*.rb)|*.rb";
pfo.PreferCommandLine =
(useCmdLine || fd == 0);
PromptFileNameResult pr =
ed.GetFileNameForOpen(pfo);
// And then try to load and execute it
if (pr.Status == PromptStatus.OK)
ExecuteRubyScript(pr.StringResult);
}
[LispFunction("RBLOAD")]
public ResultBuffer RubyLoadLISP(ResultBuffer rb)
{
const int RTSTR = 5005;
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
if (rb == null)
{
ed.WriteMessage("\nError: too few arguments\n");
}
else
{
// We're only really interested in the first argument
Array args = rb.AsArray();
TypedValue tv = (TypedValue)args.GetValue(0);
// Which should be the filename of our script
if (tv != null && tv.TypeCode == RTSTR)
{
// If we manage to execute it, let's return the
// filename as the result of the function
// (just as (arxload) does)
bool success =
ExecuteRubyScript(Convert.ToString(tv.Value));
return
(success ?
new ResultBuffer(
new TypedValue(RTSTR, tv.Value)
)
: null);
}
}
return null;
}
private static bool ExecuteRubyScript(string file)
{
// If the file exists, let's load and execute it
bool ret = System.IO.File.Exists(file);
if (ret)
{
try
{
ScriptEngine engine = Ruby.CreateEngine();
engine.ExecuteFile(file);
}
catch (System.Exception ex)
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
ed.WriteMessage(
"\nProblem executing script: {0}", ex.Message
);
}
}
return ret;
}
}
}
Here’s my first attempt at a Ruby script which does basically the same as my original attempt for IronPython. I did find that not every .rb file was loadable by the RBLOAD command - presumably it only accepts certain text encoding standards – so I ended up pasting the code into a copy of one of the standard .rb files shipping in the IronRuby lib folder.
require 'C:\Program Files\Autodesk\AutoCAD 2009\acmgd.dll'
require 'C:\Program Files\Autodesk\AutoCAD 2009\acdbmgd.dll'
require 'C:\Program Files\Autodesk\AutoCAD 2009\acmgdinternal.dll'
# Function to register AutoCAD commands
def autocad_command(name)
cc =
Autodesk::AutoCAD::Internal::CommandCallback.new method(name)
Autodesk::AutoCAD::Internal::Utils.AddCommand(
'rbcmds', name, name,
Autodesk::AutoCAD::Runtime::CommandFlags.Modal, cc)
# Let's now write a message to the command-line
app = Autodesk::AutoCAD::ApplicationServices::Application
doc = app.DocumentManager.MdiActiveDocument
ed = doc.Editor
ed.WriteMessage("\nRegistered Ruby command: {0}", name)
end
def add_commands(names)
names.each { |n| autocad_command n }
end
# A simple "Hello World!" command
def msg
app = Autodesk::AutoCAD::ApplicationServices::Application
doc = app.DocumentManager.MdiActiveDocument
ed = doc.Editor
ed.WriteMessage "\nOur test command works!"
end
# And one to do something a little more complex...
# Adds a circle to the current space
def mycir
app = Autodesk::AutoCAD::ApplicationServices::Application
doc = app.DocumentManager.MdiActiveDocument
db = doc.Database
tr = doc.TransactionManager.StartTransaction
bt =
tr.GetObject(
db.BlockTableId,
Autodesk::AutoCAD::DatabaseServices::OpenMode.ForRead)
btr =
tr.GetObject(
db.CurrentSpaceId,
Autodesk::AutoCAD::DatabaseServices::OpenMode.ForWrite)
cir =
Autodesk::AutoCAD::DatabaseServices::Circle.new(
Autodesk::AutoCAD::Geometry::Point3d.new(10, 10, 0),
Autodesk::AutoCAD::Geometry::Vector3d.ZAxis, 2)
btr.AppendEntity(cir)
tr.AddNewlyCreatedDBObject(cir, true)
tr.Commit
tr.Dispose
end
add_commands ["msg", "mycir"]
The code differs slightly in approach than the corresponding Python script: I wasn’t aware of an equivalent method for Python decorators and so used a more explicit approach for adding AutoCAD commands. The code uses the same internal (and unsupported) assembly, acmgdinternal.dll, to register the commands dynamically with AutoCAD – that piece is basically the same – it’s just the method of choosing the functions to register that differs (see the call to add_commands() at the bottom of the script).
Otherwise the code is very similar, aside from the use of namespace aliases in IronPython (something that may very well exist in IronRuby – I just haven’t come across it yet).
The execution is analogous to the Python version. When we build and NETLOAD our RubyLoader C# application and execute the RBLOAD command, we can select our Ruby script:
Command: RBLOAD
Once selected, the script gets loaded and should register a couple of commands:
Registered Ruby command: msg
Registered Ruby command: mycir
Running the MSG command will execute a simple “Hello World!”-like function, just printing a message to the command-line:
Command: MSG
Our test command works!
And running the MYCIR command should just add a simple circle to the current space in the active drawing.
Command: MYCIR