In 2021, Inform gained the ability to generate C code which could be used as part of a larger program, for example in a framework such as Unity.


§1. Introduction. Users of the Inform UI apps make programs which are usually textual simulations, and which are in any case sealed boxes running on virtual machines such as Glulx. Although very creative things have been done to hook those up to other machinery, usually via Javascript trickery when running those VMs as part of a website, it is mostly impractical to use those tricks to have Inform 7 work as part of a bigger program not written in Inform.

However, it is now, as of 2021, possible to use Inform equally well as a command-line tool and to use it to generate code which can link into a C program, and indeed it's therefore quite easy to have a hybrid program — part Inform, part C/C++ (or indeed anything else which can call C functions). For example, one could imagine a game developed with Unity which uses a portion of Inform 7 code to manage, say, just world-modelling or text generation services.

The following documentation covers how to build hybrid Inform-and-C programs.

§2. Example makes. The Inform source repository includes a small suite of examples of such hybrid programs, stored at:

inform/inform7/Tests/Test Makes/Eg1-C
inform/inform7/Tests/Test Makes/Eg2-C
...

These toy programs are automatically built and tested as part of the Inform 7 test suite, but they can also be copied and then experimented with. We'll call these projects "makes", and indeed each one contains a makefile. Change directory to it, type make, and an executable should appear.

The makefiles assume that you have access to the clang C compiler, but gcc could equally be used, or any C-99 compatible tool. The makefiles open with a reference to the location of the inform7 installation on your filing system; so that opening will need to be edited if you move these projects out of inform/inform7/Tests/Test Makes/.

§3. Example 1: Hello World. This first example is not even a hybrid. It compiles the simplest of Basic Inform programs:

To begin:
    say "Hello world.";

...by translating it into a C program, which it then compiles, to produce a native executable. The following shows this working:

$ cd "inform/inform7/Tests/Test Makes/Eg1-C"
$ ls
Eg1.i7          ideal_output.txt    makefile
$ make -silent
$ ls
Eg1             Eg1-I.o             ideal_output.txt
Eg1-I.c         Eg1.i7              makefile
$ ./Eg1
Hello world.

ideal_output.txt contains the text which the program should print, if it works correctly: this is used in testing. The make process accomplished the following:

 Eg1.i7     source in Inform 7
   |
   | inform7
  \|/
 Eg1-I.c    source in C
   |
   | clang (acting as C compiler)
  \|/
 Eg1-I.o    C object file
   |
   | clang (acting as linker)
  \|/
  Eg1       native executable

As with all of the examples, make run runs the program, building it if necessary; and make clean removes the files generated by the build process, returning the directory to its mint condition. In this case, that means deleting Eg1-I.c, Eg1-I.o and Eg1. Note the filenaming convention: the -I in the name of, say, Eg1-I.c is supposed to signal that the file was derived from an Inform original.

The makefile for Example 1 is simple, and in a full build it runs the following:

$ make
../../../Tangled/inform7 -silence -basic -format=C -o Eg1-I.c Eg1.i7
clang -g -std=c99 -c -I ../../../Internal/Miscellany -o Eg1-I.o Eg1-I.c
clang -g -o Eg1 Eg1-I.o

All three tools compile silently if no errors occur, though in the case of inform7 that's only because -silence was asked for. -basic indicates that this is a Basic Inform program, which is not asked to contain a command parser, for example. -format=C makes the output a C program rather than an Inform 6 one, of course.

The -I ../../../Internal/Miscellany switch when using clang as C compiler tells it that this directory should be in the search path for #include files. This is needed so that:

#include "inform7_clib.h"

will work. This header file is supplied in every Inform installation at:

inform7/Internal/Miscellany/inform7_clib.h

§4. Example 2: Hello Hello. This time we begin with both a C program and an Inform program, both of which want to say hello: we want to compile these into a single executable. Now the makefile has an extra step:

    Eg2.c        Eg2.i7     source in C and Inform 7 respectively
      |             |
      |             | inform7
      |            \|/
      |          Eg2-I.c    source in C
      |             |
      | clang       | clang (acting as C compiler)
     \|/           \|/
    Eg2.o        Eg2-I.o    C object files
      |             |
      +------+------+
             |
             | clang (acting as linker)
            \|/
            Eg2             native executable

That will be the basic plan for all the remaining examples too, so we'll stop with these diagrams. Here Eg2.c is the C part of our source code, and is not to be confused with Eg2-I.c, which is the conversion to C of the Inform part of our source. Each produces an object file, and the link unites these into a single executable.

The Inform source reads:

To begin:
    say "Hello from the Inform source."

The C source could be very easy too:

#include "inform7_clib.h"

int main(int argc, char **argv) {
    printf("Hello from the C source code.\n");
    i7process_t proc = i7_new_process();
    i7_run_process(&proc);
    return 0;
}

When run, this produces:

Hello from the C source code.
Hello from the Inform source.

Note that the C program is the one in command here: in particular, it contains main. That means of course that Eg2-I.c must not include a main function, or the linking stage will throw an error; whereas Eg1-I.c, in the previous example, did have to include main. So clearly something must be different about the command to generate C from Inform this time.

This issue is resolved by tweaking the -format requested. Whereas the makefile for example 1 asked inform7 for -format=C, the makefile for example 2 asks it for -format=C/no-main. The no-main tells the part of Inform which generates C code not to make a main of its own, and so all is well. -format=C/main is the default arrangement, to which -format=C is equivalent.

§5. The type i7process_t is used for an "Inform process".1 A single C program can contain any number of these, and indeed could call them independently from multiple threads at the same time: but they are all instances of the same Inform program running, each with its own independent state and data. So, for example, two different Inform processes will have two different values for the same Inform variable name. In effect, they are isolated, independent virtual machines, each running a copy of the same program.

It is essential to create a process as shown, with a call to i7_new_process. Once created, though, the same process can be run multiple times. If we had written,

    i7process_t proc = i7_new_process();
    i7_run_process(&proc);
    i7_run_process(&proc);

then the result would have been:

Hello from the C source code.
Hello from the Inform source.
Hello from the Inform source.

§6. A note about thread safety. Suppose the C program runs multiple threads. Then:

Note that all i7process_t instances are running the same Inform program. You cannot, at present, link multiple different Inform programs into the same C program.

§7. Formally speaking, i7_run_process returns the entire Inform program to its end, and then returns an exit code: 0 if that program exited cleanly, and 1 if it halted with a fatal error, for example through dividing by zero, or failing a memory allocation. In that event, an error message will be printed to stderr.

So, for the sake of tidiness, the full Eg2.c reads:

#include "inform7_clib.h"

int main(int argc, char **argv) {
    printf("Hello from the C source code.\n");
    i7process_t proc = i7_new_process();
    int exit_code = i7_run_process(&proc);
    if (exit_code == 1) {
        printf("*** Fatal error: halted ***\n");
        fflush(stdout); fflush(stderr);
    }
    return exit_code;
}

Example 2 is in fact so simple that it is impossible for the exit code to be other than 0, but checking the exit code is good practice.

§8. Example 3: HTML Receiver. This third example demonstrates a "receiver" function. The text printed by Inform 7 can be captured, processed, and in general handled however the C program would like. This is done by assigning a receiver to the process, after it is created, but before it is run:

    i7process_t proc = i7_new_process();
    i7_set_process_receiver(&proc, my_receiver, 1);

Here my_receiver is a function. The default receiver looks like this:

void i7_default_receiver(int id, inchar32_t c, char *style) {
    if (id == I7_BODY_TEXT_ID) fputc(c, stdout);
}

This receives the text printed by the Inform process, one character at a time.

As can be seen, the default receiver ignores all text not sent to the main window, and ignores all styling even on that.

The significance of the 1 in the call i7_set_process_receiver(&proc, my_receiver, 1) is that it asked for text to be sent to the receiver encoded as UTF-8. This in fact is the default receiver's arrangement, too. Call i7_set_process_receiver(&proc, my_receiver, 0) to have the characters arrive raw as a series of unencoded Unicode code-points.

In Example 3, the Inform source is:

To begin:
    say "Hello & [italic type]welcome[roman type] from <Inform code>!"

The example receiver function converts this to HTML:

<html><body>
Hello &amp; <span class="italic">welcome</span> from &lt;Inform code&gt;!
</html></body>

The word "welcome" here was printed with the style "italic"; all else was plain.

This example just prints the result, of course, but a receiver function could equally opt to bottle up the text for use later on.

§9. Example 4: CSS Receiver. Example 4 builds on Example 3 but allows arbitrary named CSS styles to applied, i.e., it is not limited to bold and italic markup.

The C program from Example 3 is unchanged, but the Inform program does something more interesting. We're going to need to do something unusual here, and define two new phrases using inline definitions as raw Inter code, written in Inform 6 notation:

To style text with (T - text):
    (- style {T}; -).

The result of this will be something which Inform 6 would not compile, because in I6 the only legal uses of style are style bold and so on: there is no legal way for style to be followed by an arbitrary value, as it is here. But Inform 6 will not be compiling this: Inform will convert style V; directly to the Inter statement !style V, which will then be translated to C as the function call i7_styling(proc, 2, V) — and that function, in our library of C code supporting Inform, can handle any textual value of V.

With that bit of fancy footwork out of the way, we can make a pair of text substitutions [style NAME] and [end style], with the possible range of styles enumerated by name, like so:

A text style is a kind of value.
The text styles are decorative, calligraphic and enlarged.

To say style as (T - text style):
    style text with "[T]".

To say end style:
    (- style roman; -).

Finally, then, we can have:

To begin:
    say "[style as enlarged]Hello[end style] & [style as calligraphic]welcome[end style] from <Inform code>!"

And the result is:

<html><body>
<span class="enlarged">Hello</span> &amp; <span class="calligraphic">welcome</span> from &lt;Inform code&gt;!
</html></body>

§10. Example 5: Sender. It's now time to make a traditional piece of interactive fiction with Inform, with a world model, a command parser, and the standard command-and-response play loop. But we will call this from C, and decide the commands there.

This means a small change to the makefile, to remove -basic from the options for inform7, since we're no longer confining ourselves to Basic Inform.

The I7 source could be that for almost any game — any of the examples in the Inform documentation would do. The C source is more interesting here. Just as an i7_process can be given a "receiver" function, which receives text from it, it can also be given a "sender" which sends text to it. Thus:

    i7process_t proc = i7_new_process();
    i7_set_process_sender(&proc, the_quitter);

Here the_quitter is the following sender function:

char *the_quitter(int count) {
    char *cmd = "";
    switch (count) {
        case 0: cmd = "quit"; break;
        case 1: cmd = "y"; break;
    }
    printf("%s\n", cmd);
    return cmd;
}

This must return a C string (that is, a char * pointer to a null-terminated sequence of non-control ASCII characters) which represents a whole line of imaginary "typing" by the player. The argument count counts upwards from 0, incrementing after each call to the sender. So the effect of the_quitter is to simulate the effect of typing "quit" and then "y".

The resulting transcript of play looks like this:

Sir Arthur Evans, hero and archeologist, invites you to explore...

Welcome
An Interactive Fiction
Release 1 / Serial number 210917 / Inform 7 v10.1.0 / D

Jarn Mound

>quit
Are you sure you want to quit? y

Thus, the "y" is needed in order to respond to the followup question "Are you sure...?".

Note that the process is stalled while waiting for the sender to return a line of input: it's not working away in the background. The default sender uses getchar, from the C standard library, to receive a genuinely typed command terminated by a new-line.

§11. Example 6: Directing actions. More radically, it's possible to take textual input away entirely, by setting the sender function to NULL.

What will happen then is that the i7_run_process function will return at the point where a command would have been requested. The C function can then trigger whatever actions it wants, in the world model, without the need to feed a textual command back to I7 which would then be parsed into actions. In this example we do just that, and also examine and modify the data belonging to the I7 program from C.

The story, such as it is:

Jarn Mound is a room.

Age is a kind of value. The ages are modern, antique and ancient. A thing has
an age. The age of a thing is usually modern.

In the Mound is a Linear B tablet. The tablet is ancient. The player is wearing
a watch.

The meaning is a text that varies. The meaning is "4 oxen, 1 broken tripod table."

Instead of examining the watch:
    say "It is approximately [time of day]."

Report examining the tablet:
    say "It is [age of tablet], and translates to '[meaning]'."

When play begins:
    now the command prompt is "";
    say "Sir Arthur Evans, hero and archeologist, invites you to explore..."

In this example the C program is:

#include "inform7_clib.h"
#include "inform7_symbols.h"

int main(int argc, char **argv) {
    i7process_t proc = i7_new_process();
    i7_set_process_sender(&proc, NULL);
    if (i7_run_process(&proc) == 0) {
        i7word_t t = i7_read_variable(&proc, i7_V_the_time);
        printf("[C program reads 'time of day' as %d]\n", t);
        i7word_t A = i7_read_prop_value(&proc, i7_I_Linear_B_tablet, i7_P_age);
        printf("[C program reads 'age of Linear B tablet' as %d]\n", A);
        i7_try(&proc, i7_A_Take, i7_I_Linear_B_tablet, 0);
        i7_try(&proc, i7_A_Inv, 0, 0);
        i7_write_variable(&proc, i7_V_the_time, 985);
        i7_try(&proc, i7_A_Examine, i7_I_watch, 0);
        i7_write_variable(&proc, i7_V_the_time, 995);
        i7_try(&proc, i7_A_Examine, i7_I_watch, 0);
        i7_write_prop_value(&proc, i7_I_Linear_B_tablet, i7_P_age, i7_I_modern);
        i7_try(&proc, i7_A_Examine, i7_I_Linear_B_tablet, 0);
        return 0;
    } else {
        printf("*** Fatal error: halted ***\n");
        fflush(stdout); fflush(stderr);
        return 1;
    }
}

Note that a header file called inform7_symbols.h is included. This defines useful constants like i7_A_Take (for the "taking" action) and i7_I_Linear_B_tablet (for the object called "Linear B tablet"). But where did this header come from?

The answer is that this file can optionally be created when inform7 is generating C. In Example 5 we ran Inform with -format=C/no-main; this time we use -format=C/no-main/symbols-header. This also needs a subtle change to the makefile logic; the difference being that Eg6-C.o is now dependent on Eg6-I.c. This forces make to create Eg6-I.c, and as a byproduct inform7_symbols.h, before compiling Eg6-C.c to make Eg6-C.o.

Eg6: Eg6-C.o Eg6-I.o
    $(LINK) -o Eg6 Eg6-C.o Eg6-I.o

Eg6-C.o: Eg6.c Eg6-I.c
    $(CC) -o Eg6-C.o Eg6.c

Eg6-I.o: Eg6-I.c
    $(CC) -o Eg6-I.o Eg6-I.c

Eg6-I.c: Eg6.i7
    $(INFORM) -format=C/no-main/symbols-header -o Eg6-I.c Eg6.i7

Anyway, the result of this is:

Sir Arthur Evans, hero and archaeologist, invites you to explore...

Welcome
An Interactive Fiction
Release 1 / Serial number 210919 / Inform 7 v10.1.0 / D

Jarn Mound
You can see a Linear B tablet here.

[C program reads 'time of day' as 540]
[C program reads 'age of Linear B tablet' as 3]
Taken.

You are carrying:
  a Linear B tablet
  a watch (being worn)

It is approximately 4:25 pm.

It is approximately 4:35 pm.

You see nothing special about the Linear B tablet.

It is modern, and translates to "4 oxen, 1 broken tripod table.".

Here we see a run of responses, as if to unheard commands: those of course are the actions sent directly to the process by i7_try.

The data inside the Inform program is represented in C by values of the type i7word_t. (In practice, this is probably a 32-bit integer, but it's best to make no assumptions.) Inform at run-time is typeless, so it's up to the C programmer to know what is meant. For example, we know the result of the call i7_read_variable(&proc, i7_V_the_time) will be an integer from 0 to 1439, because the variable has kind "time" in Inform 7, and this is how Inform stores times of day (in minutes since midnight).

§12. Example 6 used most of the following suite of functions for looking at or altering the Inform data.

First, some functions which can only be applied to instances of object:

Second, functions to look at global variables:

Note that where variables are created by kits such as WorldModelKit, their ID constants have names based on the names they have in those kits: thus i7_V_the_time refers to the time of day, not i7_V_time_of_day. Browsing inform7_symbols.h will usually make things clear, anyway.

Finally, properties of objects can similarly be read or written:

If you need access to other data inside the Inform program, it's better to process it at the Inform end. These functions are just a convenience for what's most often needed.

§13. Example 7: Reading and writing lists and texts from C. Following on from Example 6, suppose we want to deal with Inform variables (or property values) containing texts, or lists — these, for instance:

The meaning is a text that varies. The meaning is "4 oxen, 1 broken tripod table."

The dial is a list of numbers that varies. The dial is { 3, 6, 9, 12 }.

If we call, say, i7_read_variable on i7_V_meaning then the return value is an i7word_t which is, in fact, an address somewhere in the Inform process memory. What are we to do with this? In fact, it's easy to convert to and from a C string:

char *M = i7_read_string(&proc, i7_read_variable(&proc, i7_V_meaning));
printf("[C program reads 'meaning' as %s]\n", M);
i7_try(&proc, i7_A_Examine, i7_I_Linear_B_tablet, 0);
i7_write_string(&proc, i7_read_variable(&proc, i7_V_meaning),
    "the goddess of the winds beckons you!");
i7_try(&proc, i7_A_Examine, i7_I_Linear_B_tablet, 0);

And this produces the following:

[C program reads 'meaning' as 4 oxen, 1 broken tripod table.]
It is ancient, and translates to "4 oxen, 1 broken tripod table.".

It is ancient, and translates to "the goddess of the winds beckons you!".

Here we repeat the action of examining the tablet: we examine once before changing the "meaning" and once after.

Similarly for lists:

int L = 0;
i7word_t *D = i7_read_list(&proc, i7_read_variable(&proc, i7_V_dial), &L);
printf("[C program reads 'dial' as");
for (int i=0; i<L; i++) printf(" %d", D[i]);
printf("]\n");
i7_try(&proc, i7_A_Examine, i7_I_watch, 0);
D[0] = 2;
D[1] = 10;
i7_write_list(&proc, i7_read_variable(&proc, i7_V_dial), D, 2);
i7_try(&proc, i7_A_Examine, i7_I_watch, 0);

which produces:

[C program reads 'dial' as 3 6 9 12]
It is approximately 9:00 am, according to a dial showing 3, 6, 9 and 12.

It is approximately 9:00 am, according to a dial showing 2 and 10.

§14. The following functions are available:

§15. Example 8: Direct function calls between C and Inform programs. This final example shows how the C and Inform programs can call each other's functions, albeit with some minor restrictions.

First, we have a C function called collatz, and we want to call it from Inform. This function must have a prototype like so:

i7word_t collatz(i7process_t *proc, i7word_t x) {
    ...
}

In particular, it must receive a single value (other than the process) and return a single value.

At the Inform side, we need to give this a wrapper as a phrase. Say, this:

To call C collatz function with (N - a number):
    (- external__collatz({N}); -).

Note that we've written that definition inline, and that the function is here called external__collatz. (The prefix external__ signals that the function won't be found in the Inter code generated by Inform; without that, Inform would throw a problem message.) With that done, the Inform code:

    call C collatz function with 17;

will perform collatz(proc, 17), as expected.

§16. Now we want to do this the other way round: have C make a function call into Inform. The function to call this time is the one implied by this phrase declaration:

To run the Collatz algorithm on (N - a number):
    ...

And the C needed to call this is simple enough:

    i7_F_run_the_collatz_algorithm_on_X(17);

i7_F_run_the_collatz_algorithm_on_X is in fact a defined name: see the include file inform7_symbols.h for a list of the functions available for calling like this. (Phrases defined inline cannot be called, because they are not functions.)

The Inform phrase used can also return a value ("To decide what..."), and can have any number of arguments, so this is less restrictive than the first way round.

§17. Example 8 shows both of these mechanisms working, with control tossed back and forth between the two programs. They are pretending to be two people, Mr C and Mrs I, playing a simple number game: the winner is the one who reaches 1.

MR C: My friend Mrs I calls my attention to 17, so I triple and add one.
MRS I: I have heard Mr C tell that 52 is an interesting number. And so I halve.
MR C: My friend Mrs I calls my attention to 26, so I divide by two.
MRS I: I have heard Mr C tell that 13 is an interesting number. And so I treble and add 1.
MR C: My friend Mrs I calls my attention to 40, so I divide by two.
MRS I: I have heard Mr C tell that 20 is an interesting number. And so I halve.
MR C: My friend Mrs I calls my attention to 10, so I divide by two.
MRS I: I have heard Mr C tell that 5 is an interesting number. And so I treble and add 1.
MR C: My friend Mrs I calls my attention to 16, so I divide by two.
MRS I: I have heard Mr C tell that 8 is an interesting number. And so I halve.
MR C: My friend Mrs I calls my attention to 4, so I divide by two.
MRS I: I have heard Mr C tell that 2 is an interesting number. And so I halve.
MR C: My friend Mrs I calls my attention to 1, so I win! Haha!

Here the function calls reached 11 deep: Inform called C which called Inform which called C which... and so on. But everything then terminated cleanly.