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
> 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
sources. The result does not link `libphp` and contains no Zend opcode
interpreter:
Use `--nano` to compile PHP sources together with PHP Nano and PHPX sources.
The result does not link `libphp` and contains no Zend opcode interpreter:
```bash
./bin/tpc.php --nano examples/hello.php
./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.
Normal and Nano builds share the `build` directory for generated code, objects,
and other intermediate files. Use `-o` to select a different output path.

@ -11,17 +11,24 @@ Prerequisites:
- Android SDK Platform/Build Tools 36
- Android NDK r27 or newer
- 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:
```sh
export ANDROID_SDK_ROOT=/home/swoole/soft/android-sdk
export ANDROID_NDK_HOME=/home/swoole/soft/android-ndk-r27d
export PHPX_ANDROID_SDK_DIR=/path/to/android-arm64-v8a-sdk
./build-app.sh
export PHPX_HOME=/path/to/phpx
./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
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}}
phpx_root=${PHPX_HOME:-$(cd "${compiler_root}/../phpx" && pwd)}
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
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
exit 1
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
exit 1
fi
@ -39,13 +49,19 @@ mkdir -p \
"${resources_dir}/drawable-nodpi" \
"${compiled_resources_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"
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}" \
PHPX_HOME="${phpx_root}" \
PHPX_ANDROID_SDK_DIR="${phpx_android_sdk}" \
php "${compiler_root}/bin/tpc.php" "${project_dir}/android.yml" --no-progress
fi
javac -source 8 -target 8 -encoding UTF-8 \
-bootclasspath "${android_jar}" \

@ -183,6 +183,20 @@ final class NativeBuildConfigurationTest extends TestCase
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
{
$phpxDir = $this->temporaryDirectory('phpx-static-module');

@ -79,6 +79,23 @@ final class NativeDependencyAuditorTest extends TestCase
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
{
self::assertContains('flock', NativeDependencyAuditor::NON_POSIX_HOST_FUNCTION_ALLOWLIST);

@ -1334,6 +1334,22 @@ YAML);
$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
{
$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
{
$phpxDir = $this->invokeMethod('getPhpxDir');

@ -15,6 +15,14 @@ final class NativeDependencyAuditor
'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 */
public function assertLinkFlags(string $target, array $flags): void
{
@ -79,6 +87,10 @@ final class NativeDependencyAuditor
if ($symbol === 'proc_raise' || $symbol === 'sched_yield') {
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)) {
return false;
}

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

@ -434,7 +434,18 @@ class Preprocessor extends CompilerBase
{
$normalizedFile = str_replace('\\', '/', $file);
$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

@ -653,8 +653,8 @@ class Translator extends Preprocessor
}
if ($this->isNanoMode()) {
if (!$this->isBuildModeBin()) {
$this->error('--nano source composition only supports binary mode (-m bin)');
if ($this->isBuildModeExt()) {
$this->error('--nano source composition does not support extension mode (-m ext)');
}
if ($this->cxxStd !== 'c++17') {
$this->error('--nano requires the C++17 language standard');
@ -2493,7 +2493,9 @@ CODE;
'c' => $this->getCCompileCommandOptions(),
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();
unset($values['forced_include']);
return new CompileOptions($values);
@ -3042,6 +3044,19 @@ CODE;
);
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
* (for example a freestanding syscall shim). Only unresolved imports
* in the final ELF cross the host-capability boundary. */

Loading…
Cancel
Save