JNI 和 JNA 使用

Java Native Interface (JNI)是一个本地编程接口,可以让Java代码使用以其他语言(C/C++) 编写的代码和代码库。

编写Java代码

package myjni;

public class HelloJNI {
   static {
      System.loadLibrary("hello"); // Load native library at runtime
                                   // hello.dll (Windows) or libhello.so (Unixes)
   }
 
   // Declare a native method sayHello() that receives nothing and returns void
   private native void sayHello();
 
   // Test Driver
   public static void main(String[] args) {
      new HelloJNI().sayHello();  // invoke the native method  sayHello()
   }
}
  • 静态初始化块加载本地库,这个库应该被包含在Java库路径中(java.library.path 变量),否则会产生UnsatisfiedLinkError
    通过System.getProperty("java.library.path")可以查询对应的值,在执行程序的时候可以通过VM参数-Djava.library.path=path_to_lib显示指定路径
  • native 关键字声明方法,表明使用另外一种语言实现的

在实际使用中,.so文件会被放入到resource目录下,通过Class.getResource或者ClassLoader.getResource获取资源,存储到临时文件中,再加载该临时文件,而不用通过Djava.library.path显示指定路径


编译java代码

成字节码 "HelloJNI.java" -> "HelloJNI.class"

> javac  myjni\HelloJNI.java
or
> javac -d.  myjni\HelloJNI.java   //-d 指定放置生成类文件的位置,必要时创建包名对应的目录

创建 C/C++ 头文件

创建一个定义native函数签名的C/ C++头文件
通过使用JDK附带javah工具创建一个头文件,它可以生成包含class文件中native方法函数声明的头文件

// 如果使用的是IDEA 先cd  project/build/class/main
javah -d <dir>   myjni.HelloJNICpp // 生成  HelloJNICpp.h 文件

Java10中该工具不再提供,改为使用

javac -h <output_dir> myjni\HelloJNI.java

生成样式如下:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloJNI */
 
#ifndef _Included_HelloJNI
#define _Included_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     HelloJNI
 * Method:    sayHello
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_myjni_HelloJNI_sayHello(JNIEnv *, jobject);
 
#ifdef __cplusplus
}
#endif
#endif

头文件声明了一个C函数

JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *, jobject);

C函数的命名惯例是
Java_{package_and_classname}_{function_name}(JNI arguments),包名称中的点用下划线替代,函数默认先包含两个参数:

  • JNIEnv* : JNI environment 的引用(jni.h中定义),相当于一个函数表指针,用来访问所有的JNI函数
  • jobject : Java 该对象引用,相当于“this”

宏定义JNIEXPORT 和 JNICALL 是对编译器说明信息,用来做出口函数
extern "C" 只会被C++编译器识别,表明使用C的函数命名协议


C/C++代码

C代码如下:

#include <jni.h>
#include <stdio.h>
#include "HelloJNI.h"
 
JNIEXPORT void JNICALL Java_myjni_HelloJNI_sayHello(JNIEnv *env, jobject thisObj) {
   printf("Hello World!\n");
   return;
}

Ubuntu下的编译C代码,如果是C++需要使用g++编译

gcc -fPIC -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -shared -o libhello.so HelloJNI.c
  • -fPIC则表明使用地址无关代码,PIC:Position Independent Code
  • -I 指定头文件,例如jni.h

运行Java程序

显示指定.so文件

> java -Djava.library.path=. myjni.HelloJNI

JNI基础

  • Java基本类型:jint, jbyte, jshort, jlong, jfloat, jdouble, jchar, jboolean 分别对应
    int, byte, short, long, float, double, char, boolean
  • Java引用类型:jobject 应用对应于 java.lang.Object,它同样定义了众多子类型
typedef struct _jobject *jobject;

typedef jobject jclass;
typedef jobject jthrowable;
typedef jobject jstring;
typedef jobject jarray;
typedef jarray jbooleanArray;
typedef jarray jbyteArray;
typedef jarray jcharArray;
typedef jarray jshortArray;
typedef jarray jintArray;
typedef jarray jlongArray;
typedef jarray jfloatArray;
typedef jarray jdoubleArray;
typedef jarray jobjectArray;

C/C++方法收到的是JNI类型,返回值也是JNI类型(例如jstring, jintArray),但是程序内部使用的是C/C++类型,所以中间需要JNI类型与C/C++类型的相互转换

C/C++程序步骤:

  • 接收JNI类型的参数(由Java程序传递来)
  • 对于引用类型(jObject),将参数转换或复制为本地类型,例如将jstring转换为C字符串,将jintArray转换为C的int []等;对于原始JNI类型,如jint和jdouble不需要转换,可以直接操作
  • 以本地类型执行操作
  • 创建JNI类型的返回对象,并将结果复制到返回对象中

JNI编程中关键点是JNI引用类型(如jstring,jobject,jintArray,jobjectArray)和本机类型(C-string,int [])之间的转换。 JNI接口提供了许多函数来进行这样的转换

JNI是一个C接口,它不是面向对象的,所以没有真正传递对象


JNIEnv函数调用方法

通过JNIEnv可以调用众多函数,内部实际上都是JNINativeInterface_结构体内部的函数指针

struct JNINativeInterface_;

struct JNIEnv_;

#ifdef __cplusplus
typedef JNIEnv_ JNIEnv;
#else
typedef const struct JNINativeInterface_ *JNIEnv;
#endif
类型 语法
C (*env)->FunctionName(env, parameter)
C++ env->FunctionName(parameter) //使用了内联函数

下面的例子中多使用C语言的调用方式


传递原始类型

可以直接使用强制类型转换()

typedef unsigned char   jboolean;       /* unsigned 8 bits */
typedef signed char     jbyte;          /* signed 8 bits */
typedef unsigned short  jchar;          /* unsigned 16 bits */
typedef short           jshort;         /* signed 16 bits */
typedef int             jint;           /* signed 32 bits */
typedef long long       jlong;          /* signed 64 bits */
typedef float           jfloat;         /* 32-bit IEEE 754 */
typedef double          jdouble;        /* 64-bit IEEE 754 */

传递字符串

Java的字符串是16位Unicode字符序列,C的字符串是char型数组,以空字符结尾
JNIEnv提供了转换的函数

UTF-8(1到3个字节) <--> Unicode(2个字节)

//If isCopy is not NULL, then *isCopy is set to JNI_TRUE if a copy is made; or it is set to JNI_FALSE if no copy is made.
const char * GetStringUTFChars(JNIEnv *env, jstring string, jboolean *isCopy);
//将jstring转换成为Unicode格式的char*
const jchar * GetStringChars(JNIEnv *env, jstring string, jboolean *isCopy);


//告诉VM native 代码不在访问utf
void ReleaseStringUTFChars(JNIEnv *env, jstring string, const char *utf);
//释放指向Unicode格式的char*的指针
void ReleaseStringChars(JNIEnv *env, jstring string, const jchar *chars);


//创建一个java.lang.String 对象
jstring NewStringUTF(JNIEnv *env, const char *bytes);
//创建一个Unicode格式的String对象,从Unicode字符
jstring NewString(JNIEnv *env, const jchar *unicodeChars, jsize length);


//获取UTF-8形式的串长度
jsize GetStringUTFLength(JNIEnv *env, jstring string);
//获取Unicode格式的的长度
jsize GetStringLength(JNIEnv *env, jstring string);



//把一段区域中的String,转为UTF-8,放入buf
void GetStringUTFRegion(JNIEnv *env, jstring str, jsize start, jsize length, char *buf);
//复制一段区域中的String,放入buf
void GetStringRegion(JNIEnv *env, jstring str, jsize start, jsize length, jchar *buf);

GetStringChars : jstring -> char* 如果内存分配失败,返回NULL,第三个参数如果不为NULL,当返回字符串是原始string的复制时,被置为JNI_TRUE;
当直接指向初始的String类型时,对应为JNI_FALSE ,jni在运行时会尽可能执行这种情况,这时本地程序如果修改字符数组的内容,对应Java程序的字符串会发生改变


传递原始类型的数组

// ArrayType: jintArray, jbyteArray, jshortArray, jlongArray, jfloatArray, jdoubleArray, jcharArray, jbooleanArray
// PrimitiveType: int, byte, short, long, float, double, char, boolean
// NativeType: jint, jbyte, jshort, jlong, jfloat, jdouble, jchar, jboolean

NativeType * Get<PrimitiveType>ArrayElements(JNIEnv *env, ArrayType array, jboolean *isCopy);
void Release<PrimitiveType>ArrayElements(JNIEnv *env, ArrayType array, NativeType *elems, jint mode);

void Get<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize length, NativeType *buffer);
void Set<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize length, const NativeType *buffer);

ArrayType New<PrimitiveType>Array(JNIEnv *env, jsize length);

void * GetPrimitiveArrayCritical(JNIEnv *env, jarray array, jboolean *isCopy);
void ReleasePrimitiveArrayCritical(JNIEnv *env, jarray array, void *carray, jint mode);

jsize GetArrayLength(JNIEnv *env, jarray array);//获取数组的长度

访问对象的变量和方法

访问实例对象的变量和类的静态变量

jclass GetObjectClass(JNIEnv *env, jobject obj);
   // Returns the class of an object.
   
jfieldID GetFieldID(JNIEnv *env, jclass cls, const char *name, const char *sig);
  // Returns the field ID for an instance variable of a class.
jfieldID GetStaticFieldID(JNIEnv *env, jclass cls, const char *name, const char *sig);
  // Returns the field ID for a static variable of a class.
 
// 通过jfieldID读写字段值
  // <type>包括8种原始类型和object
NativeType Get<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID);
void Set<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID, NativeType value);

NativeType GetStatic<type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID);
void SetStatic<type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID, NativeType value);

GetFieldID中sig表示签名,编码形式如下

类型 sig
boolen Z
byte B
char C
short S
int I
long J
float F
double D
void V
class L<fully-qualified-name>;//引用类型 以 L 开头 ; 结尾

方法签名主要用在重载方法的说明,签名的形式是“(parameters)return-type
javap -s -p 类名称 查看签名
javap 是java类文件的反编译器
-s :输出内部类型签名
-p:显示私有成员,默认只打印public的签名信息

例子:设定一个long变量

static jlong setField_long(JNIEnv *env, jobject java_obj, const char *field_name, jlong val) {

    jclass clazz = env->GetObjectClass(java_obj);
    jfieldID field = env->GetFieldID(clazz, field_name, "J");
    if (field)
        env->SetLongField(java_obj, field, val);
    else {
        LOGE("__setField_long:field '%s' not found", field_name);
    }
    return val;
}

调用实例对象的方法和静态方法

  1. 获取这个类对象的引用 GetObjectClass()
  2. 获取方法ID GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig)
    需要提供方法名和对应的签名
  3. 基于方法ID,Call<Primitive-type>Method()CallVoidMethod()CallObjectMethod(),对应<Primitive-type>指的是返回类型,如果有参数,就在后边加上参数

JNI中对应的函数:

jmethodID GetMethodID(JNIEnv *env, jclass cls, const char *name, const char *sig);
   // Returns the method ID for an instance method of a class or interface.
   
NativeType Call<type>Method(JNIEnv *env, jobject obj, jmethodID methodID, ...);
NativeType Call<type>MethodA(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args);
NativeType Call<type>MethodV(JNIEnv *env, jobject obj, jmethodID methodID, va_list args);
   // Invoke an instance method of the object.
   // The <type> includes each of the eight primitive and Object.
   
jmethodID GetStaticMethodID(JNIEnv *env, jclass cls, const char *name, const char *sig);
   // Returns the method ID for an instance method of a class or interface.
   
NativeType CallStatic<type>Method(JNIEnv *env, jclass clazz, jmethodID methodID, ...);
NativeType CallStatic<type>MethodA(JNIEnv *env, jclass clazz, jmethodID methodID, const jvalue *args);
NativeType CallStatic<type>MethodV(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args);
   // Invoke an instance method of the object.
   // The <type> includes each of the eight primitive and Object.


//调用超类的方法
NativeType CallNonvirtual<type>Method(JNIEnv *env, jobject obj, jclass cls, jmethodID methodID, ...);
NativeType CallNonvirtual<type>MethodA(JNIEnv *env, jobject obj, jclass cls, jmethodID methodID, const jvalue *args);
NativeType CallNonvirtual<type>MethodV(JNIEnv *env, jobject obj, jclass cls, jmethodID methodID, va_list args);

创建对象和对象数组

在native代码中构建jobject,jobjectArray

创建对象

获得构造方法的ID,传递函数名为“<init>”,“V”作为返回类型

jclass FindClass(JNIEnv *env, const char *name);
 
jobject NewObject(JNIEnv *env, jclass cls, jmethodID methodID, ...);
jobject NewObjectA(JNIEnv *env, jclass cls, jmethodID methodID, const jvalue *args);
jobject NewObjectV(JNIEnv *env, jclass cls, jmethodID methodID, va_list args);
   // Constructs a new Java object. The method ID indicates which constructor method to invoke
 
jobject AllocObject(JNIEnv *env, jclass cls);
  // Allocates a new Java object without invoking any of the constructors for the object.

对象数组

  • FindClass(env, "java/lang/Double"); 找到对应的类
  • NewObjectArray(env, 2, classDouble, NULL); 创建对应的数组
  • GetMethodID(env, classDouble, "<init>", "(D)V"); 找到类的构造方法
  • NewObject(env, classDouble, midDoubleInit, average); 创建该类
  • SetObjectArrayElement(env, outJNIArray, 0, objSum); 存到数组对应的位置上
jobjectArray NewObjectArray(JNIEnv *env, jsize length, jclass elementClass, jobject initialElement);
   // Constructs a new array holding objects in class elementClass.
   // All elements are initially set to initialElement.
 
jobject GetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index);
   // Returns an element of an Object array.
 
void SetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index, jobject value);
   // Sets an element of an Object array.

本地和全局引用

JNI将native代码中的对象引用分为两类

  • 本地引用 local reference :native方法中创建的都是本地引用,方法结束后就回收,可以通过DeleteLocalRef()显式的使本地引用无效,使得JVM尽快进行GC,同时传递到native方法的参数对象,jni方法返回的Java对象都是本地引用
  • 全局引用global reference:通过jobject NewGlobalRef() (JNIEnv *env, jobject lobj);获取,然后通过void DeleteGlobalRef(JNIEnv *env, jobject gref);显示的释放

所以对全局变量赋值时,先要将本地引用转为全局引用,赋值后再释放本地引用

实际上本地引用并不是native方法里的局部变量,局部变量存放在堆栈中,而Local Reference存放在Local Reference Table中,而该Table的容量是有限的,虽然在native Method结束时,JVM会自动释放Local Reference,但在使用中,要及时使用DeleteLocalRef()删除不必要的Local Reference,避免Local Reference Table溢出


动态加载

VM虚拟机会在native库加载的时候调用JNI_OnLoad,例如通过System.loadLibrary加载的时候

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved);

静态注册本地方法的弊端

  1. 需要编译所有声明了native方法的Java类,每个所生成的class文件都得用javah生成一个头文件
  2. javah生成的JNI层函数名特别长,书写起来很不方便。
  3. 初次调用native函数时要根据函数名字搜索对应用JNI层函数来建立关联关系,这样会影响运行效率。

动态注册核心是JNINativeMethod,把Java中的方法和本地的方法关联起来

typedef struct {
    char *name; //Java中原生方法的名称
    char *signature;//方法签名,是参数类型和返回值类型的组合
    void *fnPtr;//函数指针,
} JNINativeMethod;

看了其他的教程,发现都是固定的套路,C++代码如下:


static JNINativeMethod gMethods[] = {  
    {"name",  "signature", fnptr},  
};  


JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) {
    JNIEnv *env = NULL;
    jint result = JNI_FALSE;

    //获取env指针
    if (jvm->GetEnv(reinterpret_cast<void **> (&env), JNI_VERSION_1_6.) != JNI_OK) {
        return result;
    }
    if (env == NULL) {
        return result;
    }


    //获取类引用,写类的全路径(包名+类名
    jclass clazz = env->FindClass("***/***/JNIDynamicUtils");
    if (clazz == NULL) {
        return result;
    }
    //注册方法
    if (env->RegisterNatives(clazz, gMethods, sizeof(gMethods) / sizeof(gMethods[0])) < 0) {
        return result;
    }
    //成功
    result = JNI_VERSION_1_6;
    return result;
}

在JNI_OnLoad中,可以保存JavaVM的指针,这是跨线程的,持久有效的变量


多线程

JNIEnv只在当前线程有效,如果C/C++中创建的新线程需要访问Java VM,先调用AttachCurrentThread方法把自己附着到VM并且获得env

JavaVM *vm;
JNIEnv *env;
vm->AttachCurrentThread(&env, NULL);
vm->DetachCurrentThread();//卸载

多Java线程加载同一个lib,或者单一线程加载多次,实际上都不会进行处理,因为ClassLoader内部静态字段会记录进程加载的所有共享文件,首先会检查该ClassLoad是否加载过,不会进行重复加载,如果其他ClassLoad加载过,也不会进行加载,多个线程调用同一个so文件的不同函数,共享一套全局变量,因为共享库在进程中只有一套数据

ClassLoad中关于加载native library的核心字段:

    // 所有加载的native library名称
    private static Vector<String> loadedLibraryNames = new Vector<>();

    // Native libraries belonging to system classes.
    private static Vector<NativeLibrary> systemNativeLibraries
        = new Vector<>();

    //当前class loader对应的加载的所有Native libraries
    private Vector<NativeLibrary> nativeLibraries = new Vector<>();

    // The paths searched for libraries
    private static String usr_paths[];
    private static String sys_paths[];

底层具体通过dlopen库函数加载:
jdk/src/share/native/java/lang/ClassLoader.c#Java_java_lang_ClassLoader_00024NativeLibrary_load
src/share/vm/prims/jvm.cpp#JVM_LoadLibrary
/src/os/linux/vm/os_linux.cpp#dll_load#os::Linux::dlopen_helper#dlopen(filename, RTLD_LAZY)


JNA

JNA(Java Native Access)是一个开源的Java框架,是Sun公司推出的一种调用本地方法的技术,建立在JNI基上的一个框架。JNA简化了调用本地方法的过程,可以直接调用本地共享库中的函数,不需要写Java代码之外的程序

需要添加依赖的jar包jna.jar到CLASSPATH,也可以再添加jna-platform.jar包,内部包含一些平台常用的库映射

        <dependency>
            <groupId>net.java.dev.jna</groupId>
            <artifactId>jna</artifactId>
        </dependency>

使用方法:

package com.sun.jna.examples;

import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Platform;

/** Simple example of JNA interface mapping and usage. */
public class HelloWorld {

    // This is the standard, stable way of mapping, which supports extensive
    // customization and mapping of Java to native types.

    public interface CLibrary extends Library {
        CLibrary INSTANCE = (CLibrary)
            Native.load((Platform.isWindows() ? "msvcrt" : "c"),
                                CLibrary.class);

        void printf(String format, Object... args);
    }

    public static void main(String[] args) {
        CLibrary.INSTANCE.printf("Hello, World\n");
        for (int i=0;i < args.length;i++) {
            CLibrary.INSTANCE.printf("Argument %d: %s\n", i, args[i]);
        }
    }
}
  • 一个接口类映射一个要加载的库,该类需要扩展自Library,接口内定义需要使用的本地库方法
  • 接口内部需要一个公共静态常量:INSTANCE,通过这个常量,就可以获得这个接口的实例,从而使用接口的方法,JNA内部通过代理模式,先对数据类型进行转换,调用外部dll/so的函数
  • 可以将本地库路径设定到jna.library.path

核心是不同平台数据类型的转变:

同JNI一样,Java基本数据类型可以直接映射


Default Type Mappings

数组类型:

// Original C declarations
void fill_buffer(int *buf, int len);
void fill_buffer(int buf[], int len); // same thing with array syntax

// Equivalent JNA mapping
void fill_buffer(int[] buf, int len);

结构体对应:定义一个继承Structurepublic static类,用来作为参数或返回值类型,类中的公共字段的顺序,必须与C 语言中的结构的顺序一致,同时定义getFieldOrder()方法,返回有序的字段名称

Java 调用动态链接库中的C 函数,实际上就是一段内存作为函数的参数传递给C函数。动态链接库以为这个参数就是C 语言传过来的参数。同时,C 语言的结构体是一个严格的规范,它定义了内存的次序。因此,JNA 中模拟的结构体的变量顺序绝对不能错

JNA是不能完全替代JNI的,因为NA只能实现Java访问C函数,使用JNI技术,不仅可以实现Java访问C函数,也可以实现C语言调用Java代码


Reference

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

推荐阅读更多精彩内容