inih: a SAX-style INI parser in C for embedded systems
Simple .INI file parser in C, good for embedded systems
At a glance
- What is it?
- inih parses .INI files by calling a handler for every name=value pair instead of building a document tree. It suits low-memory targets, but the fixed-size buffers and the frozen C++ wrapper are real constraints.
- Who is it for?
- Adopt inih if you need a permissively small C parser that streams name=value pairs into your own structs and you are willing to set INI_MAX_LINE, INI_MAX_SECTION and INI_MAX_NAME for your longest real input. Do not adopt it if you need a queryable document model, a supported C++ API with GetSections() and GetFields(), or a parser that fails loudly on truncated names.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 3 days ago.
- What is it written in?
- Mainly C++, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 30, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The problem inih solves, and who ends up using it
Most INI libraries build a tree: parse the whole file, store sections and keys in memory, then let the caller query them. That model assumes a heap large enough for the document plus the lookup structure. inih takes the opposite position. The README describes it as "only a couple of pages of code" designed to be "small and simple", and the parsing model is SAX style: `ini_parse()` walks the file and calls a callback for every `name=value` pair, handing over the section, name and value as strings. Nothing is retained between calls unless your handler retains it.
That makes the target audience narrow and identifiable. It is firmware and embedded developers who already know the shape of their configuration and just want the values copied into existing structs. It is also anyone who wants a single .c and .h pair dropped into a build with no dependency management. The README also notes compatibility with Python's ConfigParser style, including RFC 822-style multi-line syntax and `name: value` entries, so a config file authored for Python tooling is mostly readable by the same C code.
The cost of that design is that inih has no opinion about your schema. Unknown keys are your problem: the README's C example returns 0 from the handler for anything it does not recognise, which the parser treats as an error. There is no built-in notion of required keys, defaults, or type coercion in the C API.
How the callback data flow works in ini.c and ini.h
The public surface is small. `ini_parse()` takes a filename, a handler and a user pointer. `ini_parse_file()` reads from a `FILE*`, `ini_parse_string()` reads from a string, and `ini_parse_stream()` accepts a custom fgets-style reader function for non-standard I/O. All four funnel into the same line-oriented scanner in `ini.c`.
The scanner reads a line into a fixed buffer, strips the trailing `\r`, `\n` and NUL, and decides what the line is. A line beginning with a start-of-line comment prefix is skipped. A `[section]` line updates the current section. A `name=value` or `name: value` line is split, and the handler is invoked with the current section, the name and the value. Return 1 from the handler to continue, 0 to abort with an error. The return value of the parse call is negative on a file error and positive on the line number where the handler rejected a pair, which is a compact way to report failure without any error object.
The handler signature is the whole contract. It receives `void* user`, `const char* section`, `const char* name`, `const char* value`. If you compile with `INI_HANDLER_LINENO=1` you also get a line number, which is the only way to produce a useful message about where a bad value came from. Two other compile-time switches change the callback pattern: `INI_CALL_HANDLER_ON_NEW_SECTION=1` invokes the handler when a new section starts, with `name` and `value` set to NULL, which is how you detect a repeated section name. `INI_ALLOW_NO_VALUE=1` invokes the handler with `value` set to NULL for a bare name, instead of treating it as an error.
Installing inih and parsing a first config file
There is no package to install. The README says to download a release or browse the source, and the repository root contains `ini.c`, `ini.h` and a `meson.build`. The README's own example includes the header with a relative path, `#include "../ini.h"`, which tells you the expected layout: keep `ini.c` and `ini.h` together and compile `ini.c` into your project.
The README's C example is the shortest path from zero to a parsed value. The handler is where the work happens:
static int handler(void* user, const char* section, const char* name,
const char* value)
{
configuration* pconfig = (configuration*)user;
#define MATCH(s, n) strcmp(section, s) == 0 && strcmp(name, n) == 0
if (MATCH("protocol", "version")) {
pconfig->version = atoi(value);
} else if (MATCH("user", "name")) {
pconfig->name = strdup(value);
} else {
return 0; /* unknown section/name, error */
}
return 1;
}Returning 0 aborts the parse, and the return value of `ini_parse()` is then the line number. The `examples/` directory also holds `ini_dump.c` and `ini_xmacros.c`; the README links a blog post about the X-Macros approach for keeping the handler and the struct in sync, which is worth reading before you write a long if-else chain by hand.
For C++ the repository ships `cpp/INIReader.h`. The README's example constructs an `INIReader`, checks `ParseError()`, then reads typed values with defaults:
INIReader reader("../examples/test.ini");
if (reader.ParseError() < 0) {
std::cout << "Can't load 'test.ini'\n";
return 1;
}
std::cout << reader.GetInteger("protocol", "version", -1) << ", name="
<< reader.Get("user", "name", "UNKNOWN") << ", email="
<< reader.Get("user", "email", "UNKNOWN") << ", pi="
<< reader.GetReal("user", "pi", -1) << ", active="
<< reader.GetBoolean("user", "active", true) << "\n";The `Get*` family takes a default, so a missing key is not an error unless you make it one. `examples/cpptest.sh` and the `cpptest.txt` / `cpptesterrors.txt` files show how the C++ path is exercised in the repository.
Fixed buffers, silent truncation and the limits you must check
The defaults are the limitation. Section names and property names live in fixed buffers of `INI_MAX_SECTION` and `INI_MAX_NAME` bytes, both 50 including the NUL. The README states plainly that longer names are silently truncated. Silent is the operative word: a config key that is 52 characters long will match nothing in your handler, the handler will return 0 for an unknown key if you wrote it that way, and the parse will fail with a line number but no indication that truncation caused it. Raise both defines if your key names are long, and treat the defaults as a design decision rather than a bug.
The line buffer has the same shape. The default maximum line length is 200 bytes, and the README warns that `INI_MAX_LINE` must be 3 more than the longest line because of `\r`, `\n` and the NUL. By default the buffer is on the stack. `INI_USE_STACK=0` moves it to the heap with `malloc`, `INI_INITIAL_ALLOC` sets the initial size (200 by default), and `INI_ALLOW_REALLOC=1` lets it grow up to `INI_MAX_LINE`, doubling as needed. If you have a custom memory strategy, `INI_CUSTOM_ALLOCATOR=1` requires you to define and link `ini_malloc`, `ini_free` and, when realloc is enabled, `ini_realloc` with the same signatures as the stdlib functions.
Error handling is also a choice you make at compile time. By default inih keeps parsing after an error. `INI_STOP_ON_FIRST_ERROR=1` changes that. Neither mode gives you structured diagnostics; you get a line number and whatever your handler decides to print. If you need to report every bad key with a message, the handler is the only place to do it.
When inih is the wrong tool
If your program needs to ask questions about the file after parsing, inih is the wrong shape. There is no document object in the C API. Every value must be copied into your own storage inside the handler, and if you want to look up a key later you implement that lookup yourself. A tree-building parser such as iniparser or minIni gives you a queryable structure instead, at the cost of holding the whole file in memory. That trade is the entire decision.
The C++ wrapper is a second boundary. The README says the simple C++ API "works fine, but it's not very fully-fledged" and that the author is not planning to work more on it, pointing readers to the Blandinium and OSSystems forks for `GetSections()` and `GetFields()`. If your design depends on enumerating sections or fields through the INIReader class, you are depending on a surface the maintainer has declared frozen. The most recent release, r62, added `INIReader::ParseErrorMessage`, so the wrapper is not abandoned, but the README's statement about future work is the thing to weigh.
One more case: if your configuration is written by a tool that emits duplicate keys, inih will call your handler for each occurrence in order. There is no last-wins or first-wins policy imposed by the parser. Your handler decides, and if you did not think about it, the behaviour is whatever your if-else chain happens to do.
Alternatives and the actual difference in approach
iniparser and minIni are the closest comparisons, and the difference is architectural rather than cosmetic. Both build an in-memory representation of the file and expose lookup functions, so you parse once and query many times. inih never allocates a document. That matters on a microcontroller where the config file may be larger than the free heap, and it matters less on a desktop tool where holding a few kilobytes is irrelevant. If you find yourself writing a section table and a key lookup on top of an inih handler, you have reimplemented the other libraries badly and should switch.
rxi/ini and Inipp appear in the same search space. They are also small INI parsers, and the meaningful question to ask of any of them is the same three-part check: what happens on a line longer than the buffer, what happens on a name longer than the buffer, and what the handler or result object gives you on error. inih's answers are documented in the README, which is more than can be said for many single-file parsers. That documentation is the practical advantage here.
For Python-side work, the README's compatibility claim is the useful part: inih follows ConfigParser conventions closely enough that the same file usually works in both. If your pipeline generates config with Python and consumes it in C, that shared dialect reduces the number of format surprises, though the README says "more or less compatible", so edge cases are not guaranteed.
Maintenance, licence and the cost of upgrading
The repository is not archived, and the last push was on 2026-09-27. Releases are tagged with short names: r60 (2025-04-07) was described as aligning versions with no code changes, r61 arrived on 2025-07-25, and r62 on 2025-09-11 added `INIReader::ParseErrorMessage`. The cadence is slow and the changes are small, which is consistent with a project whose README calls it a couple of pages of code. There is a `fuzzing/` directory and a GitHub Actions tests workflow, so the parser is exercised beyond the examples.
Upgrade cost is low in the common case because the interface is a header and a .c file. Copy the new `ini.c` and `ini.h` over the old ones and rebuild. The risk is in the compile-time defines: if you set `INI_MAX_LINE`, `INI_MAX_SECTION` or `INI_MAX_NAME` in your build system rather than in a wrapper header, a change to those values is a silent behaviour change, not a compile error. Keep them in one place you can diff.
The licence field in the repository metadata is NOASSERTION, so the machine-readable classifier did not resolve it. The repository contains `LICENSE.txt` at the top level; read that file for the actual terms before you ship. Nothing here is legal advice, and the metadata alone does not tell you what the licence permits.
Editorial conclusion
Adopt inih if you need a permissively small C parser that streams name=value pairs into your own structs and you are willing to set INI_MAX_LINE, INI_MAX_SECTION and INI_MAX_NAME for your longest real input. Do not adopt it if you need a queryable document model, a supported C++ API with GetSections() and GetFields(), or a parser that fails loudly on truncated names. Before committing, verify the three buffer sizes against your actual config files, check the LICENSE.txt text yourself, and confirm whether the C++ wrapper's ParseError() behaviour in r62 matches how your build reports failures.
Frequently asked questions
Are INI files still used today?
They remain common enough that inih exists specifically to parse them, and the README notes compatibility with Python's ConfigParser style, including multi-line entries and name: value syntax. If your tooling already emits that dialect, the format is still doing useful work.
What is an INI file used for?
In inih's case it holds configuration as sections and name=value pairs. The parser calls a handler for each pair, giving the section, name and value as strings, so the file is a way to feed values into a program's own structs.
How do I open an INI file?
You do not open it by hand; inih does. Call ini_parse() with a filename, a handler and a user pointer, or ini_parse_file() with a FILE*, or ini_parse_string() if the data is already in memory.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/benhoyt-inih)