This implementation of a C-preprocessor based on ISO/IEC 9899:2024 - N3220.
You can also use a PHP C parser to subsequently parse the source code into an AST after preprocessing.
- PHP ^8.1
Library is available as a Composer repository and can be installed using the following command in the root of your project.
$ composer require ffi/preprocessorPreprocessor::process() expands the macros and executes the directives of a C source,
the result is a stringable Result.
$pre = new FFI\Preprocessor\Preprocessor();
echo $pre->process(<<<'C'
#define SQUARE(x) ((x) * (x))
int area = SQUARE(4);
C);
// int area = ((4) * (4));Method process() accepts:
SplFileInfo- filestring- the source textresource- streamPhplrt\Contracts\Source\ReadableInterface- any other source object
Besides the output, the result tells what the run has learned. Each getter reads the output to the end first, and the result can be read more than once.
$result = $pre->process("#define A 1\n#warning careful\n");
$result->hasMacro('A'); // true, the macros as they are when the run ends
$result->findMacro('A'); // MacroInterface|null
$result->getMacros(); // iterable<MacroInterface>
$result->getFiles(); // the first file and the included ones, each once
$result->getDiagnostics(); // the messages of #error and #warning
echo $result; // preprocessed resultThe $pre->macros holds the macros every run starts with.
$pre->macros->define('DEBUG'); // #define DEBUG 1
$pre->macros->define('LEVEL', '3'); // #define LEVEL 3
$pre->macros->remove('DEBUG'); // #undef DEBUG
// A closure with no parameters is an object-like macro, evaluated at each replacement
$pre->macros->define('REQUEST_ID', fn() => uniqid());
// A closure with parameters is a function-like macro, it gets the arguments as written
$pre->macros->define('MAX', fn(string $a, string $b): string
=> "(($a) > ($b) ? ($a) : ($b))"
);The
__DATE__and__TIME__are taken from a PSR-20 clock, the system one by default.$pre = new Preprocessor(clock: $myClock);
Any #include and #embed search for files in the file system of the preprocessor,
$pre->files.
$pre = new FFI\Preprocessor\Preprocessor();
// Define a file by its contents
$pre->files->add('config.h', '#define VERSION 2');
$pre->files->add('example.h', new SplFileInfo('/path/to/file.h'));
$pre->files->add('some.h', fopen('/path/to/stream.h'));
// Make a real file visible under a virtual name
$pre->files->link('gl.h', '/usr/include/GL/gl.h');
// Mount a real directory, `#include <sys/types.h>` is searched in it
$pre->files->mount('/usr/include');
// Mount it under a virtual directory, `#include <sdk/a.h>`
$pre->files->mount('/opt/sdk/include', 'sdk');Ane #error and #warning are reported to a listener. By default, an error
stops the run with DiagnosticException and a warning is skipped.
use FFI\Preprocessor\Diagnostic\DiagnosticListener\StreamDiagnosticListener;
$pre = new Preprocessor(diagnostic: new StreamDiagnosticListener(STDERR));
// {entrypoint}:1:1: warning: #warning carefulThe ready listeners are:
ThrowingDiagnosticListener- throws errorsStreamDiagnosticListener- writes diagnostic into the streamLoggerDiagnosticListener- A PSR-3 loggerNullDiagnosticListener- does nothingCompoundListener- combines several into one
Every directive has a handler, $pre->directives holds them. Add a
directive of your own or replace a standard one.
use FFI\Preprocessor\Directive\DirectiveContext;
use FFI\Preprocessor\Directive\DirectiveHandlerInterface;
use FFI\Preprocessor\Runtime\Chunk\TaggedChunk\DirectiveChunk;
use FFI\Preprocessor\Runtime\Chunk\TaggedChunk\GeneratedChunk;
$pre->directives->add('note', new class implements DirectiveHandlerInterface {
public function handle(DirectiveChunk $directive, DirectiveContext $context): iterable
{
yield new GeneratedChunk("/* {$directive->body} */\n", $directive->source, 0);
}
});
// #note hi => /* hi */The context gives the handler the macros, the file system, the diagnostic listener and the executor that preprocesses an included file.
#pragma once is executed, any other pragma is passed to the output. A pragma
is executed by a processor, add one to react to a pragma of your own.
use FFI\Preprocessor\Directive\Handler\PragmaDirectiveHandler;
use FFI\Preprocessor\Directive\Handler\PragmaDirectiveHandler\OncePragmaProcessor;
use FFI\Preprocessor\Directive\Handler\PragmaDirectiveHandler\PragmaProcessorInterface;
final class PackPragmaProcessor implements PragmaProcessorInterface
{
public function supports(string $body): bool
{
return \str_starts_with($body, 'pack');
}
public function apply(DirectiveChunk $directive, DirectiveContext $context): iterable
{
return []; // `#pragma pack(...)` leaves nothing in the output
}
}
$pre->directives->replace('pragma', new PragmaDirectiveHandler([
new OncePragmaProcessor(),
new PackPragmaProcessor(),
]));List of preprocessor limitations. May be added to in the future.
#lineis not supported,__FILE__and__LINE__always tell the real file and line.#embedhas no parameters (limit,prefix,suffix,if_empty), and__has_embedis not supported.#pragma oncerecognizes a file by its pathname, the same file included by different paths is not detected.
_Pragmaoperator is not supported.__VA_OPT__is not supported.__COUNTER__is not defined.__DATE__and__TIME__are taken from the clock at each replacement, not once per run.