Thursday, August 28, 2008

Integrating SDL with Lua

For my next little game project I decided to use SDL, OpenGL and preferably some sort of scripting language to handle the game logic. Not that I couldn't write the game logic in C/C++, but I wanted to try out, how this integration of scripting language into your application works.

The language I decided to try first was Lua. It has been used in many games, so it felt like a natural choice.

SDL skeleton

First we need some SDL/OpenGL skeleton, which will initialize SDL and OpenGL. Basically we need the simplest OpenGL application there is.

#include <windows.h>
#include <SDL/SDL.h>
#include <gl/gl.h>

static int redraw(float theta)
{
 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

 glLoadIdentity();
 glTranslatef(0.0f,0.0f,0.0f);
 glRotatef(theta, 0.0f, 0.0f, 1.0f);

 glBegin(GL_TRIANGLES);
 glColor3f(1.0f, 0.0f, 0.0f);
 glVertex2f(0.0f, 1.0f);
 glColor3f(0.0f, 1.0f, 0.0f);
 glVertex2f(0.87f, -0.5f);
 glColor3f(0.0f, 0.0f, 1.0f);
 glVertex2f(-0.87f, -0.5f);
 glEnd();

 SDL_GL_SwapBuffers();
}

int main(int argc, char*argv[])
{
 SDL_Init(SDL_INIT_VIDEO);
 SDL_Surface *screen = SDL_SetVideoMode(400, 400, 32, SDL_DOUBLEBUF | SDL_HWSURFACE | SDL_OPENGL);

 glViewport(0, 0, 400, 400);
 glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
 glClearDepth(1.0);
 glDepthFunc(GL_LESS);
 glEnable(GL_DEPTH_TEST);
 glShadeModel(GL_SMOOTH);
 glMatrixMode(GL_PROJECTION);
 glMatrixMode(GL_MODELVIEW);

 for (float theta = 0; theta < 20000; theta += 0.5)
 {
  redraw(theta);
 }

 return 0;
}

To compile, we have to use

g++ -o sdl main.cpp -lmingw32 -lSDLmain -lSDL -lopengl32 -mwindows

For Linux just remove -lmingw32 and -mwindows.

Please note, that the relative order of libraries and main.cpp is important. SDL overrides the default main function, same as Win32 does, so switching the order will result in a linking error.

After successful compilation, you should get an application looking like this:

OpenGL Triangle

The code itself is quite ridiculous (especially the for loop with floating point number), but the reason will become clear, when we insert the Lua language scripting.

Lua skeleton

Now we need a minimal example of Lua processing, so that we get a feeling for the Lua code. It's actually quite simple, you include the appropriate header files (in my case I'm using the C++ version of header files that comes with Lua, because, I'm planning to use C++ in my project). Afterwords you just initialize Lua and call the appropriate script file. It's pretty self explanatory:

#include <lua/lua.hpp>

int main(int argc, char*argv[])
{
 lua_State *L=lua_open();
 luaL_openlibs(L);

 if (luaL_dofile(L, "skeleton.lua")!=0) fprintf(stderr,"%s\n",lua_tostring(L,-1));

 lua_close(L);

 return 0;
}

We compile this with:

g++ -o lua_skeleton lua_skeleton.cpp -llua

Now we need to create a simple Lua script skeleton.lua:

io.write("Hello from Lua\n")

Upon running we should get a result looking like this

Hello from Lua

Skeletons mixed

And now we need to mix these two programs together. I want to offload the rotation of triangle to Lua, that will allow me to test how to call functions from Lua and how to retrieve variables to Lua.

#include <windows.h>
#include <SDL/SDL.h>
#include <gl/gl.h>
#include <lua/lua.hpp>

static int redraw(lua_State *L)
{
 float theta = lua_tonumber(L, 1);

 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 glLoadIdentity();
 glTranslatef(0.0f,0.0f,0.0f);
 glRotatef(theta, 0.0f, 0.0f, 1.0f);
 glBegin(GL_TRIANGLES);
 glColor3f(1.0f, 0.0f, 0.0f);
 glVertex2f(0.0f, 1.0f);
 glColor3f(0.0f, 1.0f, 0.0f);
 glVertex2f(0.87f, -0.5f);
 glColor3f(0.0f, 0.0f, 1.0f);
 glVertex2f(-0.87f, -0.5f);
 glEnd();

 SDL_GL_SwapBuffers();
}

int main(int argc, char*argv[])
{
 lua_State *L=lua_open();
 luaL_openlibs(L);

 lua_register(L, "redraw", redraw);

 atexit(SDL_Quit);
 if( SDL_Init(SDL_INIT_VIDEO) < 0 ) {
  fprintf(stderr,"Couldn't initialize SDL: %s\n", SDL_GetError());
  exit(1);
 }

 SDL_Surface *screen = SDL_SetVideoMode(400, 400, 32, SDL_DOUBLEBUF | SDL_HWSURFACE | SDL_OPENGL);

 glViewport(0, 0, 400, 400);
 glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
 glClearDepth(1.0);
 glDepthFunc(GL_LESS);
 glEnable(GL_DEPTH_TEST);
 glShadeModel(GL_SMOOTH);
 glMatrixMode(GL_PROJECTION);
 glMatrixMode(GL_MODELVIEW);

 if (luaL_dofile(L, "mixed.lua")!=0) fprintf(stderr,"%s\n",lua_tostring(L,-1));
 lua_close(L);

 return 0;
}

The Lua script looks as follows. I chose the increment step 0.5, so that I can see possible speed difference between Lua's processing of floating point and C.

for i = 1.00, 20000, 0.5 do
 redraw(i)
end 

To my tremendous surprise the Lua code performs as fast as the C code. I did a couple of measurements on different computers and I couldn't find a statistically significant difference (Lua was hitting 96 % - 101 % framerate of pure C version). Without performance issues in the way, it looks like I might be using Lua as a scripting language in my project.

To download all the source code and binaries, click here luasdl.zip 440 kB

Next time, we will look at the performance and integration with Python.

Code highlighted with syntaxhighlighter

Wednesday, August 27, 2008

Switch User With a Fork

I like open source. Mainly because of the fact, that when I'm curious why or how something works, I can just go and check. Although my previous posts were mainly Win32 related, I'm actually a hard core Linux geek stuck in corporate windows environment :). So I decided to start a section "Dig that code".

I have many friends, who like Linux as much as I do, who write scripts and programs all the time, but who would never download and check someone else's code to find out, what's going on and why something does / doesn't work. (I'm obviously talking about people who already can program, not about users). I think there is some sort of barrier people feel when touching code for the first time, so I'm going to document my steps.

Today's example will be quite basic and simple. The question is, you're logged into a computer, where the fork bomb is currently running. You need to become root to start killing everything. However you can't start any additional programs because the resource limit is already reached.

I thought the answer is exec su, until I actually tried it out.

Theory

System call execve replaces the currently running process with the process from the specified filename. So what should happen upon calling exec su is

  1. shell is replaced by su
  2. su asks for root credentials
  3. upon successful confirmation, su calles execve and is replaced by a new shell

Practice

Easy way how to test this is

$ echo $$
FIRST_PID
$ exec su
# echo $$
SECOND_PID

If the number FIRST_PID equals to SECOND_PID, then you would be able to avoid the fork bomb, however when trying it out, I got different numbers on couple of modern distributions I tried - Gentoo 2008.0 stable, Fedora 8 and Red Hat Enterprise Linux 5.2. I tried on couple of Unixes as well and guess what? The pid number stayed the same. I decided to investigate - dig that code.

The first thing I did, was that I downloaded coreutils and went for the su.c. However to my surprise the relevant lines were looking like this:

static void
run_shell (char const *shell, char const *command, char **additional_args,
    size_t n_additional_args)
{
  size_t n_args = 1 + fast_startup + 2 * !!command + n_additional_args + 1;
  char const **args = xnmalloc (n_args, sizeof *args);
  size_t argno = 1;

  if (simulate_login)
    {
      char *arg0;
      char *shell_basename;

      shell_basename = last_component (shell);
      arg0 = xmalloc (strlen (shell_basename) + 2);
      arg0[0] = '-';
      strcpy (arg0 + 1, shell_basename);
      args[0] = arg0;
    }
  else
    args[0] = last_component (shell);
  if (fast_startup)
    args[argno++] = "-f";
  if (command)
    {
      args[argno++] = "-c";
      args[argno++] = command;
    }
  memcpy (args + argno, additional_args, n_additional_args * sizeof *args);
  args[argno + n_additional_args] = NULL;
  execv (shell, (char **) args); // <- notice this

  {
    int exit_status = (errno == ENOENT ? EXIT_ENOENT : EXIT_CANNOT_INVOKE);
    error (0, errno, "%s", shell);
    exit (exit_status);
  }
}

Obviously, there's happening something fishy, because su is calling execv. There is simply no possibility for the final shell to have a different pid. So I used equery and rpm to find out, where the /bin/su command comes from. And to my surprise it doesn't come from coreutils, but from the shadow package.

After downloading the shadow package, unpacking and checking su.c, I found

/* This I ripped out of su.c from sh-utils after the Mandrake pam patch
 * have been applied.  Some work was needed to get it integrated into
 * su.c from shadow.
 */
static void run_shell (const char *shellstr, char *args[], int doshell,
         char *const envp[])
{
 int child;
 sigset_t ourset;
 int status;
 int ret;

 child = fork (); // <- notice this
 if (child == 0) { /* child shell */
  pam_end (pamh, PAM_SUCCESS);

  if (doshell)
   (void) shell (shellstr, (char *) args[0], envp);
  else
   (void) execve (shellstr, (char **) args, envp);
  exit (errno == ENOENT ? E_CMD_NOTFOUND : E_CMD_NOEXEC);
 } else if (child == -1) {
  (void) fprintf (stderr, "%s: Cannot fork user shell\n", Prog);
  SYSLOG ((LOG_WARN, "Cannot execute %s", shellstr));
  closelog ();
  exit (1);
 }

 ....

}

So there you have it, the forking happens in shadow's version of su, which is today quite a standard among distributions, because of the PAM integration. So next time you find yourself on a system with a fork bomb, you know who to thank to :).

Tuesday, August 5, 2008

Simple Oracle Client

After a longer pause because of vacation and general busyness/laziness I present you a standalone simple command line Oracle client.

It's not standalone as in independent on Oracle code (it comes with 106 megabytes of Oracle dll's), but it doesn't need any installation, doesn't modify registry and it doesn't need any privileges, just unpack it and run. Yeah I know, you're thinking "106 megabytes for dll that connects to database it's enormous". Truth to be told, I have no idea, what is Oracle doing in so much code.

The client itself, it's written in C# and it's pretty basic. Reads SQL statements from standard input, executes them in DB, sends output to standard output and errors to standard error. Nothing really spectacular.

      using System;
      using System.IO;
      using System.Text.RegularExpressions;
      using System.Collections.Generic;
      
      using Oracle.DataAccess.Client;
       
      class SimpleOracleClient
      {
              OracleConnection con;
              OracleCommand cmd;
              TextWriter output;
              TextWriter error;
       
              bool headers = true;
              string separator = ";\t";
       
              SimpleOracleClient(string connectionString)
              {
                      output = Console.Out;
                      error = Console.Error;
       
                      con = new OracleConnection(connectionString);
                      con.Open();
       
                      Console.Error.WriteLine("Connected to Oracle " + con.ServerVersion);
              }
       
              ~SimpleOracleClient()
              {
                      if (output != Console.Out)
                              output.Close();
                      if (error != Console.Error)
                              error.Close();
       
                      con.Close();
                      con.Dispose();
              }
       
              void Execute(string command)
              {
                      cmd = new OracleCommand(command, con);
       
                      OracleDataReader myReader;
       
                      try {
                              myReader = cmd.ExecuteReader();
       
       
                              if (headers)
                              {
                                      for (int i = 0; i < myReader.FieldCount; i++)
                                      {
                                              error.Write(myReader.GetName(i) + separator);
                                      }
             
                                      error.WriteLine();
                                      error.WriteLine();
                              }
       
                              while(myReader.Read())
                              {
                                      for (int i = 0; i < myReader.FieldCount; i++)
                                      {
                                              output.Write(myReader.GetValue(i).ToString() + separator);
                                      }
                                      output.WriteLine();
       
                              }
       
                              if (headers)
                              {
                                      for (int i = 0; i < myReader.FieldCount; i++)
                                      {
                                              error.Write(myReader.GetName(i) + separator);
                                      }
                                      error.WriteLine();
                              }
       
                              output.Flush();
                              error.Flush();
       
                              myReader.Close();
       
                      }
                      catch (OracleException e)
                      {
                              Console.Error.WriteLine(e.Message);
                      }
              }
       
              static string getConnectionString(string[] args)
              {
                      Dictionary<string, string> options = new Dictionary<string, string>();
       
                      options.Add("host", "localhost");
                      options.Add("port", "1521");
                      options.Add("service", "ora");
                      options.Add("user", "Administrator");
                      options.Add("password", "Administrator");
       
                      string connectionString;
       
                      if (args.Length == 0)
                      {
                              connectionString = Console.ReadLine();
                      }
                      else if (args.Length == 1)
                      {
                              connectionString = args[0];
                      }
                      else
                      {
                              for (int i = 0; i < args.Length; i++)
                              {
                                      string name = args[i].Substring(1).ToLower();
                                      if (options.ContainsKey(name))
                                      {
                                              options[name] = args[i+1];
                                      }
                              }
       
                              connectionString = "Data Source=(DESCRIPTION="
                                              + "(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)"
                                              + "(HOST=" + options["host"] + ")"  
                                              + "(PORT=" + options["port"] + ")))"
                                              + "(CONNECT_DATA=(SERVER=DEDICATED)"
                                              + "(SERVICE_NAME=" + options["service"] + ")));"
                                              + "User Id=" + options["user"] + ";"
                                              + "Password=" + options["password"] + ";";
                      }
       
                      return connectionString;
              }
       
              static int Main(string[] args)
              {
                      Console.Error.WriteLine("Simple Oracle Client 0.1");
                      SimpleOracleClient s = new SimpleOracleClient(getConnectionString(args));
       
                      Dictionary<string, string> helper = new Dictionary<string, string>();
                      helper.Add(".tables", "SELECT table_name FROM all_tables");
                      helper.Add(".databases", "SELECT * FROM user_tablespaces");
       
                      string line = "";
       
                      while ((line += Console.ReadLine()) != null)
                      {
                              if (line.Length == 0)
                                      continue;
       
                              string[] command = Regex.Split(line.Trim().ToLower(), " +");
       
                              if (command[0] == ".quit" || command[0] == ".exit")
                                      break;
       
                              bool processed = true;
       
                              switch (command[0])
                              {
                                      case ".headers":
                                      case ".header":
                                              if (command.Length > 1 && Regex.Match(command[1], "(on)|1|(true)").Success)
                                              {
                                                      s.headers = true;
                                              }
                                              else if (command.Length > 1 && Regex.Match(command[1], "(off)|0|(false)").Success)
                                              {
                                                      s.headers = false;
                                              }
                                              else
                                              {
                                                      Console.Error.WriteLine("Headers " + (s.headers ? "on" : "off"));
                                              }
                                              break;
       
                                      case ".output":
                                              if (command[1] == "stdout")
                                              {
                                                      if (s.output != Console.Out)
                                                      {
                                                              s.output.Close();
                                                              s.output = Console.Out;
                                                              s.error = Console.Error;
                                                      }
                                              }
                                              else
                                              {
                                                      if (s.output != Console.Out)
                                                      {
                                                              s.output.Close();
                                                      }
                                                      s.output = new StreamWriter(command[1]);
                                                      s.error = s.output;
                                              }
                                              break;
       
                                      case ".separator":
                                              if (command.Length > 1)
                                              {
                                                      s.separator = command[1];
                                              }
                                              else
                                              {
                                                      Console.Error.WriteLine("Separator is '{0}'", s.separator);
                                              }
                                              break;
       
                                      case ".help":
                                              Console.Error.WriteLine("Simple Oracle Client 0.1");
                                              Console.Error.WriteLine("For updates, please see http://www.gettingclever.com");
                                              Console.Error.WriteLine("");
                                              Console.Error.WriteLine(".exit - exits the program");
                                              Console.Error.WriteLine(".headers [on|off] - turn headers on");
                                              Console.Error.WriteLine(".output FILENAME - sends output to FILENAME");
                                              Console.Error.WriteLine(".output stdout - sends output to stdout");
                                              Console.Error.WriteLine(".quit - quits the program");
                                              Console.Error.WriteLine(".separator STRING - uses STRING as column separator");
                                              Console.Error.WriteLine("");
                                              break;
       
                                      default:
                                              processed = false;
                                              break;
                              }
       
                              if (!processed)
                              {
                                      if (line[line.Length-1] == '\\')
                                      {
                                              line = line.Substring(0, line.Length-1);
                                              continue;
                                      }
                                      else
                                      {
                                              if (helper.ContainsKey(line))
                                              {
                                                      s.Execute(helper[line]);
                                              }
                                              else
                                              {
                                                      s.Execute(line);
                                              }
                                      }
                              }
                              line = "";
                      }
       
                      return 0;
              }
      } 

Connecting to the database

There are three different ways to establish a connection to the DB.
  1. Specify the connection string on the command line oracle.exe " Notice the double quotes surrounding the whole connection string, they are needed, so that the whole connection string is considered as a whole.
  2. Specify connection details on the command line oracle.exe -host example.com -port 1521 -service example -user test -password secret
  3. Type in the connection string as first line from standard input - this option is probably the most useful for scripting and at the same time most secure, since it doesn't display the password in process list. Send the connection string as first line in standard input

Working with the database

When connected to the database, the work is pretty straightforward, you issue SQL commands and the results get displayed. Inspired by sqlite I implemented couple of useful dot-commands.
.databasesLists available databases
.exitExits the application
.headers [on|off]Switches displaying of column headers on and off
.helpDisplays help screen
.output FILENAMESends all output to FILENAME
.output stdoutSends all output to standard output
.quitQuits the application
.separator [NEW_SEPARATOR_STRING]Sets new separator string
.tablesLists available tables

Things to keep in mind

This is just a very basic client. There is no support for multiple SQL statements.

Following statement will fail

SELECT * FROM table1; SELECT * FROM table2;
Because it's up to the client application to separate individual SQL statements.

Not only that, but even this statement will fail because of the trailing semicolon.

SELECT * FROM table1;

This is a work in progress, there are quirks, which I'm aware of and bugs, which I'm not. I'll keep on updating this client, but keep in mind, that it was written in one afternoon, while travelling back from work. Use at your own risk. I use it on production databases, which doesn't mean you should.

Link to the code and binaries oracle.0.1.7z - 20 MB, hosted on MediaFire.

It's 7zipped for minimal download size. Standard zip yielded results somewhere about 37 megabytes.

Planned enhancements

  • Interactive password query
  • SQL parsing - possibility to enter multiple SQL statements, comments etc
  • Plain file import
  • History of commands
If you have any feature requests, please post them in comments or write and email (everything send to this domain comes to me).

Tested with Oracle 10.2.0.3 and 11.1.0.6

Please note, that the oracle client library creates %USERPROFILE%\Oracle folder, which is safe to delete.

Obligatory thanks to Mark James for creating the amazing Silk icons.

Update [15th September 2008]: Fixed the download link, I apologize for not using a direct link. I must buy some hosting.