Files
Mujoco_WASM/python/mujoco/mjpython/mjpython.mm
T
Saran Tunyasuvunakool 551a7bbc38 Drop support for Python 3.7.
Python 3.7 reaches end-of-life on 27 June 2023 (https://devguide.python.org/versions/), and future versions of MuJoCo (after 2.3.6) will no longer support it.

PiperOrigin-RevId: 543594006
Change-Id: I66a7db3dab5d08f2fc52912c43e1532080600fc8
2023-06-26 18:05:32 -07:00

312 lines
11 KiB
Plaintext

// Copyright 2023 DeepMind Technologies Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import <algorithm>
#import <atomic>
#import <cstdint>
#import <cstdlib>
#import <cstring>
#import <iostream>
#import <vector>
#import <dlfcn.h>
#import <mach-o/dyld.h>
#import <pthread.h>
#import <sys/resource.h>
#import <unistd.h>
#import <Cocoa/Cocoa.h>
#import <Python.h>
extern "C" {
extern char **environ; // for execve
// Wrap Objective-C Cocoa calls into C-style functions with default visibility,
// so that we can dlsym and call them from Python via ctypes.
__attribute__((used)) void mjpython_hide_dock_icon() {
[NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory];
}
__attribute__((used)) void mjpython_show_dock_icon() {
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
}
} // extern "C"
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 {
#define CPYTHON_FN(fname) decltype(&::fname) fname
// go/keep-sorted start
CPYTHON_FN(PyConfig_Clear);
CPYTHON_FN(PyConfig_InitPythonConfig);
CPYTHON_FN(PyConfig_SetBytesArgv);
CPYTHON_FN(PyGILState_Ensure);
CPYTHON_FN(PyGILState_Release);
CPYTHON_FN(PyRun_SimpleStringFlags);
CPYTHON_FN(Py_FinalizeEx);
CPYTHON_FN(Py_InitializeFromConfig);
CPYTHON_FN(Py_RunMain);
// go/keep-sorted end
#undef CPYTHON_FN
} cpython;
std::atomic_bool py_initialized = false;
struct Args {
int argc;
char** argv;
};
// The Python main thread (distinct from the macOS main thread, which executes the main function).
void* mjpython_pymain(void* vargs) {
Args* args = static_cast<Args*>(vargs);
PyGILState_STATE gil;
// Initialize the Python interpreter.
PyConfig config;
cpython.PyConfig_InitPythonConfig(&config);
cpython.PyConfig_SetBytesArgv(&config, args->argc, args->argv);
cpython.Py_InitializeFromConfig(&config);
cpython.PyConfig_Clear(&config);
// Set up the condition variable to pass control back to the macOS main thread.
gil = cpython.PyGILState_Ensure();
cpython.PyRun_SimpleStringFlags("import threading; cond = threading.Condition()", nullptr);
py_initialized.store(true);
// Wait until GLFW is initialized on macOS main thread, set up the queue and an atexit hook
// to enqueue a termination flag upon exit.
cpython.PyRun_SimpleStringFlags(R"(
import atexit
# The mujoco.viewer module should only be imported here after glfw.init() in the macOS main thread.
with cond:
cond.wait()
import mujoco.viewer
# Similar to a queue.Queue(maxsize=1), but where only one active task is allowed at a time.
# With queue.Queue(1), another item is allowed to be enqueued before task_done is called.
class _MjPythonImpl(mujoco.viewer._MjPythonBase):
# Termination statuses
NOT_TERMINATED = 0
TERMINATION_REQUESTED = 1
TERMINATION_ACCEPTED = 2
TERMINATED = 3
def __init__(self):
self._cond = threading.Condition()
self._task = None
self._termination = self.__class__.NOT_TERMINATED
self._busy = False
def launch_on_ui_thread(self, model, data, handle_return):
with self._cond:
if self._busy or self._task is not None:
raise RuntimeError('another MuJoCo viewer is already open')
else:
self._task = (model, data, handle_return)
self._cond.notify()
def terminate(self):
with self._cond:
self._termination = self.__class__.TERMINATION_REQUESTED
self._cond.notify()
self._cond.wait_for(
lambda: self._termination == self.__class__.TERMINATED)
def get(self):
with self._cond:
self._cond.wait_for(
lambda: self._task is not None or self._termination)
if self._termination:
if self._termination == self.__class__.TERMINATION_REQUESTED:
self._termination = self.__class__.TERMINATION_ACCEPTED
return None
task = self._task
self._busy = True
self._task = None
return task
def done(self):
with self._cond:
self._busy = False
if self._termination == self.__class__.TERMINATION_ACCEPTED:
self._termination = self.__class__.TERMINATED
self._cond.notify()
mujoco.viewer._MJPYTHON = _MjPythonImpl()
atexit.register(mujoco.viewer._MJPYTHON.terminate)
del _MjPythonImpl # Don't pollute globals for user script.
with cond:
cond.notify()
del cond # Don't pollute globals for user script.
)", nullptr);
// Run the Python interpreter main loop.
cpython.Py_RunMain();
// Tear down the interpreter.
cpython.Py_FinalizeEx();
return nullptr;
}
} // namespace
int main(int argc, char** argv) {
const char* libpython_path = getenv("MJPYTHON_LIBPYTHON");
if (!libpython_path || !libpython_path[0]) {
std::cerr << "This binary must be launched via the mjpython.py script.\n";
return EXIT_FAILURE;
}
// 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
// path is passed from a Python trampoline script, which ran inside the desired interpreter and
// exec'd this binary.
void* libpython = dlopen(libpython_path, RTLD_NOW | RTLD_GLOBAL);
if (!libpython) {
std::cerr << "failed to dlopen path '" << libpython_path << "': " << dlerror() << "\n";
return EXIT_FAILURE;
}
// Look up required CPython API functions from table of symbols already loaded into the process.
#define CPYTHON_INITFN(fname) \
{ \
cpython.fname = reinterpret_cast<decltype(cpython.fname)>(dlsym(libpython, #fname)); \
if (!cpython.fname) { \
std::cerr << "failed to dlsym '" << #fname << "': " << dlerror() << "\n"; \
return EXIT_FAILURE; \
} \
}
// go/keep-sorted start
CPYTHON_INITFN(PyConfig_Clear);
CPYTHON_INITFN(PyConfig_InitPythonConfig);
CPYTHON_INITFN(PyConfig_SetBytesArgv);
CPYTHON_INITFN(PyGILState_Ensure);
CPYTHON_INITFN(PyGILState_Release);
CPYTHON_INITFN(PyRun_SimpleStringFlags);
CPYTHON_INITFN(Py_FinalizeEx);
CPYTHON_INITFN(Py_InitializeFromConfig);
CPYTHON_INITFN(Py_RunMain);
// go/keep-sorted end
#undef CPYTHON_INITFN
// Package up argc and argv together to pass to pthread_create.
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"; \
return EXIT_FAILURE; \
} \
}
// 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".
pthread_t pymain_thread;
PTHREAD_CHECKED(pthread_create, &pymain_thread, &pthread_attr, &mjpython_pymain, &args);
pthread_attr_destroy(&pthread_attr);
#undef PTHREAD_CHECKED
// Busy-wait until Python interpreter is initialized.
while (!py_initialized.load()) {}
// Initialize GLFW on the macOS main thread, yield control to Python main thread and wait for it
// to finish setting up _MJPYTHON, then serve incoming viewer launch requests.
PyGILState_STATE gil = cpython.PyGILState_Ensure();
cpython.PyRun_SimpleStringFlags(R"(
import ctypes
# GLFW must be initialized on the OS main thread (i.e. here).
import glfw
import mujoco.viewer
glfw.init()
glfw.poll_events()
ctypes.CDLL(None).mjpython_hide_dock_icon()
# Wait for Python main thread to finish setting up _MJPYTHON
with cond:
cond.notify()
cond.wait()
while True:
try:
# Wait for an incoming payload.
task = mujoco.viewer._MJPYTHON.get()
# None means that we are exiting.
if task is None:
glfw.terminate()
break
# Otherwise, launch the viewer.
model, data, handle_return = task
ctypes.CDLL(None).mjpython_show_dock_icon()
mujoco.viewer._launch_internal(
model, data, run_physics_thread=False, handle_return=handle_return)
ctypes.CDLL(None).mjpython_hide_dock_icon()
finally:
mujoco.viewer._MJPYTHON.done()
)", nullptr);
cpython.PyGILState_Release(gil);
// Tear everything down.
pthread_join(pymain_thread, nullptr);
dlclose(libpython);
}