Embed Perl 5.38 in a C Program Without Guessing the Linker Flags
You will build a small C executable that starts an embedded Perl interpreter and passes it the same command-line code that a normal perl process would run. The example follows the lifecycle documented by perlembed(1): initialise Perl's runtime, allocate and construct an interpreter, parse and run the input, then destruct and free it.
The route
Jump straight to the step you need, or tick off Done means at the end.
Allow 20 to 30 minutes. You need Perl 5.38, a C compiler and the development files that provide the Perl library. The local reference system has Perl and perl-doc version 5.38.2-3.2ubuntu0.6, but not libperl-dev. That missing package is why the final link command cannot complete there until the development package is installed.
Checkpoint
This guide only builds and runs a local test program. It does not install a module, change Perl, run as root or execute a downloaded script.
1. Check the Perl build you will link against
Embedding is tied to the Perl build on the machine. Do not copy a compiler command from another host and assume its include or library paths still apply. First record the version and the compiler options produced by ExtUtils::Embed:
$ perl -v | sed -n '1,2p'
This is perl 5, version 38, subversion 2 (v5.38.2)
$ perl -MExtUtils::Embed -e ccopts
-D_REENTRANT ... -I/usr/lib/x86_64-linux-gnu/perl/5.38/CORE
$ perl -MExtUtils::Embed -e ldopts
-L/usr/local/lib ... -L/usr/lib/x86_64-linux-gnu/perl/5.38/CORE -lperl ...
The complete output is host-specific. The important point is that ccopts supplies the directory containing EXTERN.h and perl.h, while ldopts supplies the Perl library and its additional libraries. Using these outputs keeps the command aligned with this Perl installation.
On Debian and Ubuntu, the runtime can exist without the development linker name. Check before writing code:
$ dpkg-query -W -f='${Package} ${Version}\n' perl perl-doc libperl-dev
perl 5.38.2-3.2ubuntu0.6
perl-doc 5.38.2-3.2ubuntu0.6
dpkg-query: no path found matching pattern /usr/share/doc/libperl-dev
If libperl-dev is absent, stop here or install it through your normal package-management process. That is an elevated, system-wide change, so review the package transaction before accepting it. Do not work around a missing development package by copying a random libperl file into /usr/local/lib.
2. Create the smallest useful host
Save this as interp.c in a scratch directory. The argv passed to perl_parse is the C program's original argument list, so interp -e '...' behaves like perl -e '...'.
#include <EXTERN.h>
#include <perl.h>
#include <stdlib.h>
int main(int argc, char **argv, char **env)
{
PerlInterpreter *my_perl;
PERL_SYS_INIT3(&argc, &argv, &env);
my_perl = perl_alloc();
if (my_perl == NULL) {
PERL_SYS_TERM();
return EXIT_FAILURE;
}
perl_construct(my_perl);
PL_exit_flags |= PERL_EXIT_DESTRUCT_END;
perl_parse(my_perl, NULL, argc, argv, NULL);
perl_run(my_perl);
perl_destruct(my_perl);
perl_free(my_perl);
PERL_SYS_TERM();
return EXIT_SUCCESS;
}
PERL_SYS_INIT3 and PERL_SYS_TERM belong around the lifetime of all interpreters in the process. Call the first once before creating one, and the second once after freeing the last one. The PERL_EXIT_DESTRUCT_END flag also makes END blocks run during destruction, which matters when the host does not use the normal Perl executable.
The example is deliberately small, but production code should check the return values from perl_alloc, perl_parse and perl_run, report failures, and still perform the matching cleanup. Never skip perl_destruct and perl_free on an error path.
3. Compile with ExtUtils::Embed
Run the documented shape of the command from the directory containing interp.c:
$ cc -o interp interp.c $(perl -MExtUtils::Embed -e ccopts -e ldopts)
If the linker says it cannot find -lperl, that is a development-file or library-path problem, not a C syntax problem. Recheck the two option commands, confirm that libperl-dev is installed, and ensure the compiler is the one reported by perl -MConfig -e 'print $Config{cc}'. If the headers cannot be found, inspect the -I path. If the Perl library cannot be found, inspect the -L path.
Checkpoint
A successful compile creates interp and prints no normal output. Confirm the linked dependency before running code:
$ file interp
interp: ELF 64-bit LSB pie executable, x86-64, ...
$ ldd interp | grep -E 'libperl|not found'
libperl.so.5.38 => /usr/lib/x86_64-linux-gnu/libperl.so.5.38.2 (...)
The exact file and ldd wording varies. Any not found entry must be fixed before deployment. Do not suppress it with a global loader-path change unless you understand the service's runtime environment.
4. Run a harmless embedded statement
Use a constant expression first. This confirms that argument parsing, the interpreter lifecycle and standard output all work:
$ ./interp -e 'print "embedded-ok\\n"'
embedded-ok
Then verify the exit status immediately:
$ printf 'status: %s\n' "$?"
status: 0
That status belongs to ./interp because printf is the next command. If you run another command first, its status is what $? reports. You can also pass a file name in the argument list and let perl_run execute it, but keep the file under your control while testing.
5. Add a Perl subroutine only when you need one
For a host that calls named Perl routines, use the Perl API documented by perlcall(1). A simple pattern is to pass a Perl file as the host's arguments, call perl_parse, then invoke a known routine with call_argv. The routine name and arguments should be fixed by your application, not assembled from request data.
For one-off expressions, eval_pv and eval_sv are the API described by perlembed(1). Treat the string passed to either function as executable code. If it contains user input, an attacker may be able to run arbitrary Perl with the privileges of the host process. Validate data as data, keep code and values separate, and do not expose an unrestricted embedded interpreter in a network-facing service.
6. Keep long-running interpreters bounded
A persistent interpreter avoids loading Perl repeatedly, but its packages, symbols and cached state can grow as more code is loaded. The manpage warns that repeatedly evaluating arbitrary files can grow the process and create namespace collisions. Prefer lexical variables, known packages and a bounded request model. For untrusted or independently deployed code, a separate process with an explicit privilege boundary is easier to reason about than a shared interpreter.
Multiple interpreters need more care. Perl must be built with multiplicity or the appropriate thread options, and API calls must select the current interpreter with PERL_SET_CONTEXT. Do not turn a single-interpreter example into concurrent execution by adding threads around it.
Done means
- Perl version, compiler flags and linker flags came from the same local installation.
- The development package and Perl library are present before compiling.
- The host performs the complete initialise, construct, parse, run, destruct and free lifecycle.
./interp -e 'print ...'prints the expected text and returns status 0.- Linker diagnostics and
lddoutput contain no unresolved library. - Embedded code is trusted, bounded and never built by concatenating untrusted input.