PyAPI: remove use of stub script to execute files on WIN32

Workaround potential C-API `FILE` incompatibility by reading the
file data into memory, compiling & running it - matching existing logic
for text buffers text buffers. This replaces the in-lined stub-script
that re-opened the file from Python.

While the down-side of the stub-script was minor, it required some
non-obvious logic and had the disadvantage of requiring 2x scripts to
execute whenever a file was executed on WIN32.

Expose BLI_file_read_data_as_mem_from_handle as a public function
since it's useful to be able to read an existing FILE into memory.
This commit is contained in:
Campbell Barton
2023-11-28 17:45:25 +11:00
parent 48bc74953c
commit 41739cd3fd
3 changed files with 76 additions and 30 deletions

View File

@@ -352,6 +352,17 @@ bool BLI_file_older(const char *file1, const char *file2) ATTR_WARN_UNUSED_RESUL
* \return the lines in a linked list (an empty list when file reading fails).
*/
struct LinkNode *BLI_file_read_as_lines(const char *file) ATTR_WARN_UNUSED_RESULT ATTR_NONNULL();
/**
* Read the contents of `fp`, returning the result as a buffer or null when it can't be read.
*
* \param r_size: The size of the file contents read into the buffer (excluding `pad_bytes`).
*/
void *BLI_file_read_data_as_mem_from_handle(FILE *fp,
bool read_size_exact,
size_t pad_bytes,
size_t *r_size);
void *BLI_file_read_text_as_mem(const char *filepath, size_t pad_bytes, size_t *r_size);
/**
* Return the text file data with:
@@ -368,6 +379,7 @@ void *BLI_file_read_text_as_mem(const char *filepath, size_t pad_bytes, size_t *
* \param pad_bytes: When this is non-zero, the first byte is set to nil,
* to simplify parsing the file.
* It's recommended to pass in 1, so all text is nil terminated.
* \param r_size: The size of the file contents read into the buffer (excluding `pad_bytes`).
*
* Example looping over lines:
*

View File

@@ -444,10 +444,10 @@ bool BLI_is_file(const char *path)
/**
* Use for both text and binary file reading.
*/
static void *file_read_data_as_mem_impl(FILE *fp,
bool read_size_exact,
size_t pad_bytes,
size_t *r_size)
void *BLI_file_read_data_as_mem_from_handle(FILE *fp,
bool read_size_exact,
size_t pad_bytes,
size_t *r_size)
{
BLI_stat_t st;
if (BLI_fstat(fileno(fp), &st) == -1) {
@@ -504,7 +504,7 @@ void *BLI_file_read_text_as_mem(const char *filepath, size_t pad_bytes, size_t *
FILE *fp = BLI_fopen(filepath, "r");
void *mem = nullptr;
if (fp) {
mem = file_read_data_as_mem_impl(fp, false, pad_bytes, r_size);
mem = BLI_file_read_data_as_mem_from_handle(fp, false, pad_bytes, r_size);
fclose(fp);
}
return mem;
@@ -515,7 +515,7 @@ void *BLI_file_read_binary_as_mem(const char *filepath, size_t pad_bytes, size_t
FILE *fp = BLI_fopen(filepath, "rb");
void *mem = nullptr;
if (fp) {
mem = file_read_data_as_mem_impl(fp, true, pad_bytes, r_size);
mem = BLI_file_read_data_as_mem_from_handle(fp, true, pad_bytes, r_size);
fclose(fp);
}
return mem;

View File

@@ -79,6 +79,61 @@ struct PyModuleObject {
};
#endif
/**
* Compatibility wrapper for #PyRun_FileExFlags.
*/
static PyObject *python_compat_wrapper_PyRun_FileExFlags(FILE *fp,
const char *filepath,
const int start,
PyObject *globals,
PyObject *locals,
const int closeit,
PyCompilerFlags *flags)
{
/* Previously we used #PyRun_File to run directly the code on a FILE
* object, but as written in the Python/C API Ref Manual, chapter 2,
* 'FILE structs for different C libraries can be different and incompatible'.
* So now we load the script file data to a buffer on MS-Windows. */
#ifdef _WIN32
bool use_file_handle_workaround = true;
#else
bool use_file_handle_workaround = false;
#endif
if (!use_file_handle_workaround) {
return PyRun_FileExFlags(fp, filepath, start, globals, locals, closeit, flags);
}
PyObject *py_result = nullptr;
size_t buf_len;
char *buf = static_cast<char *>(BLI_file_read_data_as_mem_from_handle(fp, false, 1, &buf_len));
if (closeit) {
fclose(fp);
}
if (UNLIKELY(buf == nullptr)) {
PyErr_Format(PyExc_IOError, "Python file \"%s\" could not read buffer", filepath);
}
else {
buf[buf_len] = '\0';
PyObject *filepath_py = PyC_UnicodeFromBytes(filepath);
PyObject *compiled = Py_CompileStringObject(buf, filepath_py, Py_file_input, flags, -1);
MEM_freeN(buf);
Py_DECREF(filepath_py);
if (compiled == nullptr) {
if (!PyErr_Occurred()) {
PyErr_Format(PyExc_SyntaxError, "Python file \"%s\" could not be compiled", filepath);
}
}
else {
py_result = PyEval_EvalCode(compiled, globals, locals);
Py_DECREF(compiled);
}
}
return py_result;
}
/**
* Execute a file-path or text-block.
*
@@ -147,30 +202,9 @@ static bool python_script_exec(
py_result = nullptr;
}
else {
#ifdef _WIN32
/* Previously we used PyRun_File to run directly the code on a FILE
* object, but as written in the Python/C API Ref Manual, chapter 2,
* 'FILE structs for different C libraries can be different and
* incompatible'.
* So now we load the script file data to a buffer.
*
* Note on use of 'globals()', it's important not copy the dictionary because
* tools may inspect 'sys.modules["__main__"]' for variables defined in the code
* where using a copy of 'globals()' causes code execution
* to leave the main namespace untouched. see: #51444
*
* This leaves us with the problem of variables being included,
* currently this is worked around using 'dict.__del__' it's ugly but works.
*/
{
const char *pystring =
"with open(__file__, 'rb') as f:"
"exec(compile(f.read(), __file__, 'exec'), globals().__delitem__('f') or globals())";
py_result = PyRun_String(pystring, Py_file_input, py_dict, py_dict);
}
#else
py_result = PyRun_File(fp, filepath, Py_file_input, py_dict, py_dict);
#endif
py_dict = PyC_DefaultNameSpace(filepath);
py_result = python_compat_wrapper_PyRun_FileExFlags(
fp, filepath, Py_file_input, py_dict, py_dict, 0, nullptr);
}
fclose(fp);
}