Programming

/

March 25, 2025

Using Lua in Embedded

This is the revised and updated version of an article originally posted on Medium. In it, we explore how integrating Lua into embedded systems can simplify development by separating business logic from hardware-specific code.

Take your future embedded project to the next level!

What’s the point?

You might ask yourself: “What do I need to use Lua for in the first place?” — and that’s a valid question!

Embedded software is dominated by the use of C, with occasional cases where C++ is used — if the system being developed is suitable for it. There are valid reasons for this state of affairs, such as:

  • Price per unit, which directly correlates with available system resources like flash or RAM
  • Real-time requirements
  • Availability of software development tools and libraries

However, I believe that in cases where execution speed is not critical, or where we don’t have to meet strict hard/soft real-time requirements, we can benefit from using higher-level, dynamic languages like Lua or Python — even in bare-metal projects.

Real-life example

While working as part of the MuditaOS team, I had the opportunity to conduct a major refactoring of the existing phone recovery subsystem. I completed this task using a mix of back-end code written in pure C and a high-level layer written in Lua.

We’ll use this case as an example of how to incorporate a higher-level language into an existing codebase and leverage its features to achieve specific goals that were not previously possible.

Why not use eLua?

If you haven’t heard of the eLua project, feel free to check it out. It aims to provide seamless Lua support for embedded environments, including all the required low-level, board-specific modules and a ready-to-use API — essentially, a complete framework.

So why didn’t I use it? The reason is simple: I found it easier to incorporate a pure, standalone Lua interpreter into the existing C codebase, rather than adopting a full-fledged framework. The codebase I was refactoring already had an established structure and all the necessary software modules, so it was simply a matter of wrapping them with Lua bindings.

Let’s talk about the software architecture, shall we?

As mentioned earlier, we’ll split the subsystem into three parts:

  1. Set of user-defined Lua scripts implementing buiness requirements
  2. A Lua interpreter running on top of the back-end, executing the scripts
  3. A back-end written in C, responsible for booting the system, providing board-specific APIs, and preparing the runtime environment.

What does it bring to the table?

Thanks to applying the right architecture, we achieved the following benefits:

Separation of business logic from the underlying platform

The back end doesn’t need to know anything about the business logic it is currently running — and it works the other way around as well. The application code is almost completely decoupled from the underlying back end, aside from the board-specific API. This low coupling and high cohesion is always something worth striving for.

Even if our system is very basic (e.g., lacking a file system), we still benefit from this separation. In such cases, we can embed Lua scripts as raw text directly into the .text section of the firmware binary. It’s a less flexible solution, but still worthwhile.

Reusability

The vast majority of the back-end code can be reused in future projects. Even if the next project is based on different hardware, you'll likely only need to implement a new port of the HAL and BSP. There's a good chance that the runtime environment (RTE) won’t require any changes. And if necessary, you can easily extend the RTE with new bindings.

Ready-to-use testing environment

If we mock the board-specific API, we can perform most (if not all) of the development on a host machine. Think about how many times you’ve had to wait for a PCB prototype to test your code. That time could be far better spent doing unit tests, integration tests, or just implementing new features.

This approach provides greater confidence during the integration phase, as much of the functionality can be tested ahead of time.

Of course, this idea isn’t new — similar workflows can be achieved in practically any language. However, with the architecture described here, it becomes especially practical and efficient.

Using Lua is just pure bliss

Compared to C, I found that writing business logic in Lua is not only much faster but also a lot more enjoyable. It’s less mentally exhausting, too. Lua is such a simple language — it doesn’t get in your way. You don’t have to worry about manual memory management or memory leaks. You can focus entirely on solving the problem at hand, instead of constantly being bogged down by language intricacies.

For less experienced developers, onboarding becomes significantly smoother. The codebase is easier to read, and the review process becomes much faster. And by the way — you can finally say goodbye to chasing undefined behaviors!

What’s more, despite being an interpreted language, Lua is still fast. Its syntax is compact and easy to learn, so you'll be productive in just a few hours.

Example

I’m not going to dive deep into the differences between C/C++ and Lua — there’s plenty of material online covering both C++ and Lua syntax in detail.

Instead, let’s just do a quick side-by-side comparison using a common operation: reading the contents of a file into a string.

C version

I've omitted proper error handling for brevity
1int read_file_content(const char* path, const char* filename, char* const buff, const size_t buff_size)
2{
3    // You need to make sure that buffer size is big enough to store full path
4    char fbuff[64];
5
6    snprintf(fbuff, sizeof(fbuff), "%s/%s", path, filename);
7
8    FILE* fd = fopen(fbuff, "r");
9    if (fd == NULL) { return -1; }
10
11    fseek(fd, 0L, SEEK_END);
12    const long fileSize = ftell(fd);
13    fseek(fd, 0L, SEEK_SET);
14
15    if (fileSize > buff_size) {
16        fclose(fd);
17        return -1;
18    }
19
20    if (fread(buff, 1, fileSize, fd) != fileSize) {
21        fclose(fd);
22        return -1;
23    }
24    fclose(fd);
25    return 0;
26}

Lua

1function open_file(path,filename){
2  local f = assert(io.open(path .. "/" .. filename, "r"))
3  local content = assert(f:read("*a"))
4  f:close()
5  return content
6}

Pretty clear, right?

I hope you can see the difference :)Code written in Lua is much more coherent and consistent. The risk of making mistakes is significantly reduced. As I mentioned earlier, both the author of the code and the reader can focus on the actual program logic, rather than getting distracted by language-specific complexities.

Everything has its price

Of course, all this added functionality and architectural cleanliness doesn’t come for free. It’s clear by now that there’s a trade-off — we need to allocate additional memory resources to support the extra abstraction layers.

Let’s take a look at how much we actually “sacrifice”.

I compiled and ran a simple project based on the NXP RT1051 with the following setup:

  • Lua 5.4, including only the 'base', 'table', and 'string' libraries
  • Release build
  • newlib standard library

The result was:

text    data     bss     dec     hex    filename
174240  2492     6544    183276  2cbec  lua_on_embedded.elf

The .text section size can be reduced even further by disabling unused libraries — Lua is very flexible in this regard. We can also switch to using newlib-nano, which should help lower memory consumption significantly.

To test runtime memory usage, I prepared a basic script that performed simple operations on associative arrays and printed some values. Using collectgarbage('count'), Lua’s garbage collector reported usage of less than 8kB of memory — pretty impressive, in my opinion.

Conclusions

We’ve only scratched the surface of a topic that’s both broad and nuanced — as is often the case in the real world. In the next episode, we’ll finally get our hands dirty and walk through how to integrate a Lua interpreter into a C/C++ project and run it on real hardware.

If you’re interested in a direct comparison between Lua and MicroPython on an embedded platform, feel free to let me know — I’d be happy to cover it!

Stay tuned!