Compatibility fixes for mjpython.

- Find Python functions in the interpreter binary itself rather than trying to find libpython.dylib, because this doesn't exist when using Conda. (https://github.com/conda-forge/python-feedstock/issues/595#issuecomment-1311275470)

- Increase the stack size of both the Python main thread and the OS main thread to 16MiB, to match what Python normally uses on macOS. (https://github.com/python/cpython/blob/3.11/configure#L11038)

- Check whether dlfcn and pthread function calls actually succeeds. Emit error messages and exit on failure rather than continuing the program (which generally leads to segfaults).

- Fix pixelated shadow in the icon.

PiperOrigin-RevId: 517390522
Change-Id: Ib91679b4baa63bac5f6b5893e634712d72161ea5
This commit is contained in:
Saran Tunyasuvunakool
2023-03-17 05:36:58 -07:00
committed by Copybara-Service
parent 08027b4e43
commit 0c4e191dd6
3 changed files with 97 additions and 28 deletions
Binary file not shown.
+70 -18
View File
@@ -12,30 +12,42 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
#import <algorithm>
#import <atomic> #import <atomic>
#import <cstdint>
#import <cstdlib> #import <cstdlib>
#import <cstring>
#import <iostream> #import <iostream>
#import <vector>
#import <dlfcn.h> #import <dlfcn.h>
#import <mach-o/dyld.h>
#import <pthread.h> #import <pthread.h>
#import <sys/resource.h> #import <sys/resource.h>
#import <unistd.h>
#import <Cocoa/Cocoa.h> #import <Cocoa/Cocoa.h>
#import <Python.h> #import <Python.h>
extern "C" {
extern char **environ; // for execve
// Wrap Objective-C Cocoa calls into C-style functions with default visibility, // Wrap Objective-C Cocoa calls into C-style functions with default visibility,
// so that we can dlsym and call them from Python via ctypes. // so that we can dlsym and call them from Python via ctypes.
extern "C" {
__attribute__((used)) void mjpython_hide_dock_icon() { __attribute__((used)) void mjpython_hide_dock_icon() {
[NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory]; [NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory];
} }
__attribute__((used)) void mjpython_show_dock_icon() { __attribute__((used)) void mjpython_show_dock_icon() {
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
} }
} } // extern "C"
// TODO(b/273744079): Remove Python 3.7 code after end-of-life (27 Jun 2023). // TODO(b/273744079): Remove Python 3.7 code after end-of-life (27 Jun 2023).
namespace { namespace {
// 16MiB is the default Python thread stack size on macOS as of Python 3.11
// https://bugs.python.org/issue18075
constexpr rlim_t kThreadStackSize = 0x1000000;
struct { struct {
#define CPYTHON_FN(fname) decltype(&::fname) fname #define CPYTHON_FN(fname) decltype(&::fname) fname
@@ -198,14 +210,51 @@ int main(int argc, char** argv) {
return 1; return 1;
} }
// Enlarge the stack if necessary to match what Python normally expects to have when launching
// a new thread.
rlimit stack;
if (getrlimit(RLIMIT_STACK, &stack)) {
std::cerr << "getrlimit failed to query stack size with error code " << errno << " ("
<< std::strerror(errno) << ")\n";
std::cerr << "continuing anyway but crashes may occur if the stack is too small\n";
} else if (stack.rlim_cur < kThreadStackSize && stack.rlim_cur < stack.rlim_max) {
auto rlim_old = stack.rlim_cur;
stack.rlim_cur = std::min(kThreadStackSize, stack.rlim_max);
if (setrlimit(RLIMIT_STACK, &stack)) {
std::cerr << "setrlimit failed to increase stack size with error code " << errno << " ("
<< std::strerror(errno) << ")\n";
std::cerr << "continuing anyway with stack size " << rlim_old << " but crashes may occur\n";
} else {
// re-exec the binary so that the new stack size takes effect
std::uint32_t path_size = 0;
_NSGetExecutablePath(nullptr, &path_size);
std::vector<char> path(path_size);
if (_NSGetExecutablePath(path.data(), &path_size)) {
std::cerr << "unexpected error from _NSGetExecutablePath, continuing anyway\n";
} else {
execve(path.data(), argv, environ);
}
}
}
// Resolve libpython at runtime to prevent linking against the wrong dylib. The correct libpython // Resolve libpython at runtime to prevent linking against the wrong dylib. The correct libpython
// path is passed from a Python trampoline script, which ran inside the desired interpreter and // path is passed from a Python trampoline script, which ran inside the desired interpreter and
// exec'd this binary. // exec'd this binary.
void* libpython = dlopen(libpython_path, RTLD_NOW | RTLD_GLOBAL); void* libpython = dlopen(libpython_path, RTLD_NOW | RTLD_GLOBAL);
if (!libpython) {
std::cerr << "failed to dlopen path '" << libpython_path << "': " << dlerror() << "\n";
return 1;
}
// Look up required CPython API functions from table of symbols already loaded into the process. // Look up required CPython API functions from table of symbols already loaded into the process.
#define CPYTHON_INITFN(fname) \ #define CPYTHON_INITFN(fname) \
cpython.fname = reinterpret_cast<decltype(cpython.fname)>(dlsym(libpython, #fname)) { \
cpython.fname = reinterpret_cast<decltype(cpython.fname)>(dlsym(libpython, #fname)); \
if (!cpython.fname) { \
std::cerr << "failed to dlsym '" << #fname << "': " << dlerror() << "\n"; \
return 1; \
} \
}
#if PY_MINOR_VERSION >= 8 #if PY_MINOR_VERSION >= 8
CPYTHON_INITFN(Py_InitializeFromConfig); CPYTHON_INITFN(Py_InitializeFromConfig);
@@ -233,21 +282,24 @@ int main(int argc, char** argv) {
// Package up argc and argv together to pass to pthread_create. // Package up argc and argv together to pass to pthread_create.
Args args{argc, argv}; Args args{argc, argv};
#define PTHREAD_CHECKED(func, ...) \
{ \
int result = func(__VA_ARGS__); \
if (result) { \
std::cerr << #func << " failed with " << result << "(" << std::strerror(result) << ")\n"; \
} \
}
// Configure the new thread with the correct stack size;
pthread_attr_t pthread_attr;
PTHREAD_CHECKED(pthread_attr_init, &pthread_attr);
PTHREAD_CHECKED(pthread_attr_setstacksize, &pthread_attr, stack.rlim_cur);
// Create a thread to be used as the "Python main thread". // Create a thread to be used as the "Python main thread".
pthread_t pymain_thread = [&args]() { pthread_t pymain_thread;
// Set the stack size of the Python main thread to be the same as the OS main thread. PTHREAD_CHECKED(pthread_create, &pymain_thread, &pthread_attr, &mjpython_pymain, &args);
// (e.g. the default pthread stack size is too small to import NumPy) pthread_attr_destroy(&pthread_attr);
rlimit limit; #undef PTHREAD_CHECKED
getrlimit(RLIMIT_STACK, &limit);
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setstacksize(&attr, limit.rlim_cur);
pthread_t thread;
pthread_create(&thread, &attr, &mjpython_pymain, &args);
return thread;
}();
// Busy-wait until Python interpreter is initialized. // Busy-wait until Python interpreter is initialized.
while (!py_initialized.load()) {} while (!py_initialized.load()) {}
+27 -10
View File
@@ -21,23 +21,40 @@ GUI calls without blocking the user's Python script. In other words, Python's
idea of the "main thread" is different from the thread that holds the idea of the "main thread" is different from the thread that holds the
com.apple.main-thread DispatchQueue. com.apple.main-thread DispatchQueue.
""" """
import ctypes
import importlib.util import importlib.util
import os import os
import platform
import sys import sys
import sysconfig
if platform.system() != 'Darwin':
raise RuntimeError('This script only works on macOS')
_NSGetExecutablePath = getattr(ctypes.CDLL(None), '_NSGetExecutablePath')
def get_executable_path():
c_path_size = ctypes.c_int32(0)
_NSGetExecutablePath(None, ctypes.byref(c_path_size))
c_path = (ctypes.c_char * c_path_size.value)()
_NSGetExecutablePath(ctypes.byref(c_path), ctypes.byref(c_path_size))
return c_path.value.decode()
def main(argv): def main(argv):
os.environ['MJPYTHON_LIBPYTHON'] = os.path.join( module_dir = os.path.dirname(importlib.util.find_spec('mujoco').origin)
sysconfig.get_config_var('PYTHONFRAMEWORKPREFIX'), os.environ['MJPYTHON_BIN'] = os.path.join(
sysconfig.get_config_var('INSTSONAME'), module_dir, 'MuJoCo (mjpython).app/Contents/MacOS/mjpython')
)
# Conda doesn't create a separate shared library for Python.
# We instead use the Python binary itself, which can be dlopened just as well.
os.environ['MJPYTHON_LIBPYTHON'] = get_executable_path()
# argv[0] is currently the path to this script.
# Replace it with sys.executable to preserve e.g. virtualenv path.
argv[0] = sys.executable argv[0] = sys.executable
mujoco_dir = os.path.dirname(importlib.util.find_spec('mujoco').origin)
os.execve( os.execve(os.environ['MJPYTHON_BIN'], argv, os.environ)
os.path.join(mujoco_dir, 'MuJoCo (mjpython).app/Contents/MacOS/mjpython'),
argv, os.environ)
if __name__ == '__main__': if __name__ == '__main__':