diff --git a/composer.json b/composer.json index c8fb559d74..fd47dca93e 100644 --- a/composer.json +++ b/composer.json @@ -40,7 +40,6 @@ "pocketmine/bedrock-protocol": "~21.0.0+bedrock-1.19.80", "pocketmine/binaryutils": "^0.2.1", "pocketmine/callback-validator": "^1.0.2", - "pocketmine/classloader": "dev-stable", "pocketmine/color": "^0.3.0", "pocketmine/errorhandler": "^0.6.0", "pocketmine/locale-data": "~2.19.0", diff --git a/composer.lock b/composer.lock index 05d87b634d..a9d1e6d810 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0dc836612512d87a694945ffb87a4096", + "content-hash": "812a98ecf031488987b3216e785fee19", "packages": [ { "name": "adhocore/json-comment", @@ -464,52 +464,6 @@ }, "time": "2020-12-11T01:45:37+00:00" }, - { - "name": "pocketmine/classloader", - "version": "dev-stable", - "source": { - "type": "git", - "url": "https://github.com/pmmp/ClassLoader.git", - "reference": "e15c9b4d310581d2d2c9bf2794869cb940e011e1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/pmmp/ClassLoader/zipball/e15c9b4d310581d2d2c9bf2794869cb940e011e1", - "reference": "e15c9b4d310581d2d2c9bf2794869cb940e011e1", - "shasum": "" - }, - "require": { - "ext-pmmpthread": "^6.0", - "ext-reflection": "*", - "php": "^8.1" - }, - "conflict": { - "pocketmine/spl": "<0.4" - }, - "require-dev": { - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "1.10.15", - "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "^9.5" - }, - "default-branch": true, - "type": "library", - "autoload": { - "classmap": [ - "./src" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "LGPL-3.0" - ], - "description": "Ad-hoc autoloading components used by PocketMine-MP", - "support": { - "issues": "https://github.com/pmmp/ClassLoader/issues", - "source": "https://github.com/pmmp/ClassLoader/tree/stable" - }, - "time": "2023-05-19T23:39:02+00:00" - }, { "name": "pocketmine/color", "version": "0.3.1", @@ -3252,7 +3206,6 @@ "aliases": [], "minimum-stability": "stable", "stability-flags": { - "pocketmine/classloader": 20, "pocketmine/snooze": 20 }, "prefer-stable": false, diff --git a/src/PocketMine.php b/src/PocketMine.php index 018d8e0bf1..7003cca85d 100644 --- a/src/PocketMine.php +++ b/src/PocketMine.php @@ -26,6 +26,7 @@ namespace pocketmine { use Composer\InstalledVersions; use pocketmine\errorhandler\ErrorToExceptionHandler; use pocketmine\thread\ThreadManager; + use pocketmine\thread\ThreadSafeClassLoader; use pocketmine\utils\Filesystem; use pocketmine\utils\MainLogger; use pocketmine\utils\Process; @@ -327,7 +328,7 @@ JIT_WARNING /* * We now use the Composer autoloader, but this autoloader is still for loading plugins. */ - $autoloader = new \BaseClassLoader(); + $autoloader = new ThreadSafeClassLoader(); $autoloader->register(false); new Server($autoloader, $logger, $dataPath, $pluginPath); diff --git a/src/Server.php b/src/Server.php index df419c918e..f604c93d7e 100644 --- a/src/Server.php +++ b/src/Server.php @@ -93,6 +93,7 @@ use pocketmine\scheduler\AsyncPool; use pocketmine\snooze\SleeperHandler; use pocketmine\stats\SendUsageTask; use pocketmine\thread\log\AttachableThreadSafeLogger; +use pocketmine\thread\ThreadSafeClassLoader; use pocketmine\timings\Timings; use pocketmine\timings\TimingsHandler; use pocketmine\updater\UpdateChecker; @@ -414,7 +415,7 @@ class Server{ return $this->configGroup->getConfigString("motd", self::DEFAULT_SERVER_NAME); } - public function getLoader() : \DynamicClassLoader{ + public function getLoader() : ThreadSafeClassLoader{ return $this->autoloader; } @@ -760,7 +761,7 @@ class Server{ } public function __construct( - private \DynamicClassLoader $autoloader, + private ThreadSafeClassLoader $autoloader, private AttachableThreadSafeLogger $logger, string $dataPath, string $pluginPath diff --git a/src/plugin/PharPluginLoader.php b/src/plugin/PharPluginLoader.php index b812f20c8a..a8dc04804a 100644 --- a/src/plugin/PharPluginLoader.php +++ b/src/plugin/PharPluginLoader.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\plugin; +use pocketmine\thread\ThreadSafeClassLoader; use function is_file; use function str_ends_with; @@ -31,7 +32,7 @@ use function str_ends_with; */ class PharPluginLoader implements PluginLoader{ public function __construct( - private \DynamicClassLoader $loader + private ThreadSafeClassLoader $loader ){} public function canLoadPlugin(string $path) : bool{ diff --git a/src/scheduler/AsyncPool.php b/src/scheduler/AsyncPool.php index 72d8b29e5a..1ee726765e 100644 --- a/src/scheduler/AsyncPool.php +++ b/src/scheduler/AsyncPool.php @@ -28,6 +28,7 @@ use pmmp\thread\ThreadSafeArray; use pocketmine\snooze\SleeperHandler; use pocketmine\snooze\SleeperNotifier; use pocketmine\thread\log\ThreadSafeLogger; +use pocketmine\thread\ThreadSafeClassLoader; use pocketmine\utils\Utils; use function array_keys; use function array_map; @@ -70,7 +71,7 @@ class AsyncPool{ public function __construct( protected int $size, private int $workerMemoryLimit, - private \ClassLoader $classLoader, + private ThreadSafeClassLoader $classLoader, private ThreadSafeLogger $logger, private SleeperHandler $eventLoop ){} diff --git a/src/thread/CommonThreadPartsTrait.php b/src/thread/CommonThreadPartsTrait.php index a2429b056b..c35dd77914 100644 --- a/src/thread/CommonThreadPartsTrait.php +++ b/src/thread/CommonThreadPartsTrait.php @@ -30,8 +30,8 @@ use function error_reporting; trait CommonThreadPartsTrait{ /** - * @var ThreadSafeArray|\ClassLoader[]|null - * @phpstan-var ThreadSafeArray|null + * @var ThreadSafeArray|ThreadSafeClassLoader[]|null + * @phpstan-var ThreadSafeArray|null */ private ?ThreadSafeArray $classLoaders = null; protected ?string $composerAutoloaderPath = null; @@ -39,14 +39,14 @@ trait CommonThreadPartsTrait{ protected bool $isKilled = false; /** - * @return \ClassLoader[] + * @return ThreadSafeClassLoader[] */ public function getClassLoaders() : ?array{ return $this->classLoaders !== null ? (array) $this->classLoaders : null; } /** - * @param \ClassLoader[] $autoloaders + * @param ThreadSafeClassLoader[] $autoloaders */ public function setClassLoaders(?array $autoloaders = null) : void{ $this->composerAutoloaderPath = \pocketmine\COMPOSER_AUTOLOADER_PATH; @@ -82,7 +82,7 @@ trait CommonThreadPartsTrait{ $autoloaders = $this->classLoaders; if($autoloaders !== null){ foreach($autoloaders as $autoloader){ - /** @var \ClassLoader $autoloader */ + /** @var ThreadSafeClassLoader $autoloader */ $autoloader->register(false); } } diff --git a/src/thread/ThreadSafeClassLoader.php b/src/thread/ThreadSafeClassLoader.php new file mode 100644 index 0000000000..95b983dc1b --- /dev/null +++ b/src/thread/ThreadSafeClassLoader.php @@ -0,0 +1,182 @@ + + */ + private $fallbackLookup; + /** + * @var ThreadSafeArray|string[][] + * @phpstan-var ThreadSafeArray> + */ + private $psr4Lookup; + + public function __construct(){ + $this->fallbackLookup = new ThreadSafeArray(); + $this->psr4Lookup = new ThreadSafeArray(); + } + + protected function normalizePath(string $path) : string{ + $parts = explode("://", $path, 2); + if(count($parts) === 2){ + return $parts[0] . "://" . str_replace('/', DIRECTORY_SEPARATOR, $parts[1]); + } + return str_replace('/', DIRECTORY_SEPARATOR, $parts[0]); + } + + public function addPath(string $namespacePrefix, string $path, bool $prepend = false) : void{ + $path = $this->normalizePath($path); + if($namespacePrefix === '' || $namespacePrefix === '\\'){ + $this->fallbackLookup->synchronized(function() use ($path, $prepend) : void{ + $this->appendOrPrependLookupEntry($this->fallbackLookup, $path, $prepend); + }); + }else{ + $namespacePrefix = trim($namespacePrefix, '\\') . '\\'; + $this->psr4Lookup->synchronized(function() use ($namespacePrefix, $path, $prepend) : void{ + $list = $this->psr4Lookup[$namespacePrefix] ?? null; + if($list === null){ + $list = $this->psr4Lookup[$namespacePrefix] = new ThreadSafeArray(); + } + $this->appendOrPrependLookupEntry($list, $path, $prepend); + }); + } + } + + /** + * @phpstan-param ThreadSafeArray $list + */ + protected function appendOrPrependLookupEntry(ThreadSafeArray $list, string $entry, bool $prepend) : void{ + if($prepend){ + $entries = $this->getAndRemoveLookupEntries($list); + $list[] = $entry; + foreach($entries as $removedEntry){ + $list[] = $removedEntry; + } + }else{ + $list[] = $entry; + } + } + + /** + * @return string[] + * + * @phpstan-param ThreadSafeArray $list + * @phpstan-return list + */ + protected function getAndRemoveLookupEntries(ThreadSafeArray $list) : array{ + $entries = []; + while(($entry = $list->shift()) !== null){ + $entries[] = $entry; + } + return $entries; + } + + public function register(bool $prepend = false) : bool{ + return spl_autoload_register(function(string $name) : void{ + $this->loadClass($name); + }, true, $prepend); + } + + /** + * Called when there is a class to load + */ + public function loadClass(string $name) : bool{ + $path = $this->findClass($name); + if($path !== null){ + include($path); + if(!class_exists($name, false) && !interface_exists($name, false) && !trait_exists($name, false)){ + return false; + } + + if(method_exists($name, "onClassLoaded") && (new \ReflectionClass($name))->getMethod("onClassLoaded")->isStatic()){ + $name::onClassLoaded(); + } + + return true; + } + + return false; + } + + /** + * Returns the path for the class, if any + */ + public function findClass(string $name) : ?string{ + $baseName = str_replace("\\", DIRECTORY_SEPARATOR, $name); + + foreach($this->fallbackLookup as $path){ + $filename = $path . DIRECTORY_SEPARATOR . $baseName . ".php"; + if(file_exists($filename)){ + return $filename; + } + } + + // PSR-4 lookup + $logicalPathPsr4 = $baseName . ".php"; + + return $this->psr4Lookup->synchronized(function() use ($name, $logicalPathPsr4) : ?string{ + $subPath = $name; + while(false !== $lastPos = strrpos($subPath, '\\')){ + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath . '\\'; + $lookup = $this->psr4Lookup[$search] ?? null; + if($lookup !== null){ + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach($lookup as $dir){ + if(file_exists($file = $dir . $pathEnd)){ + return $file; + } + } + } + } + return null; + }); + } +} diff --git a/tests/phpunit/scheduler/AsyncPoolTest.php b/tests/phpunit/scheduler/AsyncPoolTest.php index 436251d9e3..fd7dc344ac 100644 --- a/tests/phpunit/scheduler/AsyncPoolTest.php +++ b/tests/phpunit/scheduler/AsyncPoolTest.php @@ -27,6 +27,7 @@ use PHPUnit\Framework\TestCase; use pmmp\thread\ThreadSafeArray; use pocketmine\promise\PromiseResolver; use pocketmine\snooze\SleeperHandler; +use pocketmine\thread\ThreadSafeClassLoader; use pocketmine\utils\MainLogger; use function define; use function dirname; @@ -45,7 +46,7 @@ class AsyncPoolTest extends TestCase{ public function setUp() : void{ @define('pocketmine\\COMPOSER_AUTOLOADER_PATH', dirname(__DIR__, 3) . '/vendor/autoload.php'); $this->mainLogger = new MainLogger(tempnam(sys_get_temp_dir(), "pmlog"), false, "Main", new \DateTimeZone('UTC')); - $this->pool = new AsyncPool(2, 1024, new \BaseClassLoader(), $this->mainLogger, new SleeperHandler()); + $this->pool = new AsyncPool(2, 1024, new ThreadSafeClassLoader(), $this->mainLogger, new SleeperHandler()); } public function tearDown() : void{