Kotlin 源代码编译过程分析

Kotlin 源代码编译过程分析

我们知道,Kotlin基于Java虚拟机(JVM),通过Kotlin编译器生成的JVM字节码与Java编译的字节码基本相同,也因此与Java可以完全兼容,并且语法更加简洁,让我对Kotlin的编译过程甚是好奇。一通Google之后,毫无收获,Kotlin作为一门新语言,绝大多数的资料都局限于它的用法和特性相关。幸好Kotlin所有源码都已开源,遂决定生啃之。
Kotlin源码传送门:https://github.com/JetBrains/kotlin
在具体讲Kotlin编译过程之前,我们先来看一张图。


上图是Java编译器的编译过程,正如它们俩完全兼容的特性一样,等分析完Kotlin的编译过程,你会发现,Kotlin和Java的编译过程也是很相似的。

  1. 编译入口
    整个Kotlin工程代码达到200多MB,面对如此巨大的项目,我们需要找一个入口来进行逐步深入。所以,我们从最简单直观的入手,来看一下Kotlin的编译命令:
    kotlinc Hello.kt

打开kotlinc
脚本文件看执行了什么,代码如下:

$ cat /Users/jack/soft/kotlinc/bin/kotlinc
#!/usr/bin/env bash
#
##############################################################################
# Copyright 2002-2011, LAMP/EPFL
# Copyright 2011-2015, JetBrains
#
# This is free software; see the distribution for copying conditions.
# There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE.
##############################################################################

cygwin=false;
case "`uname`" in
    CYGWIN*) cygwin=true ;;
esac

# Based on findScalaHome() from scalac script
findKotlinHome() {
    local source="${BASH_SOURCE[0]}"
    while [ -h "$source" ] ; do
        local linked="$(readlink "$source")"
        local dir="$(cd -P $(dirname "$source") && cd -P $(dirname "$linked") && pwd)"
        source="$dir/$(basename "$linked")"
    done
    (cd -P "$(dirname "$source")/.." && pwd)
}

KOTLIN_HOME="$(findKotlinHome)"

if $cygwin; then
    # Remove spaces from KOTLIN_HOME on windows
    KOTLIN_HOME=`cygpath --windows --short-name "$KOTLIN_HOME"`
fi

[ -n "$JAVA_OPTS" ] || JAVA_OPTS="-Xmx256M -Xms32M"

declare -a java_args
declare -a kotlin_args

while [ $# -gt 0 ]; do
  case "$1" in
    -D*)
      java_args=("${java_args[@]}" "$1")
      shift
      ;;
    -J*)
      java_args=("${java_args[@]}" "${1:2}")
      shift
      ;;
    *)
      kotlin_args=("${kotlin_args[@]}" "$1")
      shift
      ;;
  esac
done

if [ -z "$JAVACMD" -a -n "$JAVA_HOME" -a -x "$JAVA_HOME/bin/java" ]; then
    JAVACMD="$JAVA_HOME/bin/java"
fi

declare -a kotlin_app

if [ -n "$KOTLIN_RUNNER" ];
then
    java_args=("${java_args[@]}" "-Dkotlin.home=${KOTLIN_HOME}")
    kotlin_app=("${KOTLIN_HOME}/lib/kotlin-runner.jar" "org.jetbrains.kotlin.runner.Main")
else
    [ -n "$KOTLIN_COMPILER" ] || KOTLIN_COMPILER=org.jetbrains.kotlin.cli.jvm.K2JVMCompiler
    java_args=("${java_args[@]}" "-noverify")

    kotlin_app=("${KOTLIN_HOME}/lib/kotlin-preloader.jar" "org.jetbrains.kotlin.preloading.Preloader" "-cp" "${KOTLIN_HOME}/lib/kotlin-compiler.jar" $KOTLIN_COMPILER)
fi

"${JAVACMD:=java}" $JAVA_OPTS "${java_args[@]}" -cp "${kotlin_app[@]}" "${kotlin_args[@]}"

从代码中找到了疑似编译部分的入口代码

declare -a kotlin_app

if [ -n "$KOTLIN_RUNNER" ];
then
    java_args=("${java_args[@]}" "-Dkotlin.home=${KOTLIN_HOME}")
    kotlin_app=("${KOTLIN_HOME}/lib/kotlin-runner.jar" "org.jetbrains.kotlin.runner.Main")
else
    [ -n "$KOTLIN_COMPILER" ] || KOTLIN_COMPILER=org.jetbrains.kotlin.cli.jvm.K2JVMCompiler
    java_args=("${java_args[@]}" "-noverify")

    kotlin_app=("${KOTLIN_HOME}/lib/kotlin-preloader.jar" "org.jetbrains.kotlin.preloading.Preloader" "-cp" "${KOTLIN_HOME}/lib/kotlin-compiler.jar" $KOTLIN_COMPILER)
fi

"${JAVACMD:=java}" $JAVA_OPTS "${java_args[@]}" -cp "${kotlin_app[@]}" "${kotlin_args[@]}"

紧跟跳转,到CLICompiler.doMain()方法中。

我们可以看到,编译器执行完编译过程以后,会返回一个退出码,返回OK即为编译成功,否则直接退出编译过程。好,知道了这些,我们继续往下看。跟着代码的跳转,跳转,又跳转,看到了关键的编译入口代码,泪流满面。

词法、语法分析、语义分析、目标代码生成等过程
找到运行主类
写入文件

  1. 编译过程
    Kotlin的整个编译过程大致有以下环节:
  2. 词法分析2. 语法分析3. 语义分析及中间代码生成4. 目标代码生成

其中,我们把词法分析、语法分析、语义分析及中间代码生成称之为编译器前段,将源程序翻译成中间代码;目标代码生成称之为编译器后端,负责将中间代码转换生成目标代码,与目标语言有关的细节尽可能放在了后端。
2.1 词法分析
词法分析是将源程序读入的字符序列,按照一定的规则转换成词法单元(Token)序列的过程。词法单元是语言中具有独立意义的最小单元,包括关键字、标识符、常数、运算符、界符等等。
来看看Kotlin中划分的Token。

//org.jetbrains.kotlin.lexer.KtTokenspublic interface KtTokens { //关键字的token    KtKeywordToken PACKAGE_KEYWORD          = KtKeywordToken.keyword("package");    KtKeywordToken AS_KEYWORD               = KtKeywordToken.keyword("as");    KtKeywordToken TYPE_ALIAS_KEYWORD       = KtKeywordToken.keyword("typealias");    KtKeywordToken CLASS_KEYWORD            = KtKeywordToken.keyword("class");    KtKeywordToken THIS_KEYWORD             = KtKeywordToken.keyword("this");    KtKeywordToken SUPER_KEYWORD            = KtKeywordToken.keyword("super");    KtKeywordToken VAL_KEYWORD              = KtKeywordToken.keyword("val");    KtKeywordToken VAR_KEYWORD              = KtKeywordToken.keyword("var");    KtKeywordToken FUN_KEYWORD              = KtKeywordToken.keyword("fun");    KtKeywordToken FOR_KEYWORD              = KtKeywordToken.keyword("for");    KtKeywordToken NULL_KEYWORD             = KtKeywordToken.keyword("null");    ...    //标识符、运算符token    KtSingleValueToken LBRACKET    = new KtSingleValueToken("LBRACKET", "[");    KtSingleValueToken RBRACKET    = new KtSingleValueToken("RBRACKET", "]");    KtSingleValueToken LBRACE      = new KtSingleValueToken("LBRACE", "{");    KtSingleValueToken RBRACE      = new KtSingleValueToken("RBRACE", "}");    KtSingleValueToken LPAR        = new KtSingleValueToken("LPAR", "(");    KtSingleValueToken RPAR        = new KtSingleValueToken("RPAR", ")");    KtSingleValueToken DOT         = new KtSingleValueToken("DOT", ".");    ...    //修饰符token    KtModifierKeywordToken ABSTRACT_KEYWORD  = KtModifierKeywordToken.softKeywordModifier("abstract");    KtModifierKeywordToken ENUM_KEYWORD      = KtModifierKeywordToken.softKeywordModifier("enum");    KtModifierKeywordToken OPEN_KEYWORD      = KtModifierKeywordToken.softKeywordModifier("open");    KtModifierKeywordToken INNER_KEYWORD     = KtModifierKeywordToken.softKeywordModifier("inner");    KtModifierKeywordToken OVERRIDE_KEYWORD  = KtModifierKeywordToken.softKeywordModifier("override");    KtModifierKeywordToken PRIVATE_KEYWORD   = KtModifierKeywordToken.softKeywordModifier("private");    KtModifierKeywordToken PUBLIC_KEYWORD    = KtModifierKeywordToken.softKeywordModifier("public");    ...}

Kotlin中将所有Token按照进行了分类,同时进行了Token分组。

//关键字    KtModifierKeywordToken[] MODIFIER_KEYWORDS_ARRAY =            new KtModifierKeywordToken[] {                    ABSTRACT_KEYWORD, ENUM_KEYWORD, OPEN_KEYWORD, INNER_KEYWORD, OVERRIDE_KEYWORD, PRIVATE_KEYWORD,                    PUBLIC_KEYWORD, INTERNAL_KEYWORD, PROTECTED_KEYWORD, OUT_KEYWORD, IN_KEYWORD, FINAL_KEYWORD, VARARG_KEYWORD,                    REIFIED_KEYWORD, COMPANION_KEYWORD, SEALED_KEYWORD, LATEINIT_KEYWORD,                    DATA_KEYWORD, INLINE_KEYWORD, NOINLINE_KEYWORD, TAILREC_KEYWORD, EXTERNAL_KEYWORD, ANNOTATION_KEYWORD, CROSSINLINE_KEYWORD,                    CONST_KEYWORD, OPERATOR_KEYWORD, INFIX_KEYWORD, SUSPEND_KEYWORD, HEADER_KEYWORD, IMPL_KEYWORD            };    //访问权限修饰符    TokenSet VISIBILITY_MODIFIERS = TokenSet.create(PRIVATE_KEYWORD, PUBLIC_KEYWORD, INTERNAL_KEYWORD, PROTECTED_KEYWORD);   //操作符    TokenSet OPERATIONS = TokenSet.create(AS_KEYWORD, AS_SAFE, IS_KEYWORD, IN_KEYWORD, DOT, PLUSPLUS, MINUSMINUS, EXCLEXCL, MUL, PLUS,                                          MINUS, EXCL, DIV, PERC, LT, GT, LTEQ, GTEQ, EQEQEQ, EXCLEQEQEQ, EQEQ, EXCLEQ, ANDAND, OROR,                                          SAFE_ACCESS, ELVIS,                                          RANGE, EQ, MULTEQ, DIVEQ, PERCEQ, PLUSEQ, MINUSEQ,                                          NOT_IN, NOT_IS,                                          IDENTIFIER);...

将所有的Kotlin词法单元一一枚举出来并分组以后,就要进行词法分析了。Kotlin使用了第三方开源的JFlex作为词法分析器,并没有自己实现(当然,重复造轮子就是一件很愚蠢的事情了:))。

2.1.1 定义JFlex词法分析配置文件Kotlin.flex

配置文件分为三个部分:

  • 用户代码:
  • 选项与声明:用来定制词法分析器,包括类名、父类、权限修饰符等等,以%开头作为标记
  • 词法规则:包括一组正则表达式和动作行为,也就是当正则表达式匹配成功后要执行的代码。

具体可看Kotlin.flex详细配置文件。

2.1.2 词法分析器_JetLexer
JFlex会读取配置文件并生成一个词法分析器(扫描器),在Kotlin编译器中对应_JetLexer(http://www.jflex.de/)

上述的方法以“yy”为前缀,表示它们是由JFlex自动生成的,避免与复制到这个类中的用户代码名字有冲突。
关于如法匹配输入流:
当对输入流进行词法分析时,词法分析器依据最长匹配规则来选择输入流的正规式,即所选择的正规式能最长的匹配当前输入流。如果同时有多个满足最长匹配的正规式,则生成的词法分析器将从中选择最先出现在词法规则描述中的正规式。在确定了起作用的正规式之后,将执行贵正规式所关联的动作。如果没有匹配的正规式,词法分析器将终止对输入流的分析并给出错误消息。
最后,KotlinLexer调用_JetLexer进行词法分析。

2.2 语法分析
语法分析的任务是在词法分析的基础上将单词序列组合成各类语法短语,如“程序”,“语句”,“表达式”等等,语法分析器将判断源程序在结构上是否正确。在语法分析过程中,会生成语法树(ST)/抽象语法树(AST)。
Kotlin中定义了AST的数据结构

//AST抽象语法树节点
public interface ASTNode extends UserDataHolder {
 //节点类型 
 IElementType getElementType();  
//节点文本
  String getText();  
//父节点  
 ASTNode getTreeParent();  
//第一孩子节点  
ASTNode getFirstChildNode();  
//最后孩子节点 
 ASTNode getLastChildNode();  
//所有孩子 
 ASTNode[] getChildren(@Nullable TokenSet filter);  
//移除孩子 
 void removeChild(@NotNull ASTNode child);  ...}

Kotlin的语法分析使用了InteliJ平台的开发者项目,语法分析器继承使用了PsiParser。
然后通过提供的PsiParser实现KotlinParser。

PSI,即程序结构接口,定义了程序的结构。
PSI文件(PSI File)则能够将源代码文件内容表示为特定编程语言元素的层次结构。说的通俗一点,PSI文件可以把Java、XML等语言代码表示为层次结构(树)的形式。例如,在IntelliJ开源的项目来看,PsiJavaFile可表示为Java文件,XmlFile表示为XML文件。通过PSI文件,我们能够遍历迭代文件中的元素,从而生成AST,正也正是语法分析中所需要的。
KotlinParser语法分析器调用KotParsing进行语法分析,并生成AST抽象语法树。
关于如何生成一个简单表达式的AST树,可以参考下图:


2.3 语义分析及中间代码生成

语义分析的任务是检查抽象语法树AST的上下文相关属性,即检查源代码是否符合该编程语言的规范,比如变量类型定义是否正确,运算符是否匹配等等。

在Kotlin编译器中,语义分析的工作位于org.jetbrains.kotlin.resolve模块下。



该模块包含了所有的的上下文相关属性的检查,包括对表达式语句、常量、智能转换等上下文相关属性检查。
语义分析器进行了上下文相关属性的检查之后,会生成中间代码,位于org.jetbrains.kotlin.ir模块中。

2.4 目标代码生成
目标代码生成的任务,顾名思义,是将中间代码转换为目标代码,即JVM字节码,位于org.jetbrains.kotlin.codegen模块中。
目标代码生成入口:KotlinCodegenFacade.

在代码类生成的过程中,又包括生成类名、类体、字段、函数方法等环节,相关的生成类有 ClassBodyCodegen、ClassFunctionCodegen、MemberCodegen、ExpressionCodegen、PropertyCodegen等。

Kotlin与Java不同的编译过程主要在于目标代码生成环节,Kotlin做了更多的工作。举个例子:
在Kotlin中,如果我们定义如下代码:

var a: Int = 1;

会等价于Java中

public Int a = 1;
public Int getA(){ return a;}
public void setA(int a) { this.a = a;}

那么,在Kotlin中是怎么实现的呢,关于属性的生成部分,在PropertyCodegen中。

public class PropertyCodegen {
    private void gen( 
           @Nullable KtProperty declaration, // 属性声明
            @NotNull PropertyDescriptor descriptor,  //描述,包括权限修饰符、注解、类型等。
            @Nullable KtPropertyAccessor getter, // 决定是否生成getter  
           @Nullable KtPropertyAccessor setter  //决定是否生成setter    ) {
        assert kind == OwnerKind.PACKAGE || kind == OwnerKind.IMPLEMENTATION || kind == OwnerKind.DEFAULT_IMPLS 
               : "Generating property with a wrong kind (" + kind + "): " + descriptor;  //生成注解信息
        genBackingFieldAndAnnotations(declaration, descriptor, false); 

//根据注解和权限修饰符等信息判断是否自动生成Getter代码
        if (isAccessorNeeded(declaration, descriptor, getter)) {
            generateGetter(declaration, descriptor, getter); 
       }
        //根据注解和权限修饰符等信息判断是否自动生成Setter代码
        if (isAccessorNeeded(declaration, descriptor, setter)) { 
           generateSetter(declaration, descriptor, setter);
        }
    }
}

可以看到,Kotlin在目标代码生成环节做了更多的处理,在该环节实现了自动生成Getter、Setter的代码。
总结
Kotlin的编译过程分析完了,当然很多细节的东西并没有深入研究,并且内容太大,不是一篇文章可以说的详尽的。
那么,分析了这么多,我们得到了什么有用的信息?
Kotlin编译器在编译前端(即词法分析、语法分析、语义分析、中间代码生成)并没有做让人感到惊讶的事情,和Java是基本一致的。与Java相比,所与众不同,也最重要的细节在编译后端(目标代码生成)环节。Kotlin编译器在目标代码生成环节做了很多类似于Java封装的事情,比如自动生成Getter/Setter代码的生成、Companion转变成静态类、修改类属性为final不可继承等等工作。可以说,大部分Kotlin的特性都在这个环节处理产生。可以这么说,Kotlin将我们本来在代码层做的一些封装工作转移到了编译后端阶段,以使得我们可以更加简洁的使用Kotlin语言。

参考资料:
http://mp.weixin.qq.com/s/lEFRH523W7aNWUO1QE6ULQ


Kotlin开发者社区

专注分享 Java、 Kotlin、Spring/Spring Boot、MySQL、redis、neo4j、NoSQL、Android、JavaScript、React、Node、函数式编程、编程思想、"高可用,高性能,高实时"大型分布式系统架构设计主题。

High availability, high performance, high real-time large-scale distributed system architecture design

分布式框架:Zookeeper、分布式中间件框架等
分布式存储:GridFS、FastDFS、TFS、MemCache、redis等
分布式数据库:Cobar、tddl、Amoeba、Mycat
云计算、大数据、AI算法
虚拟化、云原生技术
分布式计算框架:MapReduce、Hadoop、Storm、Flink等
分布式通信机制:Dubbo、RPC调用、共享远程数据、消息队列等
消息队列MQ:Kafka、MetaQ,RocketMQ
怎样打造高可用系统:基于硬件、软件中间件、系统架构等一些典型方案的实现:HAProxy、基于Corosync+Pacemaker的高可用集群套件中间件系统
Mycat架构分布式演进
大数据Join背后的难题:数据、网络、内存和计算能力的矛盾和调和
Java分布式系统中的高性能难题:AIO,NIO,Netty还是自己开发框架?
高性能事件派发机制:线程池模型、Disruptor模型等等。。。

合抱之木,生于毫末;九层之台,起于垒土;千里之行,始于足下。不积跬步,无以至千里;不积小流,无以成江河。

Kotlin 简介

Kotlin是一门非研究性的语言,它是一门非常务实的工业级编程语言,它的使命就是帮助程序员们解决实际工程实践中的问题。使用Kotlin 让 Java程序员们的生活变得更好,Java中的那些空指针错误,浪费时间的冗长的样板代码,啰嗦的语法限制等等,在Kotlin中统统消失。Kotlin 简单务实,语法简洁而强大,安全且表达力强,极富生产力。

Java诞生于1995年,至今已有23年历史。当前最新版本是 Java 9。在 JVM 生态不断发展繁荣的过程中,也诞生了Scala、Groovy、Clojure 等兄弟语言。

Kotlin 也正是 JVM 家族中的优秀一员。Kotlin是一种现代语言(版本1.0于2016年2月发布)。它最初的目的是像Scala那样,优化Java语言的缺陷,提供更加简单实用的编程语言特性,并且解决了性能上的问题,比如编译时间。 JetBrains在这些方面做得非常出色。

Kotlin语言的特性

用 Java 开发多年以后,能够尝试一些新的东西真是太棒了。如果您是 Java 开发人员,使用 Kotlin 将会非常自然流畅。如果你是一个Swift开发者,你将会感到似曾相识,比如可空性(Nullability)。 Kotlin语言的特性有:

1.简洁

大幅减少样板代码量。

2.与Java的100%互操作性

Kotlin可以直接与Java类交互,反之亦然。这个特性使得我们可以直接重用我们的代码库,并将其迁移到 Kotlin中。由于Java的互操作性几乎无处不在。我们可以直接访问平台API以及现有的代码库,同时仍然享受和使用 Kotlin 的所有强大的现代语言功能。

3.扩展函数

Kotlin 类似于 C# 和 Gosu, 它提供了为现有类提供新功能扩展的能力,而不必从该类继承或使用任何类型的设计模式 (如装饰器模式)。

4.函数式编程

Kotlin 语言一等支持函数式编程,就像Scala一样。具备高阶函数、Lambda 表达式等函数式基本特性。

5.默认和命名参数

在Kotlin中,您可以为函数中的参数设置一个默认值,并给每个参数一个名称。这有助于编写易读的代码。

6.强大的开发工具支持

而由于是JetBrains出品,我们拥有很棒的IDE支持。虽然Java到Kotlin的自动转换并不是100% OK 的,但它确实是一个非常好的工具。使用 IDEA 的工具转换Java代码为 Kotlin 代码时,可以轻松地重用60%-70%的结果代码,而且修改成本很小。

Kotlin 除了简洁强大的语法特性外,还有实用性非常强的API以及围绕它构建的生态系统。例如:集合类 API、IO 扩展类、反射API 等。同时 Kotlin 社区也提供了丰富的文档和大量的学习资料,还有在线REPL。

A modern programming language that makes developers happier. Open source forever

图来自《Kotlin从入门到进阶实战》 (陈光剑,清华大学出版社)
图来自《Kotlin从入门到进阶实战》 (陈光剑,清华大学出版社)

https://kotlinlang.org/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容