DEV Community

Rory O'Connell
Rory O'Connell

Posted on

Managing Windows windows within mruby Part 2: Creating a window from mruby

We'll build off of the previous program where we created a Windows window using the Win32 API. Now we're adding the mruby VM to our program and then push creation of the Win32 window into the VM instead of calling it from our main() function.

First we need to build mruby. This is a simple affair. There is a bit of a catch-22 as the automated build scripts are in Ruby. While Ruby used to be a pain on Windows it's simple today. Easiest is through chocolatey and install the Ruby package with choco install ruby. That's it!

Clone the mruby repo and check out latest tag, currently 2.1.0, or download the latest release and extract.

GitHub logo mruby / mruby

Lightweight Ruby

Build Status

What is mruby

mruby is the lightweight implementation of the Ruby language complying to (part of) the ISO standard. Its syntax is Ruby 2.x compatible.

mruby can be linked and embedded within your application. We provide the interpreter program "mruby" and the interactive mruby shell "mirb" as examples You can also compile Ruby programs into compiled byte code using the mruby compiler "mrbc". All those tools reside in the "bin" directory. "mrbc" is also able to generate compiled byte code in a C source file, see the "mrbtest" program under the "test" directory for an example.

This achievement was sponsored by the Regional Innovation Creation R&D Programs of the Ministry of Economy, Trade and Industry of Japan.

How to get mruby

The stable version 2.1.0 of mruby can be downloaded via the following URL: https://github.com/mruby/mruby/archive/2.1.0.zip

The latest development version of mruby can be downloaded via the following URL: https://github.com/mruby/mruby/zipball/master

Run ruby minirake from dir. There are some warnings about macro re-definitions, they're harmless. When finished, copy build\host\*.lib to the win32 project dir. Copy recursive include to the project dir as well.

Either modify the batch file, adding /Imruby to compile options, or modify the .h files to use relative paths "" instead of <> include paths. Either works, I prefer the latter.

Within the batch file, add libmruby.lib libmruby_core.lib to the end of the line, telling the linker to use the mruby libraries for external symbols.

Now we'll test mruby. Add #include "mruby.h" to the top of the cpp file. At the top of main add mrb_state *mrb = mrb_open();. Bottom add mrb_close(mrb). Compile and run, ensuring everything is fine.

Important!

Before we go on there are two fundamental things required to understand. The entire VM state is in the mrb_state type we just created. We pass that state as the first parameter to all mruby functions. This is a common C pattern seen lately. Older C programs and libraries used a pattern of global hidden state. Examples include the main Ruby C interface and famously OpenGL. Now it's more common for C programs using a pointer to an internal state instead. This adds a great deal more flexibility at the cost of a bit more verbose syntax.

The other fundamental piece of information is everything with mruby uses an mrb_value type. It's similar to the C Ruby VALUE type.

union mrb_value_union {
  mrb_float f;
  void *p;
  mrb_int i;
  mrb_sym sym;
};
typedef struct mrb_value {
  union mrb_value_union value;
  enum mrb_vtype tt;
} mrb_value;

The mrb_vtype tt is important here. That specifies the table type of this value and informs how to access it. For float, fixnums, and symbol table types use the f, i and sym members of the union respectively. All others use the void *p type, with the tt property informing how to interpret the void *. For example if we have an mrb_value with a tt of MRB_TT_CLASS we cast the p member of the union to an RClass *.

// or use reinterpret_cast<RClass *> if you like more explicit C++ casts
RClass *klass = (RClass *)foo.value.p;

Sometimes you see the API use or return types like mrb_string or mrb_int. These ultimately come from a source mrb_value. If you're debugging trying to figure out why you're getting information you don't expect, back up the stack until you find the source mrb_value these derived types come from.

Much of the mruby API looks like the main Ruby C API. The Ruby C API is well covered at this point. If you're looking for information on how to do something and the information for mruby isn't readily available you can find examples with the Ruby C API and translate it back. Beyond that reading the mruby header files, mruby C source implementation, and looking at the implementation of mruby gems provides insight. All of the C code with mruby and gems surrounding it is easy to read. There are no C tricks or hard to parse syntax. Oftentimes reading the C implementation is close to reading Ruby code with some extra steps.

Creating objects in the mruby vm

First thing we'll do is create a new class in the ruby VM. This is the same as class W32Window; end;. We can do this with mrb_define_class. After initializing the mruby state, add

// RClass *mrb_define_class(mrb_state *mrb, const char *name, struct RClass *super);
RClass *window_manager_class = mrb_define_class(mrb, "W32Window", mrb->object_class);

mruby automatically creates the base object when we initialize the mrb_state and places a reference in the object_class pointer in the state. Remember, everything in Ruby comes from Object eventually, even classes, so we set the third parameter to the root object to inherit from.

Right now we'll treat W32Window as a static object containing all of it's state. We could use W32Window as a module instead. If we were to do that we'd use mrb_define_module(mrb, "W32Window");, which corresponds to module W32Window; end;. Either works for now, though I'm using a class since I'll refactor it into a constructor later.

Now that we have an RClass * we can use C to define methods in it. Defining class which runs C code instead of Ruby code is a two step process. One, define a function in C which runs when we call the Ruby method. Two, define a method on a Ruby object pointing to the C function. Lets create a test method named foo on the W32Window class which prints something to standard out. We'll be doing the Ruby equivalent of

class W32Window
  def self.foo
    puts "Hello from W32Window.foo"
  end
end

Bound mruby methods have a signature mrb_value function_name(mrb_state *state, mrb_value self). I prefer naming bound C functions with a naming pattern of class_name_method_name_method. In this case we'll name the C method w32_window_foo_method means W32Window.foo. Filling it all out we have the function

mrb_value
w32_window_foo_method(mrb_state* state, mrb_value self) {
  printf("Hello from W32Window.foo\n");
  return mrb_nil_value();
}

Every Ruby method must return some value so we'll return nil, of which there's a helpful function for.

Now the last step is creating the method on an object. For a class method this is mrb_define_class_method. For a method on a module, it is mrb_define_module_function. And a normal instance method, mrb_define_method. All have a similar signature. We're defining a class method on W32Window.

// void mrb_define_class_method(mrb_state *mrb, struct RClass *cla, 
//                              const char *name, mrb_func_t fun, 
//                              mrb_aspec aspec);
mrb_define_class_method(mrb, window_manager_class, "foo",
                        w32_window_foo_method, MRB_ARGS_NONE());

Which is all straightforward. The last parameter differs from the mainline Ruby rb_define_method. mruby uses an mrb_aspec type. This defines bit patterns for specifying the number and kind of arguments, while Ruby uses an integer with extended parameters. There are helpful macros for creating the right bit pattern, such as MRB_ARGS_NONE(). You can read the mruby.h header for all the macros for specifying number and types of parameters for a C function called from Ruby.

That's really all there is to it. We can test it out by calling the C function mrb_load_string, which is like an eval from the C side. This function is fine for limited testing or one off execution. You should not use it repeatedly in code paths since it performs a parse, compilation and execution every time. It's function prototype lives in the compile.h, so #include "mruby/compile.h" first and then

mrb_load_string(mrb, "W32Window.foo");

If you're working off of the previous Win32 project, place this function call after the CreateWindowEx call. Windows does not set up stdout until after window creation. Build, run and you'll see Hello from W32Window.foo in the console just as you expect.

Lets take a step back and analyze what we just put together. We created a new mruby VM state with mrb_open. Then, using C functions, created a new class within the Ruby VM called W32Window. We added a class method named foo on the new W32Window class. That class method runs the C function window_manager_foo_method instead of running Ruby code. Within that C function we print a message to standard out. Finally, we execute some Ruby code within the VM with C.

Creating a Win32 window within mruby

Now you see the back and forth between a C program and the mruby VM. What we want to do is push everything we can into the mruby VM. Lets do the simplest thing possible by pushing the Win32 window creating into a C function called by the VM instead. This is still fragile and has issues though it's a good first step.

Delete everything related to creating the foo method on W32Window. We'll add a create class method on the class which does all the win32 calls instead. We'll move all the Win32 window creation code into this function. It's a simple enough procedure, simply showing some of the code is sufficient for understanding. It's not much different than we already did with our test function with printf

mrb_value
w32_window_create_method(mrb_state* state, mrb_value self) {
  WNDCLASSEX wclass = {};
  wclass.cbSize = sizeof(WNDCLASSEX);
  wclass.lpszClassName = "mainWindowClass";
  wclass.lpfnWndProc = main_window_callback;
  wclass.hInstance = GetModuleHandle(nullptr);
  wclass.style = CS_OWNDC | CS_VREDRAW | CS_HREDRAW;
  RegisterClassEx(&wclass);

  DWORD main_window_style = (WS_OVERLAPPEDWINDOW | WS_VISIBLE);

  HWND main_window = CreateWindowEx(0, wclass.lpszClassName, "Playground",
                                    main_window_style, CW_USEDEFAULT, CW_USEDEFAULT,
                                    1024, 768,
                                    0, 0, GetModuleHandle(nullptr), 0);

  return mrb_nil_value();
}

/// before the main loop
mrb_define_class_method(mrb, w32_window_class, "create",
                        w32_window_create_method, MRB_ARGS_NONE());

mrb_load_string(mrb, "W32Window.create");

Run this program and... it's exactly the same as before. Which in this case is a good thing! There's a profound difference in the execution path. Now when we create a Win32 window we are within the context of the mruby VM and all it provides us. Lets do a simple proof of that. Change the mrb_load_string to

mrb_load_string(mrb, "5.times { W32Window.create }");

Running this program we now see we have five identically behaving Win32 windows!

They all behave identically in that closing any one of them quits the program. Initially we assumed we would have only one window. In our callback function when we receive a WM_CLOSE message, which happens when the window closes, we quit the program using PostQuitMessage. We want to keep track of all the open windows and then quit when the last window closes. We'll get to that next time when we start encapsulating C data into an mruby class. For now slap a //TODO in the code and lets move on.

Extracting mruby parameters in C

Lets make our creation a bit more interesting. Lets make the create method take parameters for the window title and dimensions. Written in Ruby, the method would look like def self.create window_title, width: 1024, height: 768. Starting with window_title first, we have to change the mrb_define_class_method declaring that we have one required parameter. Change MRB_ARGS_NONE to MRB_ARGS_REQ(1). The Ruby VM enforces one method argument now.

It's a simple process extracting Ruby method arguments in a method implemented in C. We use the mrb_get_args function for extracting copies of the arguments into mrb_value types. The first parameter is an mruby_state, second is a format specifier, and the rest are va_args matching the format specifier. Right from the mruby header file

/**
 * Retrieve arguments from mrb_state.
 *
 * @param mrb The current MRuby state.
 * @param format is a list of format specifiers
 * @param ... The passing variadic arguments must be a pointer of retrieving type.
 * @return the number of arguments retrieved.
 * @see mrb_args_format
 * @see mrb_kwargs
 */
MRB_API mrb_int mrb_get_args(mrb_state *mrb, mrb_args_format format, ...);

The format specifier specifies the type of argument passed in to what position. It's a standard c-string similar to printf. Since currently we're expecting a string for the window title we use the format specifier "S". Lastly we pass a reference to an empty mrb_value that mrb_get_args populates. See the mruby.h header file detailing the format specifier arguments. At the top of w32_window_create_method

mrb_value window_name;
mrb_get_args(state, "S", &window_name);

Now all we need to do is use the passed parameter as the window title instead of the hardcoded string "Playground". Note that we have an mrb_value instead of a char *. There are helpful macros for extracting data out of mrb_value types to C types. First we need to #include "mruby/string.h" for their definitions. Then we can replace "Playground" with RSTRING_PTR(window_name).

Change our call in mrb_load_string to something more interesting that demonstrates our new capabilities.

mrb_load_string(mrb, "5.times { |n| W32Window.create \"Window #{n}\" }");

Compile, run and we have five different uniquely named windows!

Lets take a moment and marvel at what we did here. We pushed creating of new windows using Win32 into the Ruby VM and parameterized the construction of the windows. We didn't need any external window management libraries, a chain of dependencies from a gemfile, didn't need something like FFI creating tenuous bindings between the Ruby VM and an external library. It's all right in front of us in less than 80 lines of code. It's pretty amazing and easy! All we needed to get to where we're at is just the mruby library and the facilities provided with Windows programming.

If this is your first foray into this style of programming you may start to develop questions on why it takes thousands of interdependent NPM, Python, Java or Ruby libraries and hundreds of lines of boilerplate to generate a 'Hello World' in a browser. If you're like me and burnt out over years to decades of overly complicated development, where it takes a whole quarter to show a birthdate this probably feels refreshing and fun again.

Lets keep going and finish this out. The last thing we want to add here are keyword arguments for the window dimensions width and height.

Extracting keyword parameters

It's straightforward and simple extracting positional parameters passed C implemented Ruby methods. It's more complex for keyword parameters which is why I want to detail it here. Most of Ruby's history didn't support method arguments other than positional arguments, a block parameter, and a construction for grouping the 'rest' arguments, like the JavaScript ...args in a function name.

We faked keyword parameters by passing a hash as a parameter into the method. The Ruby interpreter supported this by allowing the removal of the outer braces. This is similar to passing an object to a function in JavaScript While this worked enforcing some keyword parameters and providing defaults ended up with a lot of boilerplate

def foo(args)
  a = args.fetch(:a, 'default value')
  b = args.fetch(:b, nil)
end
foo a: 1, b: 2 # the same as foo({a: 1, b: 2})

Skipping ahead in history, Ruby added support for, default parameters for any positional argument, keyword parameters, keyword parameters with a default, required keyword parameters, and block parameters also supports all of those. A method signature in Ruby can look tremendously complex. This is why the mruby C API looks complicated, as we will see right now for keyword arguments.

We want the full method signature for our create method to look like

def W32Window.create window_title, width:, height:

When I'm working with Ruby I generally create methods with more than one parameter using keyword arguments. I usually put the most important information as the first positional argument and then the rest as keyword arguments.

mrb_get_args uses a special mrb_kwargs type for the definition on how to extract keyword arguments. We use this type instead of an mrb_value when extracting the arguments.

struct mrb_kwargs
{
  uint32_t num;
  mrb_value *values;
  const char *const *table;
  uint32_t required;
  mrb_value *rest;
};

num is the number of keyword arguments in total. values is a pre-created array of mrb_values that we extract the parameter value into, like we did with the string. table is a char*[], an array of string, which are the names of the keyword arguments. required is the number of required keyword arguments.

Starting with the call to mrb_get_args we can work backwards and fill out the rest of the information.

const mrb_kwargs kwargs = { 2, kw_values, kw_names, 2, nullptr };
mrb_get_args(state, "S:", &window_name, &kwargs);

We know we want two keyword arguments and require 2, so we'll use 2 for both places. There won't be a rest param, so that's null. Now we just need the two arrays

const char* kw_names[2] = { "width", "height" };
mrb_value kw_values[2] = {};

Now we reference these parameters in CreateWindowEx instead of hardcoded values. We know we want the mrb_value types in kw_values as integers. We could cast them ourselves, however mruby usually has helpful macros for that already, so lets use them. In this case it's mrb_fixnum, which is directly compatible with standard integers without any casting necessary.

HWND main_window = CreateWindowEx(0, wclass.lpszClassName, RSTRING_PTR(window_name),
                                  main_window_style, CW_USEDEFAULT, CW_USEDEFAULT,
                                  mrb_fixnum(kw_values[0]),
                                  mrb_fixnum(kw_values[1]),
                                  0, 0, GetModuleHandle(nullptr), 0);

Lastly change mrb_load_string to provide the keyword arguments

mrb_load_string(mrb, "5.times { |n| W32Window.create \"Window #{n}\", width: 1024, height: 768 }");

Build, run, and of course everything is the same as before. Change the values of the width and height parameters and they change appropriately, for instance something like

mrb_load_string(mrb, "5.times { |n| W32Window.create \"Window #{n}\", width: n * 100, height: n * 100 }");

Which gives us a predictable 5 windows of increasing size

Celebration!

That's it for our introduction to using the mruby C API. We tied together Win32, mruby and parameterized window creation from Ruby to Windows with just the mruby library in 80 lines of code. Total! Amazing.

While the volume of words here implies there's something complicated happening it really isn't. I'm being especially thorough on explaining all the details, repeating some key points so things stick. Once you understand the concepts you can delete everything, start from scratch and replicate it yourself rapidly in less than an hour the second time, and faster still the third time.

Next up we'll finish this off by turning W32Window.create into a new constructor instead, encapsulate the HWND and HINSTANCE in the instance of the class, add a reference to the mruby W32Window instance into the Win32 window so we can get it back in the main_window_callback, and then change the program behavior so that it exits when all windows close, not any of them.

The full code

#include <windows.h>
#include <stdio.h>
#include "mruby.h"
#include "mruby/compile.h"
#include "mruby/string.h"

bool g_should_quit = false;

LRESULT CALLBACK
main_window_callback(HWND window, UINT message, WPARAM wparam, LPARAM lparam) {
  LRESULT result = 0;
  switch(message) {
  case(WM_CLOSE): {
    PostQuitMessage(0);
    break;
  }
  default: {
    result = DefWindowProc(window, message, wparam, lparam);
  }
  }
  return result;
}

mrb_value
w32_window_create_method(mrb_state* state, mrb_value self) {
  mrb_value window_name;
  mrb_int width;
  mrb_int height;
  const char* kw_names[2] = { "width", "height" };
  mrb_value kw_values[2] = {};
  const mrb_kwargs kwargs = { 2, kw_values, kw_names, 2, nullptr };
  mrb_get_args(state, "S:", &window_name, &kwargs);

  WNDCLASSEX wclass = {};
  wclass.cbSize = sizeof(WNDCLASSEX);
  wclass.lpszClassName = "mainWindowClass";
  wclass.lpfnWndProc = main_window_callback;
  wclass.hInstance = GetModuleHandle(nullptr);
  wclass.style = CS_OWNDC | CS_VREDRAW | CS_HREDRAW;
  RegisterClassEx(&wclass);

  DWORD main_window_style = (WS_OVERLAPPEDWINDOW | WS_VISIBLE);

  HWND main_window = CreateWindowEx(0, wclass.lpszClassName, RSTRING_PTR(window_name),
                                    main_window_style, CW_USEDEFAULT, CW_USEDEFAULT,
                                    mrb_fixnum(kw_values[0]),
                                    mrb_fixnum(kw_values[1]),
                                    0, 0, GetModuleHandle(nullptr), 0);

  return mrb_nil_value();
}

int main() {
  mrb_state *mrb = mrb_open();
  RClass *w32_window_class = mrb_define_class(mrb, "W32Window", mrb->object_class);
  mrb_define_class_method(mrb, w32_window_class, "create",
                          w32_window_create_method, MRB_ARGS_REQ(1));

  mrb_load_string(mrb, "5.times { |n| W32Window.create \"Window #{n}\", width: n * 100, height: n * 100 }");

  while(!g_should_quit) {
    MSG message = {};
    while(PeekMessage(&message, nullptr, 0, 0, PM_REMOVE)) {
      switch (message.message) {
      case (WM_QUIT): {
        g_should_quit = true;
        break;
      }
      default: {
        TranslateMessage(&message);
        DispatchMessage(&message);
        break;
      }
      }
    }
  }

  mrb_close(mrb);

  return 0;
}

Top comments (2)

Collapse
 
lukestuts profile image
Luke Stutters

Thanks for this very interesting series. I've followed along up to this point but I can't build it after adding #include "mruby.h". I get lots of these errors:

C:\workdir>cl /Zi /std:c++latest /GR- /nologo /analyze main.cpp /Imruby /link /debug user32.lib /SUBSYSTEM:windows /ENTRY:mainCRTStartup libmruby.lib libmruby_core.lib
main.cpp
C:\workdir\mruby\string.h(23): error C4430: missing type specifier - int assumed. Note: C++ does not support default-int
C:\workdir\mruby\string.h(26): error C3646: 'len': unknown override specifier
Enter fullscreen mode Exit fullscreen mode

I've tried mruby 3 and mruby 2.1.2 with the same result.

It looks like the C headers are not being parsed correctly by cl.exe but I don't know enough to work out the underlying problem.

Collapse
 
lukestuts profile image
Luke Stutters

I got the mruby headers and .lib files working on my Windows 10 PC using the following process:

  • After downloading the mruby source from github, check out the 2.1.2 version as follows:
git checkout tags/2.1.2
Enter fullscreen mode Exit fullscreen mode
  • Build mruby as described in the article in a x64 Native Tools Command Prompt for VS2019 window (ruby minirake)
  • Copy libmruby.lib and libmruby_core.lib to the working directory as described in the article
  • Copy mruby\include to the working directory as described in the article. The mruby.h file and the mruby folder should be in the same directory as the main.cpp file.

Then, I needed to do some things differently to get the article code to compile. The differences needed on my system were:

  • Add /MD to the compiler options, since the ruby minirake task is compiling libmruby.lib and libmruby_core.lib with this option. (I worked this out by looking at the ruby minirake --verbose output. The compiler messages were not helpful.)
  • Add ws2_32.lib to the end of the compile command

My minimal test program for mruby is:

// test.cpp
#include "mruby.h"
#include "mruby/compile.h"
int main() {
    mrb_state *mrb = mrb_open();
    mrb_load_string(mrb, "5.times { puts 'derp' }");
    mrb_close(mrb);
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

and the .bat file to compile it is:

rem Build test.cpp
cl /Zi /MD /GR- /nologo /I"%~dp0." test.cpp /link user32.lib libmruby.lib libmruby_core.lib ws2_32.lib
Enter fullscreen mode Exit fullscreen mode

I needed to specify the absolute path of the working directory with the /I option on my system using the %~dp0.

The last thing that confused me is that adding /SUBSYSTEM:windows /ENTRY:mainCRTStartup to the end of the compile command will hide all printf or puts output from mruby, so I recommend leaving these out while debugging.