Ruby and Embedded Systems
It would seem, what does “hipster scripts for the web” have to do with the harsh realities of embedded development and embedded systems, with all their low-level work and limited resources? Alas, reality once again turned out to be much more interesting than prejudice, and this is how this article was born.
What is it and why
I'll start as usual with a quote:
mruby is the lightweight implementation of the Ruby language complying with part of the ISO standard. mruby can be linked and embedded within your application.
In a word, this is such special implementation of the Ruby language, with an emphasis on embedding and embedded systems – yes, yes, that very “bloody embedded“, where pure C reigns, reference arithmetic, malloc()
and other nightmares and horrors for the modern developer.
And suddenly you appear in this kingdom of Hades all in white and write something like this, high-level:
extend Yeah::DSL
set port: 3000
get '/hi/{name}' do |name|
"Привет #{name}"
end
ENV['SHELF_ENV'] = 'production'
puts "Запуск.."
__main__ [0]
And.. it just works:
And even like this:
Cool?
How many pounds of salt do you need to eat in order to work with Unicode from C so easily, especially in an embedded environment?
By the way, all this “hipster joy” is also collected in a very small binary:
Inside there will be “everything at once”:
MRuby, all used libraries and the application itself.
Although the ENT specialists noticed that “640kb is enough for everyone” this will somehow be a bit much, but let me remind you that we live in a world of pennies 128GB flash drives and the battle for every byte of free space is no longer as relevant as 10 years ago.
I fully admit that such an application displays a web interface on your home router, displays a menu on a TV, or plays advertisements on a bus – in short, it is used in most places where embedded systems are used.
Supported Platforms
Unfortunately, it was not possible to find all platforms supported by MRuby in one list, so I will limit myself only to specific examples found.
The most interesting:
development for Sega Dreamcast and Nintendo WiiFor POS terminalsembedding in iOS applications.
Well, the actual built-in systems:
Test project
Since “raspberry” was once again not at hand, it was decided to implement a test project on a banal x86 – the entire development chain was assembled, including frameworks, “Hello world” was implemented as a web application running on a built-in web server.
All manipulations were performed on an unsupported no one and nowhere FreeBSD, so most likely the problems described below with assembly on a more common Linux will not occur.
For the test project I used this: This “miracle”:
Yeah! is a DSL for quickly creating shelf applications in mruby with minimal effort
In short, this is a kind of attempt to implement “mini-Rails running on mini-Ruby.” Quite successful, it should be noted.
And now the most important thing:
Frameworks for
mruby
represent superstructurewhich in the process collects itselfmruby
and adds itself to the compiled binaries.
It sounds complicated and looks scary, but for an embedded environment it is commonplace.
So we will need to get the binaries mruby
And mrbc
with a framework packed inside yeah
and all the libraries, and then use this build to build your application.
To build the Yeah! requires external “big” Ruby and
rake
both 2.x and 3.x versions will work.
We clone a project with the Yeah! framework:
git clone https://github.com/katzer/mruby-yeah.git
Since by default only the compiler is built mirbc
without an interactive console (mirb) and an interpreter (mruby), which is not enough for normal development of the final application, add it to the file build_config.rb
:
conf.gem :core => 'mruby-bin-mruby'
conf.gem :core => 'mruby-bin-mirb'
conf.gem :core => 'mruby-bin-mrbc'
And start the build:
rake compile
This command will automatically download dependent repositories, including the desired branch of the mruby
. At this stage, the author encountered two errors.
The first one is about the header file mingw.h
:
In file included from /opt/work/tmp/mruby-yeah/mruby/build/repos/host/mruby-r3/r3/src/memory.c:34:
/opt/work/tmp/mruby-yeah/mruby/build/repos/host/mruby-r3/r3/src/mman.h:15:10: fatal error: _mingw.h: No such file or directory
15 | #include <_mingw.h>
| ^~~~~~~~~~
compilation terminated.
rake aborted!
In file mman.h
there is this line:
/* All the headers include this file. */
#ifndef _MSC_VER
#include <_mingw.h>
#endif
Variable _MSC_VER
is not set when building on FreeBSD, so the default option works – for Windows and MinGW. As a correction, I simply commented out this block without bothering with further research.
The second error is also quite trivial and occurs due to a difference in the implementation of the function mmap
:
/opt/work/tmp/mruby-yeah/mruby/build/repos/host/mruby-r3/r3/src/mman.h:52:9: error: conflicting types for 'mmap'; have 'void *(void *, size_t, int, int, int, OffsetType)' {aka 'void *(void *, long unsigned int, int, int, int, unsigned int)'}
52 | void* mmap(void *addr, size_t len, int prot, int flags, int fildes, OffsetType off);
| ^~~~
In file included from /opt/work/tmp/mruby-yeah/mruby/build/repos/host/mruby-r3/r3/src/memory.c:27:
/usr/include/stdio.h:444:10: note: previous declaration of 'mmap' with type 'void *(void *, size_t, int, int, int, __off_t)' {aka 'void *(void *, long unsigned int, int, int, int, long int)'}
444 | void *mmap(void *, size_t, int, int, int, __off_t);
| ^~~~
rake aborted!
In the same file mman.h
replace:
void* mmap(void *addr, size_t len, int prot, int flags, int fildes, OffsetType off);
to:
void* mmap(void *addr, size_t len, int prot, int flags, int fildes, __off_t);
And we restart the assembly.
If the assembly was successful, then in the folder build/host/bin
there will be ready-made binaries:
ls ./mruby/build/host/bin/
mirb mrbc mruby
Now, with their help, we launch the assembly of our test application:
/opt/work/mruby-yeah/mruby/build/host/bin/mrbc -Btest_symbol ~/test.rb
This will generate the file test.c with this content:
#include <stdint.h>
#ifdef __cplusplus
extern
#endif
const uint8_t test_symbol[] = {
0x52,0x49,0x54,0x45,0x30,0x33,0x30,0x30,0x00,0x00,0x01,0x3e,0x4d,0x41,0x54,0x5a,
0x30,0x30,0x30,0x30,0x49,0x52,0x45,0x50,0x00,0x00,0x01,0x0c,0x30,0x33,0x30,0x30,
0x00,0x00,0x00,0xcd,0x00,0x01,0x00,0x05,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x3d,
0x1d,0x02,0x01,0x1f,0x02,0x00,0x2d,0x01,0x02,0x01,0x10,0x02,0x03,0x0e,0x03,0x0b,
0xb8,0x2d,0x01,0x04,0x10,0x51,0x02,0x00,0x57,0x03,0x00,0x2e,0x01,0x05,0x01,0x1d,
0x01,0x06,0x51,0x02,0x01,0x51,0x03,0x02,0x24,0x01,0x51,0x02,0x03,0x2d,0x01,0x07,
0x01,0x06,0x02,0x47,0x02,0x01,0x2d,0x01,0x08,0x01,0x38,0x01,0x69,0x00,0x04,0x00,
0x00,0x0a,0x2f,0x68,0x69,0x2f,0x7b,0x6e,0x61,0x6d,0x65,0x7d,0x00,0x00,0x00,0x09,
0x53,0x48,0x45,0x4c,0x46,0x5f,0x45,0x4e,0x56,0x00,0x00,0x00,0x0a,0x70,0x72,0x6f,
0x64,0x75,0x63,0x74,0x69,0x6f,0x6e,0x00,0x00,0x00,0x0e,0xd0,0x97,0xd0,0xb0,0xd0,
0xbf,0xd1,0x83,0xd1,0x81,0xd0,0xba,0x2e,0x2e,0x00,0x00,0x09,0x00,0x03,0x44,0x53,
0x4c,0x00,0x00,0x04,0x59,0x65,0x61,0x68,0x00,0x00,0x06,0x65,0x78,0x74,0x65,0x6e,
0x64,0x00,0x00,0x04,0x70,0x6f,0x72,0x74,0x00,0x00,0x03,0x73,0x65,0x74,0x00,0x00,
0x03,0x67,0x65,0x74,0x00,0x00,0x03,0x45,0x4e,0x56,0x00,0x00,0x04,0x70,0x75,0x74,
0x73,0x00,0x00,0x08,0x5f,0x5f,0x6d,0x61,0x69,0x6e,0x5f,0x5f,0x00,0x00,0x00,0x00,
0x33,0x00,0x03,0x00,0x05,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0e,0x34,0x04,0x00,
0x00,0x51,0x03,0x00,0x01,0x04,0x01,0x52,0x03,0x38,0x03,0x00,0x01,0x00,0x00,0x0d,
0xd0,0x9f,0xd1,0x80,0xd0,0xb8,0xd0,0xb2,0xd0,0xb5,0xd1,0x82,0x20,0x00,0x00,0x00,
0x4c,0x56,0x41,0x52,0x00,0x00,0x00,0x16,0x00,0x00,0x00,0x01,0x00,0x04,0x6e,0x61,
0x6d,0x65,0x00,0x00,0xff,0xff,0x45,0x4e,0x44,0x00,0x00,0x00,0x00,0x08,
};
This is nothing more than ready-made mruby bytecode, written as a static byte array.
Parameter -Btest_symbol as you might guess, is responsible for the name of this array.
And this is what the launch application itself looks like (file test_stub.c
):
#include <mruby.h>
#include <mruby/irep.h>
#include <test.c>
int
main(void)
{
mrb_state *mrb = mrb_open();
if (!mrb) { /* handle error */ }
mrb_load_irep(mrb, test_symbol);
mrb_close(mrb);
return 0;
}
All that happens here is simply issuing bytecode to the interpreter mruby
when the wrapper starts. Pay attention to the line:
#include <test.c>
This is the inclusion of a file with bytecode mruby
.
And finally, building the final test application:
gcc -std=c99 -static -Os -s -I/opt/work/mruby-yeah/mruby/include -I. test_stub.c -o test_program /opt/work/mruby-yeah/mruby/build/host/lib/libmruby.a -lm -lpthread
If everything goes well, the final binary will appear in the current folder test_program
which I launched at the very beginning.
Epilogue
This article began with a post on ENT about a “Ruby binary” with a ridiculous size of less than a megabyte and cross-compilation for embedded systems. I was impressed and started digging, and this article was born.
I think the material presented is enough for those readers who deal with embedded systems to try MRuby for their tasks, fortunately the author sees this thing as extremely promising.
As well as the government of Japan, which this project finances.
Simply because it removes a whole class of problems associated with developing application systems in pure C – memory management, Unicode, strings, and so on.
I'm looking forward to feedback on actual use.
PS
This is a slightly censored version of the article, original which is available on our blog.
0x08 Software
We are a small team of IT industry veterans, we create and develop a wide variety of software; our software automates business processes on three continents, in a wide variety of industries and conditions.
Let's revive long dead, fixing something that never worked and create impossible — then we talk about it in our articles.