Currently I am developing an ASP.NET application that for legacy reasons needs to execute some Perl scripts. For this I wrote a small C++ library that uses the embedded Perl API. This library has one single (C) entry point that allows the C# code to execute a script while passing command line arguments and an environment. This environment allows the C# code to mimic a CGI call for the Perl script.
Now I am seeing something strange. The C# application allows for (a maximum number of) concurrent Perl script to be executed. For each call a different environment is created and this is passed to my C++ library that then passes this environment to the perl_parse
function. What I then notice is that the actual environment seen by the Perl script does not match the actual environment passed, but seems to be an old copy. So my question is, am I overlooking something? Is there a special way to run multiple embedded Perl interpreters concurrently? Also when I limit the number of concurrent threads to 1, the issue is still there.
The relevant parts of the C++ library are as follows:
extern "C" __declspec(dllexport) BOOL ExecutePerlScript(PCSTR* environmentVariables,
PCSTR path)
{
BOOL result(FALSE);
// Create the Perl interpreter
PerlInterpreter* my_perl(perl_alloc());
if (NULL != my_perl)
{
PERL_SET_CONTEXT(my_perl);
PL_perl_destruct_level = 1;
perl_construct(my_perl);
PL_origalen = 1;
PL_exit_flags |= PERL_EXIT_DESTRUCT_END;
// Initialize the Perl interpreter
result = (perl_parse(my_perl,
XsInit,
NR_DEFAULT_ARGUMENTS,
DEFAULT_ARGUMENTS,
const_cast<char**>(environmentVariables)) == 0) ? TRUE : FALSE;
// Run the interpreter
if (result)
{
result = (perl_run(my_perl) == 0) ? TRUE : FALSE;
}
if (result)
{
result = LoadFile(path,
my_perl);
}
if (result)
{
// Execute the Perl script
eval_pv("eval \"$" SCRIPT_TO_EVALUATE_VARIABLE_NAME "; 1\" or do { $" SCRIPT_EXECUTION_ERROR_VARIABLE_NAME " = $@; }",
TRUE);
}
// Destruct the interpreter
PL_perl_destruct_level = 1;
perl_destruct(my_perl);
perl_free(my_perl);
}
return result;
}
extern "C" BOOL WINAPI DllMain(HINSTANCE hinstDLL,
DWORD fdwReason,
LPVOID lpvReserved)
{
BOOL result(FALSE);
switch (fdwReason)
{
case DLL_PROCESS_ATTACH:
if (0 == g_initCount)
{
PERL_SYS_INIT3(0,
NULL,
NULL);
}
g_initCount++;
result = TRUE;
break;
case DLL_PROCESS_DETACH:
if (g_initCount > 0)
{
g_initCount--;
if (0 == g_initCount)
{
PERL_SYS_TERM();
}
}
result = TRUE;
break;
}
return result;
}
The format of environmentVariables
in the above snippet is an array of char*
where each element is in the form <variable name>=<variable value>
and the last element of the array is NULL
.
The Perl script I run is as follows:
use strict;
use CGI qw/:standard/;
print "---- ENVIRONMENT ----\n";
for my $env (sort keys %ENV)
{
print "$env = $ENV{$env}\n";
}
print "\n";
For example, one of the executions (in a loop) passes the following environment to the C++ function:
- AUTH_TYPE =
- CONTENT_LENGTH = 47
- CONTENT_TYPE = application/x-www-form-urlencoded
- GATEWAY_INTERFACE = CGI/1.1
- PATH_INFO = /test.pl
- PATH_TRANSLATED = E:\Perl\PerlTestApplication\test.pl
- QUERY_STRING = lang=nl
- REMOTE_ADDR = 1.2.3.4
- REMOTE_HOST = remote.host
- REMOTE_USER =
- REQUEST_METHOD = POST
- SCRIPT_NAME = /test.pl
- SERVER_NAME = example.domain
- SERVER_PORT = 443
- SERVER_PROTOCOL = HTTP/1.1
- SERVER_SOFTWARE = Microsoft-IIS/10.0
and then the script prints the following environment:
---- ENVIRONMENT ----
AUTH_TYPE =
CONTENT_LENGTH = 45
CONTENT_TYPE = application/x-www-form-urlencoded
GATEWAY_INTERFACE = CGI/1.1
PATH_INFO = /test.pl
PATH_TRANSLATED = E:\Perl\PerlTestApplication\test.pl
QUERY_STRING = lang=nl
REMOTE_ADDR = 1.2.3.4
REMOTE_HOST = remote.host
REMOTE_USER =
REQUEST_METHOD = POST
SCRIPT_NAME = /test.pl
SERVER_NAME = example.domain
SERVER_PORT = 443
SERVER_PROTOCOL = HTTP/1.1
SERVER_SOFTWARE = Microsoft-IIS/10.0
As can be seen the value of the CONTENT_LENGTH
variable is different and in the Perl environment is the same as an environment passed earlier to the script. So somehow, the environment that I pass to the new Perl interpreter instance is not cleaned up and another environment is still used. I already use the PERL_SET_CONTEXT
to set the context in the current thread right after construction, but that just doesn't seem to be enough.
I have tried this both on an Active Perl installation of Perl 5.24 and on a Strawberry Perl installation of Perl 5.30, but both give the same erroneous result.
What am I doing wrong?
In the source code for Perl, specifically perl_parse
the env
parameter isn't used. So I'm guessing it's reusing the current processes environment? To get around this, you could try something like this:
HV* envHV = get_hv("main::ENV", 0);
if (envHV) {
auto envPtr = const_cast<char**>(environmentVariables);
for (int i = 0; envPtr[i]; i++) {
std::string str(envPtr[i]);
std::string key = str.substr(0, str.find("="));
str.erase(0, str.find("=") + 1);
SV* nsv = newSVpvn(str.c_str(), str.length());
hv_store(envHV, key.c_str(), key.length(), nsv, 0); // let hv_store hash for us
}
}
eval_pv("my $ eval \"$" SCRIPT_TO_EVALUATE_VARIABLE_NAME "; 1\" or do { $" SCRIPT_EXECUTION_ERROR_VARIABLE_NAME " = $@; }", TRUE);
Here we get the %ENV
hash, parse out the environmentVariables
array, and insert them into the hash. Then, we execute the script as usual. Note: This does make the assumption that environmentVariables
ends with a nullptr
, so adjust as needed.
Another option is to store these variables in another HV
, think something like main::CGI_ENV
if you don't want to overwrite existing values in the current environment. Then you'd just do $CGI_ENV{key}
.