feat(nano): support Android library applications

master
韩天峰 2 weeks ago
parent d2824d2d08
commit f7f7547ffa
  1. 13
      README.md
  2. 13
      examples/android-native/README.md
  3. 20
      examples/android-native/build-app.sh
  4. 14
      phpunit/src/Build/NativeBuildConfigurationTest.php
  5. 17
      phpunit/src/Build/NativeDependencyAuditorTest.php
  6. 42
      phpunit/src/CompilerBaseApiTest.php
  7. 12
      src/Build/NativeDependencyAuditor.php
  8. 2
      src/Build/SourcePipelineTrait.php
  9. 13
      src/Preprocessor.php
  10. 21
      src/Translator.php

@ -257,17 +257,22 @@ string(16) "Linux ..."
> arguments, and must return `void`. Top-level executable statements are not > arguments, and must return `void`. Top-level executable statements are not
> allowed; executable code belongs in a function or method. > allowed; executable code belongs in a function or method.
### VM-free Nano executable ### VM-free Nano executables and libraries
Use `--nano` to compile one PHP source file together with PHP Nano and PHPX Use `--nano` to compile PHP sources together with PHP Nano and PHPX sources.
sources. The result does not link `libphp` and contains no Zend opcode The result does not link `libphp` and contains no Zend opcode interpreter:
interpreter:
```bash ```bash
./bin/tpc.php --nano examples/hello.php ./bin/tpc.php --nano examples/hello.php
./hello ./hello
``` ```
Nano supports `mode: bin` and `mode: lib`. Binary mode generates the process
`main()` entry and requires a TypePHP `main()` function. Library mode defines
`TYPEPHP_NO_MAIN` and exports the project-specific runtime initialization and
shutdown ABI for hosts such as an Android Activity. Nano does not support
`mode: ext`.
By default, the executable is emitted in the directory where `tpc` was invoked. By default, the executable is emitted in the directory where `tpc` was invoked.
Normal and Nano builds share the `build` directory for generated code, objects, Normal and Nano builds share the `build` directory for generated code, objects,
and other intermediate files. Use `-o` to select a different output path. and other intermediate files. Use `-o` to select a different output path.

@ -11,17 +11,24 @@ Prerequisites:
- Android SDK Platform/Build Tools 36 - Android SDK Platform/Build Tools 36
- Android NDK r27 or newer - Android NDK r27 or newer
- JDK with `javac` and `keytool` - JDK with `javac` and `keytool`
- the PHPX Android SDK produced by `phpx/sdk/build-native.sh` - the compiler Composer dependencies, including php-nano and PHPX sources
Build: Build:
```sh ```sh
export ANDROID_SDK_ROOT=/home/swoole/soft/android-sdk export ANDROID_SDK_ROOT=/home/swoole/soft/android-sdk
export ANDROID_NDK_HOME=/home/swoole/soft/android-ndk-r27d export ANDROID_NDK_HOME=/home/swoole/soft/android-ndk-r27d
export PHPX_ANDROID_SDK_DIR=/path/to/android-arm64-v8a-sdk export PHPX_HOME=/path/to/phpx
./build-app.sh ./build-app.sh --nano
``` ```
`tpc.php` itself still runs dynamically on the host PHP. `--nano` applies to
the generated JNI shared library, which compiles the selected php-nano and PHPX
sources directly and does not require a prebuilt Android `libphp.a` or
`libphpx.a`. For a non-Nano build, additionally set
`PHPX_ANDROID_SDK_DIR` to the SDK produced by `phpx/sdk/build-native.sh` and run
`./build-app.sh` without `--nano`.
The result is `dist/typephp-android-hello-debug.apk`. With an arm64 Android The result is `dist/typephp-android-hello-debug.apk`. With an arm64 Android
device connected and USB debugging enabled, install and launch it using: device connected and USB debugging enabled, install and launch it using:

@ -8,6 +8,16 @@ android_sdk=${ANDROID_SDK_ROOT:-${ANDROID_HOME:-/home/swoole/soft/android-sdk}}
android_ndk=${ANDROID_NDK_HOME:-${ANDROID_NDK_ROOT:-/home/swoole/soft/android-ndk-r27d}} android_ndk=${ANDROID_NDK_HOME:-${ANDROID_NDK_ROOT:-/home/swoole/soft/android-ndk-r27d}}
phpx_root=${PHPX_HOME:-$(cd "${compiler_root}/../phpx" && pwd)} phpx_root=${PHPX_HOME:-$(cd "${compiler_root}/../phpx" && pwd)}
phpx_android_sdk=${PHPX_ANDROID_SDK_DIR:-${phpx_root}/android/arm64-v8a} phpx_android_sdk=${PHPX_ANDROID_SDK_DIR:-${phpx_root}/android/arm64-v8a}
nano_arg=
case "${1:-}" in
"") ;;
--nano) nano_arg=--nano ;;
*)
echo "Usage: $0 [--nano]" >&2
exit 2
;;
esac
if [[ ! -d "${android_sdk}/build-tools" || ! -f "${android_sdk}/platforms/android-36/android.jar" ]]; then if [[ ! -d "${android_sdk}/build-tools" || ! -f "${android_sdk}/platforms/android-36/android.jar" ]]; then
echo "Android SDK Platform 36 and Build Tools are required: ${android_sdk}" >&2 echo "Android SDK Platform 36 and Build Tools are required: ${android_sdk}" >&2
@ -17,7 +27,7 @@ if [[ ! -f "${android_ndk}/build/cmake/android.toolchain.cmake" ]]; then
echo "Android NDK is required: ${android_ndk}" >&2 echo "Android NDK is required: ${android_ndk}" >&2
exit 1 exit 1
fi fi
if [[ ! -f "${phpx_android_sdk}/.typephp-android-sdk-abi" ]]; then if [[ -z "${nano_arg}" && ! -f "${phpx_android_sdk}/.typephp-android-sdk-abi" ]]; then
echo "TypePHP Android SDK is required: ${phpx_android_sdk}" >&2 echo "TypePHP Android SDK is required: ${phpx_android_sdk}" >&2
exit 1 exit 1
fi fi
@ -39,13 +49,19 @@ mkdir -p \
"${resources_dir}/drawable-nodpi" \ "${resources_dir}/drawable-nodpi" \
"${compiled_resources_dir}" \ "${compiled_resources_dir}" \
"${dist_dir}" "${dist_dir}"
cp -p "${compiler_root}/examples/objective-c-macos/ios-assets/AppIcon-180.png" \ cp -p "${compiler_root}/examples/apple-native/ios-assets/AppIcon-180.png" \
"${resources_dir}/drawable-nodpi/typephp_icon.png" "${resources_dir}/drawable-nodpi/typephp_icon.png"
if [[ -n "${nano_arg}" ]]; then
ANDROID_NDK_HOME="${android_ndk}" \
PHPX_HOME="${phpx_root}" \
php "${compiler_root}/bin/tpc.php" "${project_dir}/android.yml" "${nano_arg}" --no-progress
else
ANDROID_NDK_HOME="${android_ndk}" \ ANDROID_NDK_HOME="${android_ndk}" \
PHPX_HOME="${phpx_root}" \ PHPX_HOME="${phpx_root}" \
PHPX_ANDROID_SDK_DIR="${phpx_android_sdk}" \ PHPX_ANDROID_SDK_DIR="${phpx_android_sdk}" \
php "${compiler_root}/bin/tpc.php" "${project_dir}/android.yml" --no-progress php "${compiler_root}/bin/tpc.php" "${project_dir}/android.yml" --no-progress
fi
javac -source 8 -target 8 -encoding UTF-8 \ javac -source 8 -target 8 -encoding UTF-8 \
-bootclasspath "${android_jar}" \ -bootclasspath "${android_jar}" \

@ -183,6 +183,20 @@ final class NativeBuildConfigurationTest extends TestCase
self::assertSame(24, Android::getApiLevel('aarch64-linux-android24')); self::assertSame(24, Android::getApiLevel('aarch64-linux-android24'));
} }
public function testAndroidNanoLibraryDoesNotRequirePrebuiltPhpSdk(): void
{
$compiler = $this->newCompiler(new Android());
$compiler->setBuildMode(\TypePhp\CompilerBase::BUILD_MODE_LIB);
$compiler->enableNanoForTest();
self::assertSame([], $compiler->getLibraryPathsForTest());
self::assertSame([], $compiler->getLibrariesForTest());
$defines = $compiler->getCommonCompileOptionsForTest()['user_defines'];
self::assertContains('PHPX_ANDROID=1', $defines);
self::assertContains('TYPEPHP_NO_MAIN=1', $defines);
self::assertContains('PHP_NANO=1', $defines);
}
public function testNativeModulesDoNotFallBackToStaticPhpx(): void public function testNativeModulesDoNotFallBackToStaticPhpx(): void
{ {
$phpxDir = $this->temporaryDirectory('phpx-static-module'); $phpxDir = $this->temporaryDirectory('phpx-static-module');

@ -79,6 +79,23 @@ final class NativeDependencyAuditorTest extends TestCase
self::addToAssertionCount(1); self::addToAssertionCount(1);
} }
public function testAndroidAllowsRequiredBionicAbiButStillRejectsNetwork(): void
{
$auditor = new NativeDependencyAuditor();
$auditor->assertUndefinedSymbols(
'android',
" U closelog\n"
. " U openlog\n"
. " U syscall\n"
. " U syslog\n",
);
self::addToAssertionCount(1);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('socket');
$auditor->assertUndefinedSymbols('android', " U socket\n");
}
public function testOnlyNonPosixExceptionsNeedAnAllowlistEntry(): void public function testOnlyNonPosixExceptionsNeedAnAllowlistEntry(): void
{ {
self::assertContains('flock', NativeDependencyAuditor::NON_POSIX_HOST_FUNCTION_ALLOWLIST); self::assertContains('flock', NativeDependencyAuditor::NON_POSIX_HOST_FUNCTION_ALLOWLIST);

@ -1334,6 +1334,22 @@ YAML);
$this->assertSame([dirname($projectFile) . '/build'], $this->compiler->getLinkPaths()); $this->assertSame([dirname($projectFile) . '/build'], $this->compiler->getLinkPaths());
} }
public function testNanoModeSupportsLibraryBuildsButRejectsExtensions(): void
{
$this->setPropertyValue('nanoMode', true);
$this->setPropertyValue('buildMode', CompilerBase::BUILD_MODE_LIB);
$this->invokeMethod('applyCommandLineArguments');
$this->assertSame(CompilerBase::BUILD_MODE_LIB, $this->compiler->getBuildMode());
$this->setPropertyValue('buildMode', CompilerBase::BUILD_MODE_EXT);
$this->expectException(TestError::class);
$this->expectExceptionMessage(
'--nano source composition does not support extension mode (-m ext)',
);
$this->invokeMethod('applyCommandLineArguments');
}
public function testParseProjectYamlFiltersIgnoredFilesFromReturnedSources(): void public function testParseProjectYamlFiltersIgnoredFilesFromReturnedSources(): void
{ {
$projectFile = $this->createProjectFile(<<<'YAML' $projectFile = $this->createProjectFile(<<<'YAML'
@ -1686,6 +1702,32 @@ YAML);
); );
} }
public function testNanoLibraryRuntimeEntryHasProjectSpecificCompileOptions(): void
{
$this->compiler->setTargetName('nano_module_accessor');
$this->setPropertyValue('buildMode', CompilerBase::BUILD_MODE_LIB);
$this->setPropertyValue('nanoMode', true);
$this->setPropertyValue('precompiledHeader', [
'header' => '/tmp/typephp_pch.hpp',
'artifact' => '/tmp/typephp_pch.hpp.gch',
]);
$phpxDir = $this->invokeMethod('getPhpxDir');
$entry = $phpxDir . '/src/typephp/typephp_main_nano.cc';
$options = $this->invokeMethod('getSourceCompileCommandOptions', $entry, null);
$this->assertContains('TYPEPHP_PROJECT_NAME=nano_module_accessor', $options['user_defines']);
$this->assertContains('TYPEPHP_RUNTIME_EXPORTS=1', $options['user_defines']);
$this->assertArrayNotHasKey('forced_include', $options->toArray());
$this->assertArrayNotHasKey('precompiled_header', $options->toArray());
$this->assertFalse($this->compiler->hasMiscObjectFileCache($entry));
$nanoCore = dirname($phpxDir) . '/php-nano/src/core.cpp';
$this->setPropertyValue('nanoRuntimeSources', [$nanoCore => true]);
$coreOptions = $this->invokeMethod('getSourceCompileCommandOptions', $nanoCore, null);
$this->assertArrayNotHasKey('forced_include', $coreOptions->toArray());
}
public function testProjectIndependentMiscObjectsUseSharedCacheScope(): void public function testProjectIndependentMiscObjectsUseSharedCacheScope(): void
{ {
$phpxDir = $this->invokeMethod('getPhpxDir'); $phpxDir = $this->invokeMethod('getPhpxDir');

@ -15,6 +15,14 @@ final class NativeDependencyAuditor
'flock', 'flock',
]; ];
/** Bionic/libc++ implementation imports required by an Android JNI library. */
public const array ANDROID_SYSTEM_ABI_ALLOWLIST = [
'closelog',
'openlog',
'syscall',
'syslog',
];
/** @param list<string> $flags */ /** @param list<string> $flags */
public function assertLinkFlags(string $target, array $flags): void public function assertLinkFlags(string $target, array $flags): void
{ {
@ -79,6 +87,10 @@ final class NativeDependencyAuditor
if ($symbol === 'proc_raise' || $symbol === 'sched_yield') { if ($symbol === 'proc_raise' || $symbol === 'sched_yield') {
return true; return true;
} }
} elseif ($target === 'android'
&& in_array($symbol, self::ANDROID_SYSTEM_ABI_ALLOWLIST, true)
) {
return false;
} elseif (in_array($symbol, self::NON_POSIX_HOST_FUNCTION_ALLOWLIST, true)) { } elseif (in_array($symbol, self::NON_POSIX_HOST_FUNCTION_ALLOWLIST, true)) {
return false; return false;
} }

@ -1228,7 +1228,7 @@ trait SourcePipelineTrait
&& ($this->isSapiBuild() || $this->embeddedFiles !== [] || $this->embeddedOpcodeFiles !== [])) { && ($this->isSapiBuild() || $this->embeddedFiles !== [] || $this->embeddedOpcodeFiles !== [])) {
array_push($sourceFiles, ...$this->genEmbeddedOpcodeTable()); array_push($sourceFiles, ...$this->genEmbeddedOpcodeTable());
} }
if ($this->isNanoMode()) { if ($this->isNanoMode() && $this->isBuildModeBin()) {
$sourceFiles[] = $this->genNanoEntrypoint(); $sourceFiles[] = $this->genNanoEntrypoint();
} }
$this->getStableIdRegistry()->flush(); $this->getStableIdRegistry()->flush();

@ -434,7 +434,18 @@ class Preprocessor extends CompilerBase
{ {
$normalizedFile = str_replace('\\', '/', $file); $normalizedFile = str_replace('\\', '/', $file);
$runtimeEntry = str_replace('\\', '/', $this->getPhpxDir() . '/src/misc/typephp_main.cc'); $runtimeEntry = str_replace('\\', '/', $this->getPhpxDir() . '/src/misc/typephp_main.cc');
return $normalizedFile === $runtimeEntry; if ($normalizedFile === $runtimeEntry) {
return true;
}
if (!$this->isNanoMode() || !$this->isBuildModeLib()) {
return false;
}
$nanoLibraryEntry = str_replace(
'\\',
'/',
$this->getPhpxDir() . '/src/typephp/typephp_main_nano.cc',
);
return $normalizedFile === $nanoLibraryEntry;
} }
public function prepareFile(string $file): void public function prepareFile(string $file): void

@ -653,8 +653,8 @@ class Translator extends Preprocessor
} }
if ($this->isNanoMode()) { if ($this->isNanoMode()) {
if (!$this->isBuildModeBin()) { if ($this->isBuildModeExt()) {
$this->error('--nano source composition only supports binary mode (-m bin)'); $this->error('--nano source composition does not support extension mode (-m ext)');
} }
if ($this->cxxStd !== 'c++17') { if ($this->cxxStd !== 'c++17') {
$this->error('--nano requires the C++17 language standard'); $this->error('--nano requires the C++17 language standard');
@ -2493,7 +2493,9 @@ CODE;
'c' => $this->getCCompileCommandOptions(), 'c' => $this->getCCompileCommandOptions(),
default => $this->getNativeCompileCommandOptions($language), default => $this->getNativeCompileCommandOptions($language),
}; };
if ($this->isGeneratedProjectSource($sourceFile) && isset($options['forced_include'])) { if (($this->isGeneratedProjectSource($sourceFile)
|| isset($this->nanoRuntimeSources[$sourceFile]))
&& isset($options['forced_include'])) {
$values = $options->toArray(); $values = $options->toArray();
unset($values['forced_include']); unset($values['forced_include']);
return new CompileOptions($values); return new CompileOptions($values);
@ -3042,6 +3044,19 @@ CODE;
); );
return; return;
} }
if ($this->isAndroidTarget()) {
$nm = getenv('TYPEPHP_ANDROID_NM');
if (!is_string($nm) || $nm === '') {
$compiler = $this->getCompilerBackend()->getCompilerCommand();
$candidate = dirname($compiler) . DIRECTORY_SEPARATOR . 'llvm-nm';
$nm = is_executable($candidate) ? $candidate : 'llvm-nm';
}
$auditor->assertUndefinedSymbols(
'android',
$this->captureNativeCommand([$nm, '--undefined-only', $targetFile]),
);
return;
}
/* Native objects may deliberately provide an OS ABI to one another /* Native objects may deliberately provide an OS ABI to one another
* (for example a freestanding syscall shim). Only unresolved imports * (for example a freestanding syscall shim). Only unresolved imports
* in the final ELF cross the host-capability boundary. */ * in the final ELF cross the host-capability boundary. */

Loading…
Cancel
Save