Calling External Programs

There are various ways that SIMION can call external code or programs:

  • Calling external programs via the command-line interface, using Lua functions like os.execute() or io.popen(). The program must support a command-line interface. There is the overhead of starting up the program, so this method is typically not so suitable if you need to call the code, say, thousands of times per second. Details are given below.
  • Calling external programs via COM (luacom). The program must support a COM interface (e.g. like Excel), and this is only supported in Windows. The call may be in-process or out-of-process depending on the object, but in either case it is relatively efficient for doing that. SIMION Example: excel uses this extensively.
  • Writing some code in a language like C, compiling it to a DLL, and loading that into Lua. This method can be very efficient, with very little (though not zero) overhead between calls, but it requires some coding and compiling. See the SIMION Example: extension example and C API (Lua 5.1 Reference Manual).
  • Using TCP/IP sockets. You can use the Lua library luasockets to send/receive data with some service listening on a TCP/IP socket, or even a web server. This library is bundled with SIMION 8.1.
  • Using ZeroMQ (ZMQ). For parallel processing, such as communication between multiple SIMION instances, or other programming languages, it can be preferrable to use ZeroMQ rather than TCP/IP sockets. ZeroMQ may provide a more efficient and simpler interface to TCP/IP sockets (and often implemented over TCP/IP sockets). It is possible for SIMION to communicate with other SIMION instances or with other programming languages (e.g. Perl, Python, Java, C#, etc.) via ZeroMQ. The ZeroMQ library is bundled with SIMION 8.2. See SIMION Example: multiprocess_zmq (8.2) for usage.
  • Using named pipes: Although usable via Windows too, this is primarily suggested for Linux. Named pipes can be read/written just like normal files with the Lua io library, but the data is sent from/to a program. SIMION Example: gnuplot uses named pipes on Linux.

Using the Command-Line Interface

The Lua os.execute() function can be used to run an external program from SIMION. The Lua io.popen() function is similar but captures any data written to standard output as a string (or writes a string to standard input). API details on these functions are in the Lua Reference Manual.

For example, either of these will open a certain file in Windows Notepad:

os.execute 'c:/windows/notepad.exe "c:/Program Files/SIMION-8.1/README.html" '
os.execute 'notepad.exe "c:/Program Files/SIMION-8.1/README.html" '

(The shorter form can be used since the directory c:\windows\ is in the system PATH environment variable.)

This will start Windows Notepad but not block SIMION:

os.execute 'start notepad.exe "c:/Program Files/SIMION-8.1/README.html" '

This will load the given file in whatever is the default program in the Windows file associations (e.g. .txt probably with a text editor, .html probably with a web browser, and .csv probably with Excel):

os.execute ' "c:/Program Files/SIMION-8.1/README.html" '
os.execute ' "c:/Program Files/SIMION-8.1/examples/field_array/solenoid.csv" '

Be careful about backslashes \ and forward slashes /. For strings surrounded by single or double quotes, backslashes must be escaped with an additional backslash. You can often avoid this by just using forward slashes or quote strings with [[ ]], which ignores backslash escape sequences. The following three commands are all valid:

os.execute 'notepad.exe "c:\\Program Files\\SIMION-8.1\\README.html" '
os.execute 'notepad.exe "c:/Program Files/SIMION-8.1/README.html" '
os.execute [[notepad.exe "c:\Program Files\SIMION-8.1\README.html" ]]

In some cases, the Windows program or shell does not recognize forward slashes and you need to instead use backslashes. Be careful also about spaces in program arguments; for example, the space in Program Files requires the full path to be quoted. According to Windows conventions, some of these quoting rules are a little obscure, and Linux has somewhat different rules. Some resort to writing a batch file (.bat) to disk and then just invoking that batch file without arguments.

The command line interface may also be used to execute scripts in other languages, like Python/Perl/Matlab/Octave:

os.execute 'python myprogram.py 1 2 3'
os.execute 'perl myprogram.pl  1 2 3'
os.execute 'c:/cygwin/bin/perl.exe myprogram.pl 1 2 3'

The full path must be specified (like the last example above) if the folder containing the program is not listed in the system PATH environment variable, which you can display like this:

print(os.getenv('PATH'))

Depending on the interface supported by the called program, you may pass data through command-line arguments or files (e.g. 1 2 3), including perhaps a file name. If the program reads from standard input, then appending e.g. <input.txt sends the contents of the file input.txt through standard input. If the program writes to standard output, then appending e.g. >output.txt writes standard output to the file output.txt:

os.execute 'c:/cygwin/bin/perl.exe myprogram.pl my.txt <input.txt >output.txt'

Some helper functions may be useful:

local function read_file(path)
    local fh = assert(io.open(path))
    data = fh:read'*a'
    fh:close()
    return data
    end
local function write_file(path, data)
    local fh = assert(io.open(path, 'w'))
    fh:write(data)
    fh:close()
    end
local function exec(cmd, input)
    local input_path = os.getenv('TEMP') .. os.tmpname()
    local output_path = os.getenv('TEMP') .. os.tmpname()
    os.remove(input_path)
    os.remove(output_path)
    write_file(input_path, input)
    cmd = cmd .. ' <"' .. input_path .. '" >"' .. output_path .. '"'
    assert(os.execute(cmd) == 0, cmd)
    local output = read_file(output_path)
    return output
end

print(exec('c:/cygwin/bin/perl.exe -ne "print $_ * 2"', "1\n2\n3"))
-- uses Perl do double the numbers, printing "246"

An alternate function io.popen() can also be used to execute a program and either capture standard output to a string or send a string to its standard input. One advantage is that it does so without reading/writing to a temporary file, but the disadvantage is that it doesn’t both read and write at the same time. Some helper functions may be useful:

local function execr(cmd)
    local fh = assert(io.popen(cmd))
    local data = fh:read('*a')
    fh:close()
    return data
end
print(execr [[c:/cygwin/bin/perl.exe -e 'for my $x (1..3) { print "$x\n"; } ']])
-- prints "1 2 3" to the SIMION Log window.

local function execw(cmd, data)
    local fh = assert(io.popen(cmd, 'w'))
    fh:write(data)
    fh:close()
end
execw ([[c:/cygwin/bin/perl.exe -e 'for my $x (<STDIN>) { print $x*2, "\n" for 1..1000000; } ' >out.txt]], "1\n2\n3")
-- writes "1 2 3" to the file out.txt.

But if you need to both send and receive extensive data to/from the command, you generally need to instead just have the command read/write temporary files, like the exec function above.

Another advantage of io.popen is that it can read/write multiple chunks of data from/to the program over time, while keeping the program in memory. The SIMION Example: gnuplot uses this to invoke the gnuplot plotting program and update the plot over time by sending more data.

Using COM

See luacom.

Using a DLL

See SIMION Example: extension example and C API (Lua 5.1 Reference Manual). Not only C code, but other languages can be compiled as DLLs (e.g. Fortran).

Using LuaSockets

The following example uses LuaSockets to retrieves a web page using raw TCP/IP:

local socket = require "socket"
local client = socket.tcp()
assert(client:connect('www.google.com', 80))
client:send('GET / HTTP/1.1\nHost: www.google.com\nConnection: close\n\n')
local result = client:receive'*a'
client:close()
print(result)

There are higher-level interfaces that implement the HTTP protocol (over TCP/IP), like socket.http, if you want that:

local http = require "socket.http"
local data,code,headers,status = http.request("http://www.google.com/")
print(data)
print('code=', code, 'status=', status)
if headers then for k,v in pairs(headers) do print('header', k,v) end end

SIMION Example: multiprocess used sockets, though ZeroMQ is now preferred.

Using ZeroMQ (ZMQ)

The ZeroMQ library is bundled with SIMION 8.2. See SIMION Example: multiprocess_zmq (8.2) for usage.

SIMION Example: multiprocess_zmq/client_server also shows simple client-server implementations in multiple languages (Lua/Python/Perl/C/C#/F#).

Using Named Pipes (Linux)

Here is an example of creating and writing to a named pipe that is read by another program (in Linux):

os.execute'mkfifo /tmp/mypipe'
local fh = assert(io.open('/tmp/mypipe', 'w'))
fh:write('test')
fh:close()

-- Then read the pipe in another program, like
-- cat /tmp/mypipe