APT插件实战

Annotation Processor

大量框架(例如Room, Dagger2, DataBinding, Butterknife)都利用Java1.6引入的annotation processor(后文统称注解处理器)来实现自己的框架功能。

这篇文章简单介绍注解处理器

注解处理器的运行原理

整个代码生成的过程发生在编译期,javac会获知所有的注解处理器,同时在编译时处理所有的注解。
由于整个过程在编译期完成,你可以认为注解处理器是编译器的一部分,因此最后的编译产物是不会包含编译处理器的内容的。

实现一个简单的注解处理器

基本步骤

  1. 创建一个注解module(Java Library),这个module只包含注解(Annotation)
  2. 创建一个处理器module(Java Library),这个module包含相关注解的处理逻辑

解决依赖

注解相关的module不需要任何依赖。
处理器module需要依赖我们自定义的注解和一些工具库。上文中提到过,这个module将会在编译器完成自己的任务,因此依赖此module的app不会打包这个module的内容。所以不用担心这个module的包体积问题

1
2
3
4
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.google.guava:guava:21.0'
implementation 'com.squareup:javapoet:1.8.0'
implementation project(':annotation')

之后给我们的app添加上注解处理器的编译依赖,这里使用另外一个依赖关键字annotationProcessor

1
2
implementation project(':annotation')
annotationProcessor project(':processor')

annotationProcessor表示我们只需要编译期有这个依赖,不需要打包进apk

创建注解

这里复习一下Java中注解的相关概念

@Target:指明你的注解作用的对象,可以有许多类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public enum ElementType {
TYPE, //If you want to annotate class, interface, enum..
FIELD, //If you want to annotate field (includes enum constants)
METHOD, //If you want to annotate method
PARAMETER, //If you want to annotate parameter
CONSTRUCTOR, //If you want to annotate constructor
LOCAL_VARIABLE, //..
ANNOTATION_TYPE, //..
PACKAGE, //..
TYPE_PARAMETER, //..(java 8)
TYPE_USE; //..(java 8)

private ElementType() {
}
}

@Retention:指明注解如何存储,有三种存储方式

  1. SOURCE—用于编译期,不会保存
  2. CLASS—保存到最后的class文件,运行期不会保留
  3. RUNTIME—保存到class文件,同时运行期也会保留(用于反射)

我们这个场景只需要编译期注解:

1
2
3
4
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface NewIntent {
}

创建注解处理器

在注解处理器模块添加一个新类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.github.tedaliez.processor;

import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;

public class NewIntentProcessor extends AbstractProcessor {

private Types typeUtils;
private Elements elementUtils;
private Filer filer;
private Messager messager;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
typeUtils = processingEnv.getTypeUtils();
elementUtils = processingEnv.getElementUtils();
filer = processingEnv.getFiler();
messager = processingEnv.getMessager();
}

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
return false;
}

@Override
public Set<String> getSupportedAnnotationTypes() {
return super.getSupportedAnnotationTypes();
}

@Override
public SourceVersion getSupportedSourceVersion() {
return super.getSupportedSourceVersion();
}
}

init()

初始化的回调。在这个方法中我们创建了四个类型的引用:

  1. Elements:Element(下文介绍)的工具类
  2. Types:TypeMirror(下文介绍)的工具类
  3. Filer:用来创建文件
  4. Messager:用来当注解处理器出现编译错误时打印错误信息
    Element
    注解处理过程中我们会扫描我们的java源代码。每个代码都有一个确定的Element类型,换句话说,Elment代表了一个程序的语言层级的元素
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.example;	// PackageElement

public class Foo { // TypeElement

private int a; // VariableElement
private Foo other; // VariableElement

public Foo () {} // ExecuteableElement

public void setA ( // ExecuteableElement
int newA // TypeElement
) {}
}

但是你不能从Element中获取类的相关信息,例如类的父类关系。这类信息得从TypeMirror中获得

Messager

用来当注解处理器出现编译错误时打印错误信息,需要注意的是当有异常出现时,你的注解处理器也应该正常结束而非选择抛出异常。如果抛出异常结束,那么javac会打印你的堆栈,但你messager的信息则不会被打印出来

process()

注解处理器中最重要的方法,方法提供了所有被注解的元素。在这个函数中你可以来处理你的注解,我们在这里实现生成代码的逻辑

getSupportedAnnotationTypes()

返回注解处理器支持的注解类型,你可以认为这个方法的返回值是process方法的第一个参数值。这个方法也可以使用@SupportedAnnotationTypes注解来替代,二者等效

getSupportedSourceVersion()

指明支持代码生成的java版本。这个方法也可以使用@SupportedSourceVersion注解来替代,二者等效

注解处理器方法实现

推荐使用JavaPoet来完成新的class文件的生成

向javac声明这个注解处理器

在处理器模块代码根目录下(一般是src/main),创建resources/META-INF/services/javax.annotation.processing.Processor文件,在这个文件中声明你的注解器类路径

1
com.github.tedaliez.processor.NewIntentProcessor

换行可以声明下一个注解器的路径,这里我们只有一个。

编译,体验成果

具体的代码可以参考https://github.com/TedaLIEz/AptExample

注解处理器不能做(不建议)的事情

  1. 修改已有的类文件。注解处理器的初衷是希望用注解来生成一些新的类文件,修改已有的类文件可能会有办法,但属于特殊技巧
  2. 受1的限制,注解处理器并不能很好地实现面向切面编程,因为我们很难侵入到一个方法执行过程中加入一段我们的代码。

注解处理器的特性

注解处理器有下面几个特性:

  1. 提供编译期的错误检查(fail-fast).更容易暴露错误和调试,个人认为这是注解处理器解决问题的最大优势
  2. 提供了类似C++的模版能力,比C++的模版有着更好的错误信息
  3. 避开了运行期使用反射获取注解带来的性能问题

v8执行WebAssembly遇到的问题和解决方法

最近在 7.2 分支上做一些 WebAssembly 能力的验证, 但是发现了一些奇怪的现象, 具体的问题描述可以参考https://stackoverflow.com/questions/56428990/webassembly-instantiate-didnt-call-then-nor-catch-in-v8-embedded

简单来说,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
WebAssembly.instantiate(
new Uint8Array([
0,
97,
115,
109,
1,
0,
0,
0,
1,
8,
2,
96,
1,
127,
0,
96,
0,
0,
2,
8,
1,
2,
106,
115,
1,
95,
0,
0,
3,
2,
1,
1,
8,
1,
1,
10,
9,
1,
7,
0,
65,
185,
10,
16,
0,
11
]),
{
js: {
_: console.log("Called from WebAssembly Hello world")
}
}
)
.then(function(obj) {
console.log("Called with instance " + obj);
})
.catch(function(err) {
console.log("Called with error " + err);
});

上面这段代码既没有执行 then 函数, 又没有执行 catch 函数, 明显不符合 Web API 的预期.

问题的简单分析

为了分析这个问题, 我尝试在我自己编译的 d8 中执行上面这段脚本, d8 是 v8 编译时默认生成的一个可交互 JS 运行环境, 简单理解就是一个 JS 的 REPL

很神奇, 我居然之后的 REPL 中获得了 catch 的调用, 也就是说 d8 一次执行结束后也没有返回 Promise 的 resolve 或者 reject. 那么这里就需要查查 d8 实现的方式了

查阅d8.cc

直接来到src/d8.cc, 来到最关键的函数Shell::Main

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
int Shell::RunMain(Isolate* isolate, int argc, char* argv[], bool last_run) {
for (int i = 1; i < options.num_isolates; ++i) {
options.isolate_sources[i].StartExecuteInThread();
}
{
SetWaitUntilDone(isolate, false);
if (options.lcov_file) {
debug::Coverage::SelectMode(isolate, debug::Coverage::kBlockCount);
}
HandleScope scope(isolate);
Local<Context> context = CreateEvaluationContext(isolate);
bool use_existing_context = last_run && use_interactive_shell();
if (use_existing_context) {
// Keep using the same context in the interactive shell.
evaluation_context_.Reset(isolate, context);
}
{
Context::Scope cscope(context);
InspectorClient inspector_client(context, options.enable_inspector);
PerIsolateData::RealmScope realm_scope(PerIsolateData::Get(isolate));
options.isolate_sources[0].Execute(isolate); // 关键
CompleteMessageLoop(isolate); // 关键
}
if (!use_existing_context) {
DisposeModuleEmbedderData(context);
}
WriteLcovData(isolate, options.lcov_file);
}
CollectGarbage(isolate);
for (int i = 1; i < options.num_isolates; ++i) {
if (last_run) {
options.isolate_sources[i].JoinThread();
} else {
options.isolate_sources[i].WaitForThread();
}
}
CleanupWorkers();
return 0;
}

关键看这两行:

1
2
options.isolate_sources[0].Execute(isolate);  // 关键
CompleteMessageLoop(isolate); // 关键

options.isolate_sources[0]实际上是一个 SourceGroup 对象

SourceGroup::Execute 简单分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void SourceGroup::Execute(Isolate* isolate) {
bool exception_was_thrown = false;
for (int i = begin_offset_; i < end_offset_; ++i) {
// 参数检查, 这里略去

// Use all other arguments as names of files to load and run.
HandleScope handle_scope(isolate);
Local<String> file_name =
String::NewFromUtf8(isolate, arg, NewStringType::kNormal)
.ToLocalChecked();
Local<String> source = ReadFile(isolate, arg);
if (source.IsEmpty()) {
printf("Error reading '%s'\n", arg);
base::OS::ExitProcess(1);
}
Shell::set_script_executed();
if (!Shell::ExecuteString(isolate, source, file_name, Shell::kNoPrintResult,
Shell::kReportExceptions,
Shell::kProcessMessageQueue)) { // 关键
exception_was_thrown = true;
break;
}
}
if (exception_was_thrown != Shell::options.expected_to_throw) {
base::OS::ExitProcess(1);
}
}

再进去看Shell::ExecuteString

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
bool Shell::ExecuteString(Isolate* isolate, Local<String> source,
Local<Value> name, PrintResult print_result,
ReportExceptions report_exceptions,
ProcessMessageQueue process_message_queue) {
HandleScope handle_scope(isolate);
TryCatch try_catch(isolate);
try_catch.SetVerbose(true);

MaybeLocal<Value> maybe_result;
bool success = true;
{
PerIsolateData* data = PerIsolateData::Get(isolate);
Local<Context> realm =
Local<Context>::New(isolate, data->realms_[data->realm_current_]);
Context::Scope context_scope(realm);
MaybeLocal<Script> maybe_script;
Local<Context> context(isolate->GetCurrentContext());
ScriptOrigin origin(name);

//  部分代码略去

Local<Script> script;
if (!maybe_script.ToLocal(&script)) {
// Print errors that happened during compilation.
if (report_exceptions) ReportException(isolate, &try_catch);
return false;
}

// 部分代码略去
maybe_result = script->Run(realm);
if (options.code_cache_options ==
ShellOptions::CodeCacheOptions::kProduceCacheAfterExecute) {
// Serialize and store it in memory for the next execution.
ScriptCompiler::CachedData* cached_data =
ScriptCompiler::CreateCodeCache(script->GetUnboundScript());
StoreInCodeCache(isolate, source, cached_data);
delete cached_data;
}
if (process_message_queue && !EmptyMessageQueues(isolate)) success = false; // 关键
data->realm_current_ = data->realm_switch_;
}
Local<Value> result;
if (!maybe_result.ToLocal(&result)) {
DCHECK(try_catch.HasCaught());
// Print errors that happened during execution.
if (report_exceptions) ReportException(isolate, &try_catch);
return false;
}
DCHECK(!try_catch.HasCaught());
if (print_result) {
if (options.test_shell) {
if (!result->IsUndefined()) {
// If all went well and the result wasn't undefined then print
// the returned value.
v8::String::Utf8Value str(isolate, result);
fwrite(*str, sizeof(**str), str.length(), stdout);
printf("\n");
}
} else {
v8::String::Utf8Value str(isolate, Stringify(isolate, result));
fwrite(*str, sizeof(**str), str.length(), stdout);
printf("\n");
}
}
return success;
}

看着就跟官方 demo 里面执行 hello world 差不多, 但是其中的一行有点令人在意.

1
if (process_message_queue && !EmptyMessageQueues(isolate)) success = false;

这个 EmptyMessageQueues 是什么意思?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
bool Shell::EmptyMessageQueues(Isolate* isolate) {
return ProcessMessages(
isolate, []() { return platform::MessageLoopBehavior::kDoNotWait; });
}

bool ProcessMessages(
Isolate* isolate,
const std::function<platform::MessageLoopBehavior()>& behavior) {
while (true) {
i::Isolate* i_isolate = reinterpret_cast<i::Isolate*>(isolate);
i::SaveContext saved_context(i_isolate);
i_isolate->set_context(i::Context());
SealHandleScope shs(isolate);
while (v8::platform::PumpMessageLoop(g_default_platform, isolate,
behavior())) {
isolate->RunMicrotasks();
}
if (g_default_platform->IdleTasksEnabled(isolate)) {
v8::platform::RunIdleTasks(g_default_platform, isolate,
50.0 / base::Time::kMillisecondsPerSecond);
}
HandleScope handle_scope(isolate);
PerIsolateData* data = PerIsolateData::Get(isolate);
Local<Function> callback;
if (!data->GetTimeoutCallback().ToLocal(&callback)) break;
Local<Context> context;
if (!data->GetTimeoutContext().ToLocal(&context)) break;
TryCatch try_catch(isolate);
try_catch.SetVerbose(true);
Context::Scope context_scope(context);
if (callback->Call(context, Undefined(isolate), 0, nullptr).IsEmpty()) {
Shell::ReportException(isolate, &try_catch);
return false;
}
}
return true;
}

这里的 ProcessMessages 就很有意思了.

首先是一个死循环, 接着在这个死循环里面我们有另外一个 while 循环

1
2
3
4
while (v8::platform::PumpMessageLoop(g_default_platform, isolate,
behavior())) {
isolate->RunMicrotasks();
}

这里behavior是外部传入的 lambda, 表示 PumpMessageLoop 的等待参数, 我们看一下这个参数的含义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

/**
* Pumps the message loop for the given isolate.
*
* The caller has to make sure that this is called from the right thread.
* Returns true if a task was executed, and false otherwise. Unless requested
* through the |behavior| parameter, this call does not block if no task is
* pending. The |platform| has to be created using |NewDefaultPlatform|.
*/
V8_PLATFORM_EXPORT bool PumpMessageLoop(
v8::Platform* platform, v8::Isolate* isolate,
MessageLoopBehavior behavior = MessageLoopBehavior::kDoNotWait);

enum class MessageLoopBehavior : bool {
kDoNotWait = false,
kWaitForWork = true
};

/**
* Runs the Microtask Work Queue until empty
* Any exceptions thrown by microtask callbacks are swallowed.
*/
void RunMicrotasks();

根据定义, kDoNotWait 为默认属性, 表示不等待消息到达, 即表示 PumpMessageLoop 如果没有任务, 不会阻塞线程. 返回 true 表示有任务被执行, false 表示没有.

刚才的 EmptyMessageQueues 函数中直接返回了 kDoNotWait, 也就是说只要没有消息, 就直接返回 false, 继续执行代码; 如果为 true, 则将微队列任务全部执行.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (g_default_platform->IdleTasksEnabled(isolate)) {
v8::platform::RunIdleTasks(g_default_platform, isolate,
50.0 / base::Time::kMillisecondsPerSecond);
}
HandleScope handle_scope(isolate);
PerIsolateData* data = PerIsolateData::Get(isolate);
Local<Function> callback;
if (!data->GetTimeoutCallback().ToLocal(&callback)) break;
Local<Context> context;
if (!data->GetTimeoutContext().ToLocal(&context)) break;
TryCatch try_catch(isolate);
try_catch.SetVerbose(true);
Context::Scope context_scope(context);
if (callback->Call(context, Undefined(isolate), 0, nullptr).IsEmpty()) {
Shell::ReportException(isolate, &try_catch);
return false;
}

默认不开启 IdleTasksEnabled, 这里暂时跳过. 后面这段代码结合 d8 源码来看, 其目的就是尝试去获取 Isolate 中是否还有之前通过 timeout 函数设置的 callback, 如果有的话就尝试执行, 如果已经没有则 break 掉 while(true)循环

总结一下, 在 ExecuteString 函数中, 会以 kDoNotWait 方式去清空微队列中的任务, 并且执行所有的 timeout 回调

CompleteMessageLoop 分析

RunMain 中的第二行关键调用就是 CompleteMessageLoop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void Shell::CompleteMessageLoop(Isolate* isolate) {
auto get_waiting_behaviour = [isolate]() {
base::MutexGuard guard(isolate_status_lock_.Pointer());
DCHECK_GT(isolate_status_.count(isolate), 0);
i::Isolate* i_isolate = reinterpret_cast<i::Isolate*>(isolate);
i::wasm::WasmEngine* wasm_engine = i_isolate->wasm_engine();
bool should_wait = (options.wait_for_wasm &&
wasm_engine->HasRunningCompileJob(i_isolate)) ||
isolate_status_[isolate];
return should_wait ? platform::MessageLoopBehavior::kWaitForWork
: platform::MessageLoopBehavior::kDoNotWait;
};
ProcessMessages(isolate, get_waiting_behaviour);
}

这里居然直接写了一段跟 WASM 强相关的逻辑:

1
2
3
bool should_wait = (options.wait_for_wasm &&
wasm_engine->HasRunningCompileJob(i_isolate)) ||
isolate_status_[isolate];

wait_for_wasm默认为true, 也就是说, 只要 HasRunningCompileJob 也为 true, 我们投递给 PumpMessageLoop 的 MessageLoopBehavior 即 kWaitForWork.

当 MessageLoopBehavior 为 kWaitForWork 时, v8 会等待任务执行结束, 没有任务则会直接阻塞线程.

虽然还没有具体分析, 但目前可以推测 WASM 在 7.2 这个版本编译的实现变成了一个异步任务, 而外部如果想要知道这个执行结束的消息一定会依赖 Promise#resolve 方式来执行, d8 采用了 kWaitForWork 的方式来确保线程能够等待到 WASM 编译结束, 调用其对应的 resolve 或 reject 函数. (这里还需要再具体地根据源码分析, 如果以后被打脸了就再修改这里的分析内容)

解决方案

d8 中对 wasm 的处理方式对我们是否有所启发呢?
对比一下我和 d8 的代码, 很快就可以得出结论了.

在我的代码中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// sample wasm javascript code here.
const char *csource = R"(
WebAssembly.instantiate(new Uint8Array([0,97,115,109,1,0,0,0,1,8,2,96,1,127,0,96,0,0,2,8,1,2,106,
115,1,95,0,0,3,2,1,1,8,1,1,10,9,1,7,0,65,185,10,16,0,11]),
{js:{_:console.log('Called from WebAssembly Hello world')}}).then(function(obj) {
log('Called with instance ' + obj);
}).catch(function(err) {
log('Called with error ' + err);
});
)"; // should call my Hello World log and trigger the error or return the instance successfully

v8::HandleScope handle_scope(isolate);
auto ctx = persistentContext.Get(isolate);
v8::Context::Scope context_scope(ctx);
v8::TryCatch try_catch(isolate);
v8::Local<v8::String> source = v8::String::NewFromUtf8(isolate, csource,
v8::NewStringType::kNormal).ToLocalChecked();

v8::Local<v8::Script> script =
v8::Script::Compile(ctx, source).ToLocalChecked();
v8::Local<v8::Value> result;
if (!script->Run(ctx).ToLocal(&result)) {
ReportException(isolate, &try_catch); // report exception, ignore the implementation here
return;
}
// Convert the result to an UTF8 string and print it.
v8::String::Utf8Value utf8(isolate, result);
__android_log_print(ANDROID_LOG_INFO, "V8Native", "%s\n", *utf8);

当执行这段代码时, script->Run(ctx)可以理解为 REPL 中的 Evaluate 环节,
__android_log_print(ANDROID_LOG_INFO, "V8Native", "%s\n", *utf8)可以理解为 REPL 中的 Print 环节.

但这里我们少了个 Loop, 这里引入了问题. WebAssembly.instantiate 的异步编译导致我们不再有机会执行异步编译成功/失败后的 resolve 或者 reject 方法, 我们没有等待 WebAssembly 执行结束就直接返回了.

那么如何解决这个问题呢?

最暴力的方案就是直接在我们的函数结尾加上一段:

1
2
3
while (v8::platform::PumpMessageLoop(platform, isolate, v8::platform::MessageLoopBehavior::kWaitForWork)) {
isolate->RunMicrotasks();
}

这样我们会强制要求等待所有的消息到达并全部执行后才会结束, 这样解决了我的 demo 问题, 但这个方式显然不解决通用场景.

v8::platform::MessageLoopBehavior::kWaitForWork表示如果没有消息了, 那么 v8 会选择阻塞线程来等待, 你可以类比这个为 epoll_wait(), 那么我们如果有一段 js 代码, 例如:

1
console.log("Hello world");

这段代码本身不涉及任何微队列或异步问题, Run 结束后就应该立刻返回, 但是 kWaitForWork 会告诉 v8, 没有消息也必须等待, 那么我们的函数也就永远不会返回了.

实际上比较合理的做法应该是在你的 JS 执行线程中进行一个固定的 timer 操作, 类似 node.js 中的 Event Loop 去固定触发一个定时器, 这个定时器一方面要及时去触发 setTimeout 或 setInterval 之类的定时操作, 另一方面则是需要让 v8 及时地去刷新自己的消息队列并执行微队列任务, 例如:

1
2
3
4
5
6
void eventLoop() {
while (v8::platform::PumpMessageLoop(platform, isolate)) {
isolate->RunMicrotasks();
}
m_spTimer->updateCallback();
}

这样就解决了我的问题, 我的 WASM 也成功地在 Android 设备上执行了.这篇文章有一些自己的主观臆断, 如果有与我分析不一致的情况, 欢迎通过邮件或 github issues 的方式讨论.

v8 Android交叉编译

编译准备

整个编译考虑到网络问题,强烈建议使用VPS的方式在服务器上进行编译。

编译

第一个难关就是代码的获取,由于v8整个工具链比较长,这里建议直接用Proxifier设置代理后下载. Proxifier的配置这里不赘述. 整个过程都需要代理

安装好Git和depot_tools后

1
2
3
4
5
6
7
8
mkdir ~/v8
cd ~/v8
fetch v8
cd v8

alias gm=/path/to/v8/tools/dev/gm.py

gm x64.release

参考 https://v8.dev/docs/build-gn 来做一些编译定制化

Android端交叉编译

遵循上述步骤后, 来到~/v8你会看到一个.glient文件, 修改文件为

1
2
3
4
5
6
7
8
9
10
 solutions = [
{
"url": "https://chromium.googlesource.com/v8/v8.git",
"managed": False,
"name": "v8",
"deps_file": "DEPS",
"custom_deps": {},
},
];
target_os = ["android", "unix"];

之后gclient sync来获取Android交叉编译需要的工具

开始编译

这里我们使用gn工具进行编译,下面贴以下我的gn编译的配置

tools/dev/v8gen.py gen -m client.v8.ports -b “V8 Android Arm - builder” android.arm.release

1
2
3
4
5
6
7
8
9
10
11
12
android_unstripped_runtime_outputs = true
v8_use_external_startup_data = false
is_debug = false
symbol_level = 1
target_cpu = "arm"
target_os = "android"
use_goma = false
v8_enable_i18n_support = false
v8_static_library = true
is_component_build = false
v8_monolithic = true
v8_android_log_stdout = true

其中v8_monolithic表示将静态库编译为一个文件,而非几个单独文件

之后ninja -C android.arm.release

编译成功后,在android.arm.release/obj找到libv8_monolith.a

Android 项目配置

我们需要把v8的头文件include进来,同时配置CMakeLists.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
add_library(v8 STATIC IMPORTED)
set_target_properties( v8 PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}/libv8_monolith.a)

add_library( # Sets the name of the library.
native-lib

# Sets the library as a shared library.
SHARED

# Provides a relative path to your source file(s).
${CMAKE_SOURCE_DIR}/src/main/cpp/native-lib.cpp)

target_include_directories( native-lib PRIVATE ${CMAKE_SOURCE_DIR}/libs/include)

branch_head/7.2分支上测试成功

具体的代码可以参考https://github.com/TedaLIEz/V8AndroidEmbedding

P.S

将7.4之后的v8编译,就会遇到一个错误

1
2
3
4
../../buildtools/third_party/libc++/trunk/include/string:2724: error: undefined reference to 'std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >::insert(unsigned int, char const*, unsigned int)'
/Users/JianGuo/AndroidProject/V8DemoApplication/app/src/main/cpp/native-lib.cpp:25: error: undefined reference to 'v8::platform::NewDefaultPlatform(int, v8::platform::IdleTaskSupport, v8::platform::InProcessStackDumping, std::__ndk1::unique_ptr<v8::TracingController, std::__ndk1::default_delete<v8::TracingController> >)'
clang++: error: linker command failed with exit code 1 (use -v to see invocation)
ninja: build stopped: subcommand failed.

看日志,我们似乎编译出了一个错误的命名空间。那么我们来检查一下我们的静态库的命名空间

objdump -D app/libs/armeabi-v7a/libv8_monolith.a | grep NewDefault

得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Disassembly of section .text._ZN2v88platform18NewDefaultPlatformEiNS0_15IdleTaskSupportENS0_21InProcessStackDumpingENSt3__110unique_ptrINS_17TracingControllerENS3_14default_deleteIS5_EEEE:
_ZN2v88platform18NewDefaultPlatformEiNS0_15IdleTaskSupportENS0_21InProcessStackDumpingENSt3__110unique_ptrINS_17TracingControllerENS3_14default_deleteIS5_EEEE:
4: 81 b0 01 2b blhs #442884 <_ZN2v88platform18NewDefaultPlatformEiNS0_15IdleTaskSupportENS0_21InProcessStackDumpingENSt3__110unique_ptrINS_17TracingControllerENS3_14default_deleteIS5_EEEE+0x6C210>
Disassembly of section .rel.text._ZN2v88platform18NewDefaultPlatformEiNS0_15IdleTaskSupportENS0_21InProcessStackDumpingENSt3__110unique_ptrINS_17TracingControllerENS3_14default_deleteIS5_EEEE:
.rel.text._ZN2v88platform18NewDefaultPlatformEiNS0_15IdleTaskSupportENS0_21InProcessStackDumpingENSt3__110unique_ptrINS_17TracingControllerENS3_14default_deleteIS5_EEEE:
Disassembly of section .ARM.exidx.text._ZN2v88platform18NewDefaultPlatformEiNS0_15IdleTaskSupportENS0_21InProcessStackDumpingENSt3__110unique_ptrINS_17TracingControllerENS3_14default_deleteIS5_EEEE:
.ARM.exidx.text._ZN2v88platform18NewDefaultPlatformEiNS0_15IdleTaskSupportENS0_21InProcessStackDumpingENSt3__110unique_ptrINS_17TracingControllerENS3_14default_deleteIS5_EEEE:
Disassembly of section .rel.ARM.exidx.text._ZN2v88platform18NewDefaultPlatformEiNS0_15IdleTaskSupportENS0_21InProcessStackDumpingENSt3__110unique_ptrINS_17TracingControllerENS3_14default_deleteIS5_EEEE:
.rel.ARM.exidx.text._ZN2v88platform18NewDefaultPlatformEiNS0_15IdleTaskSupportENS0_21InProcessStackDumpingENSt3__110unique_ptrINS_17TracingControllerENS3_14default_deleteIS5_EEEE:
Disassembly of section .text._ZN2v811ArrayBuffer9Allocator19NewDefaultAllocatorEv:
_ZN2v811ArrayBuffer9Allocator19NewDefaultAllocatorEv:
Disassembly of section .rel.text._ZN2v811ArrayBuffer9Allocator19NewDefaultAllocatorEv:
.rel.text._ZN2v811ArrayBuffer9Allocator19NewDefaultAllocatorEv:
Disassembly of section .ARM.exidx.text._ZN2v811ArrayBuffer9Allocator19NewDefaultAllocatorEv:
.ARM.exidx.text._ZN2v811ArrayBuffer9Allocator19NewDefaultAllocatorEv:
Disassembly of section .rel.ARM.exidx.text._ZN2v811ArrayBuffer9Allocator19NewDefaultAllocatorEv:
.rel.ARM.exidx.text._ZN2v811ArrayBuffer9Allocator19NewDefaultAllocatorEv:

其中得到这么一串粉碎命名:

_ZN2v88platform18NewDefaultPlatformEiNS0_15IdleTaskSupportENS0_21InProcessStackDumpingENSt3__110unique_ptrINS_17TracingControllerENS3_14default_deleteIS5_EEEE

通过demangle后得到:

v8::platform::NewDefaultPlatform(int, v8::platform::IdleTaskSupport, v8::platform::InProcessStackDumping, std::__1::unique_ptr<v8::TracingController, std::__1::default_delete<v8::TracingController> >)
也就是说,我们的静态库编译出了std::1::unique_ptr,但是ld时却寻找std::ndk1::unique_ptr。

继续看日志,发现编译时尝试寻找../../buildtools/third_party/libc++/trunk/include/string:2724, 不妨去v8的源码中看一下。来到buildtools/third_party/libc++/trunk/__config:

1
2
3
4
5
6
7
#ifndef _LIBCPP_ABI_NAMESPACE
# define _LIBCPP_ABI_NAMESPACE _LIBCPP_CONCAT(__,_LIBCPP_ABI_VERSION)
#endif
#define _LIBCPP_BEGIN_NAMESPACE_STD namespace std { inline namespace _LIBCPP_ABI_NAMESPACE {
#define _LIBCPP_END_NAMESPACE_STD } }
#define _VSTD std::_LIBCPP_ABI_NAMESPACE
_LIBCPP_BEGIN_NAMESPACE_STD _LIBCPP_END_NAMESPACE_STD

这里我们展开后其实就会得到:

1
2
3
4
5
namespace std {
inline namespace __1 {

}
}

对比以下ndk中的__config:

thrid_party/android_ndk/cxx-stl/llvm-libc++/include/__config

1
2
3
4
5
6
7
8
9
#define _LIBCPP_NAMESPACE _LIBCPP_CONCAT(__ndk,_LIBCPP_ABI_VERSION)
#define _LIBCPP_BEGIN_NAMESPACE_STD namespace std {inline namespace _LIBCPP_NAMESPACE {
#define _LIBCPP_END_NAMESPACE_STD } }
#define _VSTD std::_LIBCPP_NAMESPACE

namespace std {
inline namespace _LIBCPP_NAMESPACE {
}
}

这里展开后为:

1
2
3
4
namespace std {
inline namespace __ndk1 {
}
}

这里显然是不一样的。也就是说ndk的libc++实际上更改了命名空间到__ndk这个前缀上,而v8进行编译时使用的libc++是
https://chromium.googlesource.com/chromium/llvm-project/libcxx
二者的命名空间显然是不同的。但为什么我编译使用的是buildtools/third_party/libc++而非thrid_party/android_ndk/cxx-stl/llvm-libc++?这个我还没有答案。

WebAssembly初探

开发环境演进

目前稳定的webassembly的编译方式都需要通过asm.js来生成wasm(hacking)。LLVM WebAssembly 后端目前还处在研发过程中,没有稳定的版本。

整个开发环境的演变可以参考下面这两个ppt
https://kripken.github.io/talks/emwasm.html

http://kripken.github.io/talks/wasm.html

开发环境

目前最常用的开发工具链是emscripten,emscripten可以将LLVM bitcode编译为javascript(LLVM to web compiler)

使用Emscripten可以:

  1. 编译C和C++代码为javascript
  2. 将任何可以编译为LLVM bitcode的代码编译为javascript
  3. 编译C/C++ runtimes 代码为javascript

整个工具已经有了许多大型应用,可以认为是比较稳定的工具链 https://github.com/emscripten-core/emscripten/wiki/Porting-Examples-and-Demos

Emscripten同时也支持WebAssembly,默认通过asm2wasm将asm.js转为wasm https://github.com/WebAssembly/binaryen#cc-source--asm2wasm--webassembly

Emscripten同时也提供了一些标准库的实现和WebAssembly能力的封装,减少我们配置WASM的负担。当然你也可以选择不使用Emscripten提供的能力,采用最原始的方式操作WASM

Emscripten工具链简述

emcc使用clang将C/C++转为LLVMbitcode,然后使用Emscripten自己的编译器后端Fastcomp将bitcode转为javascript https://emscripten.org/docs/compiling/WebAssembly.html#llvm-webassembly-backend

实战

1
2
3
Aside: When you compile C/C++ normally, you link with the system to provide implementations of the standard library methods your code uses.

JavaScript doesn't have these methods—either not with the same signatures or names (e.g, Math.atan in JavaScript vs atan in C), or because it's conceptually different (think malloc vs JavaScript's objects and garbage collection)—so Emscripten has to provide them for you.

ONLY_MY_CODE 标志位用来表示你在编译不需要emcc提供的一些stardard库函数

但是一般我们都是需要emcc提供的标准库函数的,不然我们连下面这个hello world代码都编译不出来

1
2
3
4
5
6
#include <iostream>

int main(int, char**) {
std::cout << "Hello, world!\n";
return 0;
}

emcc -o output.js *.cpp -s WASM=1 -s ONLY_MY_CODE=1会提示:

script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
error: undefined symbol: _ZNKSt3__26locale9use_facetERNS0_2idE
warning: To disable errors for undefined symbols use `-s ERROR_ON_UNDEFINED_SYMBOLS=0`
error: undefined symbol: _ZNKSt3__28ios_base6getlocEv
error: undefined symbol: _ZNSt3__212basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE6__initEmc
error: undefined symbol: _ZNSt3__212basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEED1Ev
error: undefined symbol: _ZNSt3__213basic_ostreamIcNS_11char_traitsIcEEE6sentryC1ERS3_
error: undefined symbol: _ZNSt3__213basic_ostreamIcNS_11char_traitsIcEEE6sentryD1Ev
error: undefined symbol: _ZNSt3__26localeD1Ev
error: undefined symbol: _ZNSt3__28ios_base5clearEj
error: undefined symbol: _ZSt9terminatev
error: undefined symbol: __cxa_begin_catch
error: undefined symbol: __gxx_personality_v0
error: undefined symbol: strlen
error: undefined symbol: _ZNSt3__24coutE
error: undefined symbol: _ZNSt3__25ctypeIcE2idE
Error: Aborting compilation due to previous errors

理由也很简单,因为iostream中用到的标准api没有找到实现

EXPORTED_FUNCTIONS表示我们希望从js能访问的函数,这个宏一般是我们编译希望不依赖Emscripten library的代码时用到。如果用了Emscripten提供的能力,不需要在编译参数里声明这个参数

JS中加载wasm的方式

1
2
3
4
5
async function createWebAssembly(path, importObject) {
const result = await window.fetch(path);
const bytes = await result.arrayBuffer();
return WebAssembly.instantiate(bytes, importObject);
}

这里有个很关键的概念,importObject

API文档对这个对象的定义是一个包含需要被import的对象,可以理解为被加载wasm需要的上下文,而这个上下文是非常基础的,可以包含内存大小的定义。

一个典型的importObject如下

1
2
3
4
5
6
7
8
9
10
11
const memory = new WebAssembly.Memory({initial: 256, maximum: 256});
const env = {
'abortStackOverflow': _ => { throw new Error('overflow'); },
'table': new WebAssembly.Table({initial: 0, maximum: 0, element: 'anyfunc'}), // wasm调用JS里的方法的函数表
'__table_base': 0,
'memory': memory,
'__memory_base': 1024,
'STACKTOP': 0,
'STACK_MAX': memory.buffer.byteLength,
};
const importObject = {env};

可以看到这个importObject配置了内存相关的内容,wasm的内存模型也是一个线性模型, 配置项配置了整个内存的大小,并区分了栈和堆的内存地址

上面的方式下,你不能用C/C++标准库的API,但好在Emscripten能够提供这些标准库的实现。Emscripten能够提供下面的能力:

  1. 标准库方法,例如malloc,free等
  2. 利用Emscripten Embind的能力将C++的函数和类暴露到js中

JS中调用wasm中的函数或类

调用函数

原生实现

这种方式下我们不需要emscripten提供的能力来调用到wasm中的函数

1
2
const wa = await createWebAssembly('output.wasm', importObject);
wa.instance.exports.fun();
Embind能力实现

emcc编译时默认会生成一个wasm文件和一个js文件,这个js文件就提供了相关的绑定能力,而我们在C++代码中则需要使用EMSCRIPTEN_BINDINGS这个宏

例如:

1
2
3
4
5
6
7
8
#include <emscripten/bind.h>

emscripten::val mandelbrot(int w, int h, double zoom, double moveX, double moveY);

EMSCRIPTEN_BINDINGS(hello)
{
emscripten::function("mandelbrot", &mandelbrot);
}

我们可以在js中调用函数mandelbrot

1
2
3
4
<script src="mandelbrot.js"></script>
<script>
const mandelbrot = Module.mandelbrot(width, 1, -0.5, 0);
</script>

cwrap或ccall实现调用

这种方式依赖cwrap这个emscripten的api来实现

1
2
3
4
5
6
7
#include "emscripten.h"
#include <stdlib.h>

EMSCRIPTEN_KEEPALIVE
uint8_t* create_buffer(int width, int height) {
return malloc(width * height * 4 * sizeof(uint8_t));
}

编译时使用命令emcc -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["create_buffer"]' lib.c

1
2
3
4
5
6
7
<script src="lib.js"></script>
<script>
Module.onRuntimeInitialized = _ => {
const fib = Module.cwrap('create_buffer', 'number', ['number']);
console.log(fib(12));
};
</script>

EMSCRIPTEN_KEEPALIVE表示提示编译器在优化过程中不要删去函数的代码,因为这个函数后续会被js调用

引用一个类

利用Embind,在JS代码中引用c++里的一个类

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <emscripten/bind.h>
class Mandelbrot {
public:
emscripten::val nextTile();
Mandelbrot(int width, int height, double zoom, double moveX, double moveY)
: width(width), height(height), zoom(zoom), moveX(moveX), moveY(moveY) {}
};

EMSCRIPTEN_BINDINGS(hello) {
emscripten::class_<Mandelbrot>("Mandelbrot")
.constructor<int, int, double, double, double>()
.function("nextTile", &Mandelbrot::nextTile);
}
1
2
3
4
5
6
<script src="mandelbrot.js"></script>
<script>
const mandelbrot = new Module.Mandelbrot(width, height,
1, -0.5, 0);
const tile = mandelbrot.nextTile();
</script>

TypeScript中this的问题

typescript中对于this的转译有点意思,例如下面一段代码

1
2
3
4
5
6
7
8
9
10
11
12
class Foo {
bar = 123;
bas() {
setTimeout(function() {
console.log('this is', this);
console.log(this.bar);
}, 100);
}
}

var foo = new Foo();
foo.bas();

这段代码中,this将会指向settimeout的this,即window,那么this.bar就会是undefined

转译结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var Foo = (function () {
function Foo() {
this.bar = 123;
}
Foo.prototype.bas = function() {
setTimeout(function () {
console.log('this is', this);
console.log(this.bar);
}, 100);
}
return Foo;
})();

var foo = new Foo();
foo.bas();

为了解决这个问题,最快的一个方法就是使用ts的lambda表达式

1
2
3
4
5
6
7
8
9
10
11
12
class Foo {
bar = 123;
bas() {
setTimeout(() => {
console.log('this is', this);
console.log(this.bar);
}, 100);
}
}

var foo = new Foo();
foo.bas();

转译结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var Foo = (function () {
function foo() {
this.bar = 123;
}
Foo.prototype.bas = function() {
var _this = this; // tricky part
setTimeout(function () {
console.log('this is', _this);
console.log(_this.bar);
}, 100);
};
return Foo;
})();

var foo = new Foo();
foo.bas();

类似的还有

1
2
3
4
5
6
7
8
9
10
11
class Foo {
bar = 123;
bas() {
console.log('this is', this);
console.log(this.bar);
}
}

var foo = new Foo();
var bas = foo.bas;
bas();

这里我们把Foo的成员函数传给了bas变量,然后bas变量在外部调用。这个类似的场景还有将一个类的成员变量函数作为参数传给外部(回调等场景)

转译结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var Foo = (function () {
function foo() {
this.bar = 123;
}

Foo.prototype.bas = function() {
console.log('this is', this);
console.log(this.bar);
};
return bas;
})();

var foo = new Foo();
var bas = foo.bas;
bas();

这个时候,bas调用的this将会指向全局变量,即window

解决方案同样是使用lambda表达式

1
2
3
4
5
6
7
8
9
10
11
12
class Foo {
bar = 123;

bas = () => {
console.log('this is', this);
console.log(this.bar);
}
}

var foo = new Foo();
var bas = foo.bas;
bas();

转译结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var Foo = (function () => {
function Foo() {
var _this = this;
this.bar = 123;

this.bas = function() {
console.log('this is', _this);
console.log(_this.bar);
};
}
return foo;
})();

var foo = new Foo();

var bas = foo.bas;
bas();

NDK学习笔记

记录一下自己NDK开发过程中零散的学习记录

如何让CMake下的NDK build打开verbose开关

1
2
3
4
5
6
externalNativeBuild {
cmake {
cppFlags "-std=c++11"
arguments "-DCMAKE_VERBOSE_MAKEFILE:BOOL=ON" // 关键
}
}

如何在externalNativeBuild中传递linker flags

The -Wl,xxx option for gcc passes a comma-separated list of tokens as a space-separated list of arguments to the linker. So

1
gcc -Wl,aaa,bbb,ccc

eventually becomes a linker call

1
ld aaa bbb ccc

The idea is that you build modules in CMake, and link them together. Let’s ignore header files for now, as they can be all included in your source files.

Say you have file1.cpp, file2.cpp, main.cpp. You add them to your project with:

1
2
3
4
ADD_LIBRARY(LibsModule 
file1.cpp
file2.cpp
)

Now you added them to a module called LibsModule. Keep that in mind. Say you want to link to pthread for example that’s already in the system. You can combine it with LibsModule using the command:

1
target_link_libraries(LibsModule -lpthread)

And if you want to link a static library to that too, you do this:

1
target_link_libraries(LibsModule liblapack.a)

And if you want to add a directory where any of these libraries are located, you do this:

1
target_link_libraries(LibsModule -L/home/user/libs/somelibpath/)

Now you add an executable, and you link it with your main file:

1
ADD_EXECUTABLE(MyProgramExecBlaBla main.cpp)

(I added BlaBla just to make it clear that the name is custom). And then you link LibsModule with your executable module MyProgramExecBlaBla

1
target_link_libraries(MyProgramExecBlaBla LibsModule)

And this will do it.

What I see in your CMake file is a lot of redundancy. For example, why do you have texture_mapping, which is an executable module in your include directories? So you need to clean this up and follow the simple logic I explained. Hopefully it works.

In summary, it looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
project (MyProgramExecBlaBla)  #not sure whether this should be the same name of the executable, but I always see that "convention"
cmake_minimum_required(VERSION 2.8)

ADD_LIBRARY(LibsModule
file1.cpp
file2.cpp
)

target_link_libraries(LibsModule -lpthread)
target_link_libraries(LibsModule liblapack.a)
target_link_libraries(LibsModule -L/home/user/libs/somelibpath/)
ADD_EXECUTABLE(MyProgramExecBlaBla main.cpp)
target_link_libraries(MyProgramExecBlaBla LibsModule)

The most important thing to understand is the module structure, where you create modules and link them all together with your executable. Once this works, you can complicate your project further with more details. Good luck!

AOSP MacOS编译问题总结

  1. AS反复提示indexing

    https://github.com/flutter/flutter-intellij/issues/1735#issuecomment-376597481 在flutter中找到一个办法,在AS 3.1中对应的iml配置项下加一行

    1
    <option name="ALLOW_USER_CONFIGURATION" value="false" />
  2. “sed: illegal option – r”

    mac上安装brew install gnu-sed --with-default-names之后export PATH="/usr/local/opt/gnu-sed/libexec/gnubin:$PATH"

  3. repo init -u https://github.com/LineageOS/android.git -b lineage-15.2

  4. 获取 proprietary blobs的相关文件,在.repo/local_manifests/roomservice.xml(如果没有手动创建一个),

    1
    2
    3
    4
    5
    6
    7
    8
    <?xml version="1.0" encoding="UTF-8"?>
    <manifest>
    <project name="LineageOS/android_device_oneplus_oneplus3" path="device/oneplus/oneplus3" remote="github" />
    <project name="LineageOS/android_kernel_oneplus_msm8996" path="kernel/oneplus/msm8996" remote="github" />
    <project name="LineageOS/android_packages_resources_devicesettings" path="packages/resources/devicesettings" remote="github" />
    <project depth="1" name="TheMuppets/proprietary_vendor_oneplus" path="vendor/oneplus" />
    <project name="LineageOS/android_device_oppo_common" path="device/oppo/common" remote="github" />
    </manifest>
  5. repo sync

  6. 未完待续,后续补充