Skip to content

Ruby talks to AutoLISP

davidbl edited this page Sep 13, 2010 · 1 revision

Often when coding against AutoCAD in some language other than AutoLISP, it is nice to be able to read or write LISP variables. As it turns out, it is now pretty easy to do just that using Ruby and AcadHelper.

I was looking through a dumpbin listing for acad.exe and found this little jewel, ?acedEvaluateLisp@@YAHPB_WAAPAUresbuf@@@Z. I played with a bit without any success so I googled it and found some code by Alexander Rivilis on the autodesk discussion board

So I used Alexander’s code to create a little C# class

  public class RubyLisp
    {
        //This code is taken from a post here http://discussion.autodesk.com/forums/thread.jspa?threadID=522825
        //by  Alexander Rivilis - Thanks Alexander
        [SuppressUnmanagedCodeSecurity]
        [DllImport(@"C:\Program Files (x86)\AutoCAD Civil 3D 2009\acad.exe", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode,
               EntryPoint = "?acedEvaluateLisp@@YAHPB_WAAPAUresbuf@@@Z")]
        extern private static int acedEvaluateLisp(string lispString, out IntPtr result);

        static public Object evalLisp(string lispString)
        {
            IntPtr out_data = IntPtr.Zero;
            acedEvaluateLisp(lispString, out out_data);
            if (out_data != IntPtr.Zero)
            {
                try
                {
                    ResultBuffer ret_data = DisposableWrapper.Create(typeof(ResultBuffer), out_data, true) as ResultBuffer;
                    return ret_data;
                }
                catch
                {
                    return null;
                }
            }
            return null;
        }
}

I compiled that code and then, using IronRuby, I hacked together this.

#wrappers for interfacing with the AutoLISP memory space and AutoLISP Functions


require 'C:/LISP/RubyTest/RubyTest/bin/Release/RubyLisp.dll'


# extensions to the ResultBuffer class for easier access	
class Autodesk::AutoCAD::DatabaseServices::ResultBuffer
	def to_a
		ret_val = []
		self.AsArray.each do |typed_val|
			ret_val << typed_val.Value
		end
		ret_val
	end
	
	def to_full_array
		ret_val = []
		self.AsArray.each do |typed_val|
			ret_val << [typed_val.type_code, typed_val.value]
		end
		ret_val
	end
	
#just return the value	
	def val
		self.AsArray[0].value
	end
	
#just return the code	
	def code
		self.AsArray[0].type_code
	end
	
	def size
		self.AsArray.size
	end

#return the element whose code matches our argument	
	def assoc(code)
		arr = self.to_full_array
		arr.assoc(code)
	end
	
#return just the value of the element whose code matches our argument	
	def cdr_assoc(code)
		arr = self.to_full_array
		arr.assoc(code)[1]
	end
	
end	


#some wrapper methods for the C# EvaluateLisp function

#evaluate a string of LISP code
#examples of valid LISP strings include (setq x 55.5), (setq my_name "David Blackmon"), (entget(entlist)), (load "my_lisp_file")
#but, because the argument must be passed as a string,  we have to escape the quotes when the need them
#actual argument strings would be "(setq x 55.5)", "(setq my_name \"David Blackmon\")"
	def lisp_eval(lisp_string)
		ret_val = RubyLisp::RubyLisp.evalLisp(lisp_string)		
		return nil if ret_val.code == 5019
		ret_val
	end
	
#load a LISP file.  Raising ArgumentError is file is not in the AutoCAD search path	
	def lisp_load(file_name)
		find_file_res = lisp_eval "(findfile  \"#{file_name}\")"

		if find_file_res
			lisp_eval "(load \"#{file_name}\")" 
		else 
			raise ArgumentError, "file #{file_name} not found"	
		end	
	end

#a wrapper for LISP setq - now only works for simple assigns such as (setq x 5), (setq y "abc")	
#and lists (setq my_list (list 1 2 3 "test" "foo" "bar" 3.1419))
#the sym argument can be a Ruby symbol or a string
#valid calls are setq("x", 55.5), setq(:x, 55.5), setq("y", "this is a string variable")
#setq(:my_list, (list 22, 33, 44))
	def setq(sym, expr)
		if expr.is_a?(String)
			if expr.match(/\(list/)	
				str = "(setq #{sym.to_s} #{expr.to_s})" 
			else	
				str = "(setq #{sym.to_s} \"#{expr.to_s}\")" 
			end	
			puts str
		else
			str = "(setq #{sym.to_s} #{expr.to_s})" 
			puts str

		end	
		lisp_eval str
	end

#get the value of a LISP symbol, passed a Ruby symbol,eg getq(:x), getq(:my_list)	
	def getq(sym)
		ret_val = lisp_eval "(vl-symbol-value \'#{sym.to_s} )"
	end

Using these new wrappers, we can write cool stuff such as

def lisp_load_test
	begin
		lisp_load "rubytest"
		x = lisp_load "rubytestasdfasdf"
		puts x.inspect
	rescue Exception => e
		puts_ex e
	end		
end

def lisp_test
	begin
		#create some variables in LISP memory space
		setq(:x, 25) #an integer
		setq('y', "abc asdf 12")  #a string
		setq('z', '(list 1 2.1 "asdf" 3.0 4.46464)') # a list

        #try to load an LSP file named 'rubytest.lsp'. it must be in the search path
		lisp_load "rubytest"

		#try to read an existing LISP variable
		lsp_string = "(eval ruby_test)"
		lisp_val = lisp_eval lsp_string
		puts lisp_val.inspect
		
		#send some lisp code and look at the return value
		lsp_string = "(entget (entlast))"
		ret_val = lisp_eval lsp_string
		puts ret_val.to_full_array.inspect
		
		#we can iterate over the entire entity list, searching for our match
		ret_val.each do |typed_val|
			puts "Layer of last entity in drawing is #{typed_val.Value}" if typed_val.TypeCode == 8
		end
		
		#or we can reach into the list like we would in LISP using ResultBuffer#cdr_assoc
		layer_name = ret_val.cdr_assoc(8)
		puts "Layer of last entity in drawing is #{layer_name}"
		
		#we can also get the full assoc element
		puts ret_val.assoc(40).inspect

        #get the values of the LISP variables we set above		
        #we can get the value as an array of values
		qq = getq(:z).to_a
		puts qq.inspect
		qq = getq(:x).to_a
		puts qq.inspect
		
		#we can get the value as the pure ResultBuffer
		qq = getq(:Pi)
		puts qq.inspect
		
		#we can get just the value without the type data
		qq = getq(:Pi).val
		puts qq
		
		#we can get just the type data
		puts getq(:Pi).code
		
		#we can get the size of the result buffer
        puts getq(:z).size

        #we can get a nest array of [type_codes, values] for the entire ResultBuffer
        puts getq(:z).to_full_array.inspect

	rescue Exception => e
		puts_ex e
	end
	
end

This code will be added to the master repo soon

Have fun!

ps. Next up, Block insertion made fun and easy