|
| 1 | +--- |
| 2 | +title: "Porting a library to an ESP-IDF component" |
| 3 | +date: "2025-10-20" |
| 4 | +summary: "This article shows how to port an external library into an ESP-IDF project by converting it into a reusable component. Using tinyexpr as an example, it covers obtaining the source code, creating a new project, building a component, configuring the build system, and testing on hardware." |
| 5 | +authors: |
| 6 | + - "francesco-bez" |
| 7 | +tags: ["esp32c3", "component","porting"] |
| 8 | +--- |
| 9 | + |
| 10 | +## Introduction |
| 11 | + |
| 12 | +When developing with [ESP-IDF](https://github.com/espressif/esp-idf), you may eventually need functionality that isn’t provided by its built-in components. In many cases, the solution is an open-source library from GitHub or another repository. However, these external libraries are not always structured for ESP-IDF, and simply copying their source files into your project can lead to messy integrations, difficult updates, and code that’s hard to reuse or share. |
| 13 | + |
| 14 | +A cleaner and more maintainable approach is to wrap the external library into a **reusable ESP-IDF component**. Components integrate naturally with the ESP-IDF build system, can be reused across multiple projects, and make sharing and maintaining code much easier. They also help keep your project organized and scalable over time. |
| 15 | + |
| 16 | +In this article, we’ll demonstrate this approach by porting the [tinyexpr](https://github.com/codeplea/tinyexpr?tab=readme-ov-file#tinyexpr) library into a fully functional ESP-IDF component, ready for reuse in future applications. |
| 17 | +To keep things simple, we’ll start by manually adding the library’s source and header files into a new component structure. In a future article, we’ll extend this setup by adding the library as a git submodule, making it easier to stay synchronized with the upstream repository as new features or bug fixes are released. |
| 18 | + |
| 19 | +We’ll be using an **ESP32-C3-DevKitM-1** board and the **ESP-IDF Extension for VS Code**, though the same steps apply to other boards and SoCs. |
| 20 | + |
| 21 | +## Prepare to create a component |
| 22 | + |
| 23 | +To keep things straightforward, we will work with a self-contained library that does not rely on specific peripherals. A good example of such a library is _tinyexpr_, which we will use throughout this article. |
| 24 | + |
| 25 | +Integrating an external library as an ESP-IDF component involves several steps: |
| 26 | + |
| 27 | +1. Obtain library code |
| 28 | +2. Set up a test project in ESP-IDF |
| 29 | +3. Test the component |
| 30 | +4. Solve compatibility issues |
| 31 | + |
| 32 | +These are the steps we will follow in the rest of the article. |
| 33 | + |
| 34 | +## Obtain library code |
| 35 | + |
| 36 | +The tinyexpr code is available on GitHub: |
| 37 | + |
| 38 | +{{< github repo="codeplea/tinyexpr" >}} |
| 39 | + |
| 40 | +We can clone the repository to inspect the files: |
| 41 | + |
| 42 | +```bash |
| 43 | +git clone https://github.com/codeplea/tinyexpr.git |
| 44 | +``` |
| 45 | + |
| 46 | +Although the repository contains several source files, the library itself consists of just two key files, as noted in the `README`: |
| 47 | + |
| 48 | +* `tinyexpr.h` |
| 49 | +* `tinyexpr.c` |
| 50 | + |
| 51 | +There are also several examples in the repo, showing how to use the library. The simplest one is `example.c`, so it's a good idea to use it to test our component. |
| 52 | + |
| 53 | +## Set up a test project in ESP-IDF |
| 54 | + |
| 55 | +To work on the porting, we need to |
| 56 | +1. Create a new project |
| 57 | +2. Create a new component |
| 58 | + |
| 59 | +### Create a new project |
| 60 | + |
| 61 | +To keep things simple and organized, we will start with a basic project using a template app. You can follow one of the two approaches below: |
| 62 | + |
| 63 | +{{< tabs groupId="devtool" >}} |
| 64 | + {{% tab name="ESP-IDF Extension for VS Code New Project" %}} |
| 65 | + |
| 66 | +* In VS Code command palette enter: `> ESP-IDF: New Project` |
| 67 | + |
| 68 | +You will be presented with a screen like Fig.1. |
| 69 | +{{< figure |
| 70 | +default=true |
| 71 | +src="img/new_project.webp" |
| 72 | +height=500 |
| 73 | +caption="Fig.1 - Choosing the project settings" |
| 74 | + >}} |
| 75 | + |
| 76 | +If you're using the ESP32-C3-DevKitM, fill the fields as follows: |
| 77 | +* Project name: `esp_tinyexpr_test` |
| 78 | +* Choose ESP-IDF Target: `esp32c3` |
| 79 | +* Choose ESP-IDF Board: `ESP32-C3 chip (via builtin USB-JTAG)` |
| 80 | +* Choose serial port: <YOUR SERIAL PORT> (e.g. `COM25` or `/dev/tty.usbserial-11133`) |
| 81 | + |
| 82 | +Feel free to change the settings according to your platform. |
| 83 | + |
| 84 | +* Click on `Choose Template` and select `template_app` |
| 85 | +* Open the project with VS Code |
| 86 | + |
| 87 | +You should now have the following project structure: |
| 88 | +```bash |
| 89 | +. |
| 90 | +├── CMakeLists.txt |
| 91 | +├── main |
| 92 | +│ ├── CMakeLists.txt |
| 93 | +│ └── main.c |
| 94 | +└── README.md |
| 95 | +``` |
| 96 | + |
| 97 | + |
| 98 | + {{% /tab %}} |
| 99 | + {{% tab name="Clone basic example" %}} |
| 100 | +* Download the basic example code found on [this github repo](https://github.com/FBEZ-docs-and-templates/devrel-tutorials-code/tree/main/tutorial-basic-project). |
| 101 | +* Open the project with VSCode |
| 102 | +* `> ESP-IDF: Set Espressif Device target` |
| 103 | +* `> ESP-IDF: Select Port to Use` |
| 104 | + |
| 105 | + {{% /tab %}} |
| 106 | +{{< /tabs >}} |
| 107 | + |
| 108 | + |
| 109 | +### Create a new component |
| 110 | + |
| 111 | +Next, we will create a new component named `my_tinyexpr` to hold the `tinyexpr` files. |
| 112 | + |
| 113 | +* Select `> ESP-IDF: Create New ESP-IDF Component` |
| 114 | + |
| 115 | + * Name it `my_tinyexpr` |
| 116 | + |
| 117 | +Your project structure now looks like this. |
| 118 | + |
| 119 | +```bash |
| 120 | +. |
| 121 | +├── CMakeLists.txt |
| 122 | +├── components |
| 123 | +│ └── my_tinyexpr |
| 124 | +│ ├── CMakeLists.txt |
| 125 | +│ ├── include |
| 126 | +│ │ └── my_tinyexpr.h |
| 127 | +│ └── my_tinyexpr.c |
| 128 | +├── main |
| 129 | +│ ├── CMakeLists.txt |
| 130 | +│ └── main.c |
| 131 | +├── README.md |
| 132 | +├── sdkconfig |
| 133 | +└── sdkconfig.old |
| 134 | +``` |
| 135 | + |
| 136 | +* Replace `my_tinyexpr.c` with the downloaded `tinyexpr.c` |
| 137 | +* Replace `my_tinyexpr.h` with the downloaded `tinyexpr.h` |
| 138 | + |
| 139 | +Since the filenames differ from the default, update the component's `CMakeLists.txt` to register the correct source file: |
| 140 | + |
| 141 | +```txt |
| 142 | +idf_component_register(SRCS "tinyexpr.c" |
| 143 | + INCLUDE_DIRS "include") |
| 144 | +``` |
| 145 | + |
| 146 | +{{< alert icon="circle-info" cardColor="#b3e0f2" iconColor="#04a5e5">}} |
| 147 | +The build system automatically includes all files in the `include` directory, so no additional configuration is needed to locate `tinyexpr.h`. |
| 148 | +{{< /alert >}} |
| 149 | + |
| 150 | +Now that we’ve created the new component, it’s time to test it. |
| 151 | + |
| 152 | +## Test the component |
| 153 | + |
| 154 | +To test the component we need to |
| 155 | +1. Inform the build system about the new component |
| 156 | +2. Include the header file |
| 157 | +3. Call the function of the library |
| 158 | + |
| 159 | +### Inform the build system about the new component |
| 160 | + |
| 161 | +In the `CMakeLists.txt` of the `__main__` component, add `REQUIRES "my_tinyexpr"` to let the build system know about the new component: |
| 162 | + |
| 163 | +```txt |
| 164 | +idf_component_register(SRCS "main.c" |
| 165 | + REQUIRES "my_tinyexpr" |
| 166 | + INCLUDE_DIRS ".") |
| 167 | +``` |
| 168 | + |
| 169 | +This ensures that the build system includes `my_tinyexpr` when compiling your project. |
| 170 | + |
| 171 | + |
| 172 | +### Include the header file |
| 173 | + |
| 174 | +In your main file, include the header from the `tinyexpr` library: |
| 175 | + |
| 176 | +```c |
| 177 | +#include "tinyexpr.h" |
| 178 | +``` |
| 179 | + |
| 180 | + |
| 181 | +{{< alert icon="lightbulb" iconColor="#179299" cardColor="#9cccce">}} |
| 182 | +In general, a component’s header file does not need to match its name, and this applies broadly across ESP-IDF projects. |
| 183 | +{{< /alert >}} |
| 184 | + |
| 185 | +### Call a function of the library |
| 186 | + |
| 187 | +In the tinyexpr repository, there is an `example.c` file that demonstrates how to use the library. |
| 188 | + |
| 189 | +* Copy the relevant portions of `example.c` into your `main.c` file. |
| 190 | + |
| 191 | +Your `app_main` function should now look like this: |
| 192 | + |
| 193 | +```c |
| 194 | +#include <stdio.h> |
| 195 | +#include "tinyexpr.h" |
| 196 | + |
| 197 | +void app_main(void) |
| 198 | +{ |
| 199 | + const char *c = "sqrt(5^2+7^2+11^2+(8-2)^2)"; |
| 200 | + double r = te_interp(c, 0); |
| 201 | + printf("The expression:\n\t%s\nevaluates to:\n\t%f\n", c, r); |
| 202 | +} |
| 203 | +``` |
| 204 | +
|
| 205 | +Next, build, flash, and monitor the project: |
| 206 | +
|
| 207 | +* `ESP-IDF: Build, Flash and Start a Monitor on Your Device` |
| 208 | +
|
| 209 | +And we got and error! |
| 210 | +
|
| 211 | +```console |
| 212 | +In file included from <PATH>/tutorial-porting-tinyexpr/components/my_tinyexpr/tinyexpr.c:43: |
| 213 | +<PATH>/tutorial-porting-tinyexpr/components/my_tinyexpr/tinyexpr.c: In function 'next_token': |
| 214 | +<PATH>/tutorial-porting-tinyexpr/components/my_tinyexpr/tinyexpr.c:255:32: error: array subscript has type 'char' [-Werror=char-subscripts] |
| 215 | + 255 | if (isalpha(s->next[0])) { |
| 216 | + | ~~~~~~~^~~ |
| 217 | +<PATH>/tutorial-porting-tinyexpr/components/my_tinyexpr/tinyexpr.c:258:39: error: array subscript has type 'char' [-Werror=char-subscripts] |
| 218 | + 258 | while (isalpha(s->next[0]) || isdigit(s->next[0]) || (s->next[0] == '_')) s->next++; |
| 219 | + | ~~~~~~~^~~ |
| 220 | +<PATH>/tutorial-porting-tinyexpr/components/my_tinyexpr/tinyexpr.c:258:62: error: array subscript has type 'char' [-Werror=char-subscripts] |
| 221 | + 258 | while (isalpha(s->next[0]) || isdigit(s->next[0]) || (s->next[0] == '_')) s->next++; |
| 222 | + | ~~~~~~~^~~ |
| 223 | +cc1: some warnings being treated as errors |
| 224 | +ninja: build stopped: subcommand failed. |
| 225 | +``` |
| 226 | + |
| 227 | +There is a compatibility issue. Although the library you found is written in C and appears to work, compiler settings or library dependencies could still cause problems. Let’s investigate what is preventing the compilation. |
| 228 | + |
| 229 | +## Solve compatibility issues |
| 230 | + |
| 231 | +The error is in the following code. |
| 232 | + |
| 233 | +```c |
| 234 | +if (isalpha(s->next[0])) { ... } |
| 235 | +while (isalpha(s->next[0]) || isdigit(s->next[0]) || (s->next[0] == '_')) s->next++; |
| 236 | +``` |
| 237 | +
|
| 238 | +It happens because a `char` is directly passed to `isalpha()` and `isdigit()`. On many platforms, `char` is signed, and these functions expect an `int` in the range of `unsigned char` (or `EOF`). Passing a signed `char` can trigger warnings or undefined behavior. |
| 239 | +
|
| 240 | +ESP-IDF uses very strict compilation flags and treats all warnings as errors (`-Werror`), which is why these warnings stop the build. |
| 241 | +
|
| 242 | +So we have two options, changing the code or changing the compilation flag. |
| 243 | +
|
| 244 | +{{< alert iconColor="#df8e1d" cardColor="#edcea3">}} |
| 245 | +Modifying the code of an external library is not recommended, as it makes it harder to keep your version synchronized with the official release and to apply future updates or bug fixes. |
| 246 | +{{< /alert >}} |
| 247 | +
|
| 248 | +
|
| 249 | +### Changing the code |
| 250 | +
|
| 251 | +A fast way to fix it is to cast the character to `unsigned char`: |
| 252 | +
|
| 253 | +```c |
| 254 | +if (isalpha((unsigned char)s->next[0])) { ... } |
| 255 | +
|
| 256 | +while (isalpha((unsigned char)s->next[0]) || |
| 257 | + isdigit((unsigned char)s->next[0]) || |
| 258 | + (s->next[0] == '_')) s->next++; |
| 259 | +``` |
| 260 | + |
| 261 | +Now we can build, flash, and monitor the project again. |
| 262 | + |
| 263 | +* `ESP-IDF: Build, Flash and Start a Monitor on Your Device` |
| 264 | + |
| 265 | +And we get the expected output. |
| 266 | + |
| 267 | +```console |
| 268 | +The expression: |
| 269 | + sqrt(5^2+7^2+11^2+(8-2)^2) |
| 270 | +evaluates to: |
| 271 | + 15.198684 |
| 272 | +``` |
| 273 | + |
| 274 | +### Changing the compilation flag |
| 275 | + |
| 276 | +Alternatively, the issue can be resolved by adjusting the compilation flags instead of modifying the source code. Since the error originates from `-Werror=char-subscripts`, we can suppress it by adding the following line to the `CMakeLists.txt` file of your component: |
| 277 | + |
| 278 | +```cmake |
| 279 | +target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-char-subscripts) |
| 280 | +``` |
| 281 | + |
| 282 | +This approach is often preferable when working with external libraries, as it allows you to keep the original source code intact. It also makes it easier to manage updates: if the library is included as a git submodule, any future improvements or security fixes can be applied by simply updating the submodule, without the need to reapply code changes. |
| 283 | + |
| 284 | + |
| 285 | +Now that we’ve covered the basics of creating a component and resolving common compatibility issues, the next article will build on this foundation. You’ll see how to import a component as a git submodule, integrate it into multiple projects, and share it with the community. This workflow not only helps keep your code organized and maintainable but also ensures that updates and improvements can be easily propagated across projects without modifying the original source. |
| 286 | + |
| 287 | +## Conclusion |
| 288 | + |
| 289 | +In this article, we demonstrated how to take an existing open source library and integrate it into an ESP-IDF project as a reusable component. We located the tinyexpr library, created a new ESP-IDF project, built a dedicated component for the library, resolved compatibility details, and verified its functionality on an ESP32-C3-DevkitM-1 board. By packaging the library as a component rather than copying source files directly, we ensured cleaner integration, easier maintenance, and effortless reuse in future projects. |
0 commit comments