Home / Alt manpages / perlembed(1)

  • perlembed(1)
  • User command
  • linux

Embed Perl 5.38 in C Without Guessing Linker Flags

Half the pain of embedding Perl in a C program is the link line, and perlembed(1) exists precisely so you never have to guess it. You will build a small C executable that starts an embedded Perl interpreter and passes it the same command-line code a normal perl process would run: initialise the runtime, allocate and construct an interpreter, parse and run the input, then destruct and free it.

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.

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 ...
  • ccopts supplies the include path. It points to the directory containing EXTERN.h and perl.h, and the exact output is host-specific.
  • ldopts supplies the library. It hands you the Perl library and its additional dependencies. Using both outputs keeps the command aligned with this exact 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 make sure 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 instead. 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.

Warning

For one-off expressions, eval_pv and eval_sv are the API described by perlembed(1), and the string passed to either function is 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

  • Flags came from this host. Perl version, compiler flags and linker flags came from the same local installation.
  • Dependencies present. The development package and Perl library are installed before compiling.
  • Full lifecycle used. The host performs the complete initialise, construct, parse, run, destruct and free sequence.
  • Statement runs clean. ./interp -e 'print ...' prints the expected text and returns status 0.
  • No unresolved libraries. Linker diagnostics and ldd output contain no unresolved library.
  • Embedded code stays trusted. It is bounded and never built by concatenating untrusted input.