Conversational Design

After years and years of doing software, and trying to help other engineers create software, I’ve come to a couple of conclusions. One, and probably the most important, is how you name things really matters. Ideally, you can create nice functions, and use nice language, such that you could have an almost speaking conversation while explaining how something should work.

I think where this goes astray is when the conversation becomes strained, and we try to push a metaphor too far. Object based systems tend to have this effect, particularly when they’ve existed for multiple years, and have morphed from their original intention.

At any rate, what conversation am I trying to have here?

I’ve been exploring busting apart the core event loop that runs applications. I’ve explored IEnumerable vs IObserverable, and I noted the different between alert notifications, and events. The whole point of teasing these concepts apart is so that I can reconstruct these core concepts in a way that is much easier to understand, easier to write code for, and ultimately easier to maintain.

So, this time around, I begin with keyboard events.

OnKeyDown = function(kbd, keycode)
  print("KEYDOWN: ", keycode);
end

OnKeyUp = function(kbd, keycode)
  print("KEYUP: ", keycode);

  -- Halt the loop if they press the "Esc" key
  if keycode == KEY_ESC then
    loop:Halt();
  end
end

OnKeyRepeat = function(kbd, keycode, count)
  print("KEYREP: ", keycode, count);
end

This should be pretty straight to understand. Basically, when one of these events occurs, I want to print out the key code. In the case of a KeyUp event, I will check to see if it’s the “Esc” key. If it is, then I want to exit the event loop. Otherwise, I just print out the keycode.

This says nothing about async, iobservable, inumerable, or anything else. This doesn’t have to be a part of any class, or structure. They’re just plain old ordinary functions. Of course, they could be a part of something larger and more organized, but there is nothing about this conversation that says they must be.

OK. Great, there are some functions that will be called which are handed enough information to figure out what’s going on with key presses. So, what calls these functions?

Now I have the concept of the event loop. An event loop is nothing more than a while statement which will continue until explicitly stopped.

local IOAlertEmitter = require "IOAlertEmitter"

local EventLoop = {}
local EventLoop_mt = {
  __index = EventLoop,
}

EventLoop.new = function()
  local obj = {
    Observers = {},
    Emitter = IOAlertEmitter.new(),
    Running = false;
  }

  setmetatable(obj, EventLoop_mt);

  return obj;
end


EventLoop.AddObservable = function(self, observable, observer)
  observer = observer or observable
  self.Observers[observable.AlertHandle:getfd()] = observer;

  return self.Emitter:AddAlertable(observable.AlertHandle, observer.OnAlert, observable.WhichAlerts);
end

EventLoop.Halt = function(self)
  self.Running = false;
end

EventLoop.Run = function(self, timeout)
  timeout = timeout or 0

  self.Running = true;

  while self.Running do
    local alerts, err = self.Emitter:Wait(timeout)

    if alerts and #alerts > 0 then
      for i=1,#alerts do	
        -- get the appropriate observer
        local observer = self.Observers[alerts[i].fd];
        if observer and observer.OnAlert then
          observer:OnAlert(self, alerts[i].fd, alerts[i].events)
        end
      end
    end

    -- Allow some idle work to occur
    if self.OnIdle then
      self.OnIdle(self)
    end
  end
end

return EventLoop;

This loop has been seen before. This time it is encapsulated in an object which deals explictly with being a loop, and nothing else. Observables can be added to this list, and those observables are added to the loop’s epoll watcher, so that any time there is activity on them, the right function will be called.

This loop is so simple that if you want some slightly different behavior, you can just copy and paste the code, and alter it to fit your particular situation. You’re not forced to stick with this particular kind of loop.

Alright, that takes care of the basic loop. What about the keyboard that is supposed to be generating these events in the first place?

local ffi = require "ffi"

local S = require "syscall"
local UI = require "input"

local Keyboard = {}
local Keyboard_mt = {
	__index = Keyboard,
}


Keyboard.new = function(devicename)
	devicename = devicename or "/dev/input/event0";

	-- Create Actual Device Handle
	local devicefd, err = S.open(devicename, S.c.O.RDONLY);
	if not devicefd then
		return false, err
	end

	local obj = {
		DeviceDescriptor = devicefd,
		AlertHandle = devicefd,
		WhichAlerts = S.c.POLL.RDNORM,
	}

	setmetatable(obj, Keyboard_mt)

	return obj
end



Keyboard.OnAlert = function(self, loop, fd, events)
	--print("Keyboard.OnAlert: ", fd, events);

	-- Read the keyboard device
	local event = input_event();
	local bytesread, err = S.read(fd, event, ffi.sizeof(event));

	if not bytesread then
		return false, err
	end

	if event.type == EV_MSC then
		if event.code == MSC_SCAN then
			--print("MSC_SCAN: ", string.format("0x%x",event.value));
		else
			--print("MSC: ", event.code, event.value);
		end
	elseif event.type == EV_KEY then
		if event.value == 1 and self.OnKeyDown then
			self:OnKeyDown(event.code);
		elseif event.value == 0 and self.OnKeyUp then
			self:OnKeyUp(event.code);
		elseif event.value == 2 and self.OnKeyRepeat then
			self:OnKeyRepeat(event.code, 1);
		end
	else
		--print("EVENT TYPE: ", UI.EventTypes[event.type][2], "CODE:",event.code, "VALUE: ", string.format("0x%x",event.value));
	end
end

return Keyboard;

This has been seen before as well. Basically, just some object that encapsulates the devicename which is to be opened so that keyboard activity can be read. Then, the OnAlert() function, which will be called whenever there is any activity on the keyboard. In this particular case, some translation occurs so that some function can be called indicating keyup, keydown, or keyrepeat. This saves everyone from having to know fairly low level detail about how keyboard activities are read in the system.

And finally, to pull it all together:

-- Setup an event loop and keyboard
loop = EventLoop.new();
local kbd = Keyboard.new();

-- Setup some keyboard with event handlers
kbd.OnKeyDown = OnKeyDown;
kbd.OnKeyUp = OnKeyUp;
kbd.OnKeyRepeat = OnKeyRepeat;

loop:AddObservable(kbd);

loop:Run(15);

Basically, create a loop. Create a keyboard proxy. Assign some functions to be called when there is keyboard activity. Add the keyboard to the loop as something to be observed. Run the loop, with a 15ms wait for each key press. And that’s it.

Now I can have a conversation about how to handle IO events. I can talk about an EventLooper, which will watch over various kinds of Observable objects. When there is any activity on that observable object, the loop will call functions which were indicated to deal with the activity.

As all these objects are small and composable, it becomes fairly easy to add new kinds of observables, as well as different event handler functions. This is all very “Observable”, and there are “Callbacks”, but that can be easily changed.

The really neat thing comes when you start imagining different formulations:

local app = EventLoop.new();
local kbd = Keyboard.new();
local mouse = Mouse.new();
local listenSocket = Socket.new(80);

app:AddObservable(kbd);
app:AddObservable(mouse);
app:AddObservable(listenSocket);

app:Run();

This simple set of composable parts can handle a typical UI app just as easily as a scalable web service. With this set of components in hand, I no longer need to be dependent on libev, libevent, libuv, or any other eventing library or framework. That’s a relief because those systems, although very robust and purpose built, aren’t quite as easily composable, or as well integrated with the rest of Lua that this approach takes.

At this point, I’ve got my basic event loop, basic keyboard, and can start to stitch my applications back together. The next step is to create the rudimentary mouse object, and add in the base TCP/IP socket objects. At that point, things get very interesting.

Advertisements


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s