Java調(diào)用Golang生成的動(dòng)態(tài)庫(dll,so)

1. 環(huán)境準(zhǔn)備

A. GCC

在控制臺(tái)中輸入

gcc -v

如果提示命令未找到螟加,那么說明你的計(jì)算機(jī)中還沒有g(shù)cc,去安裝一個(gè)吧,gcc官方網(wǎng)站:https://gcc.gnu.org/ 如果從來沒有安裝過gcc的朋友可以直接安裝win-build,可以幫你快速的安裝 官方網(wǎng)站:http://mingw-w64.org/doku.php/download/win-builds

2. 編寫go程序

我們這里只是編寫一個(gè)簡單的計(jì)算加法的程序规哪,接受兩個(gè)整數(shù),然后計(jì)算他們的和掠械,并返回。 在這里注祖,我們將文件命名為libhello.go

package main

import "C"

//export Sum
func Sum(a int, b int) int {
    return a + b
}

func main() {
}

注意猾蒂,即使是要編譯成動(dòng)態(tài)庫,也要有main函數(shù)是晨,上面的import "C"一定要有 而且一定要有注釋

//export Sum

經(jīng)測(cè)試肚菠,如果沒有這個(gè)導(dǎo)出的DLL庫中找不到對(duì)應(yīng)的函數(shù)

3. 編譯go程序

首先,將控制臺(tái)的所在目錄切換到go程序的所在目錄罩缴,即libhello.go所在目錄

A. Windows動(dòng)態(tài)庫

執(zhí)行如下命令生成DLL動(dòng)態(tài)鏈接庫:

go build -buildmode=c-shared -o libhello.dll .\libhello.go

如果控制臺(tái)沒有報(bào)錯(cuò),那么會(huì)在當(dāng)前路徑下生成libhello.dll文件

B. Linux/Unix/macOS動(dòng)態(tài)庫

執(zhí)行如下命令生成SO動(dòng)態(tài)庫:

go build -buildmode=c-shared -o libhello.so .\libhello.go

4. 在java中調(diào)用

A. JNA的引用

Java調(diào)用Native的動(dòng)態(tài)庫有兩種方式,JNI和JNA奏甫,JNA是Oracle最新推出的與Native交互的方式乍丈,具體介紹我就不多說了,引用百度百科的連接:https://baike.baidu.com/item/JNA/8637274?fr=aladdin乓土,有需要的朋友可以去看看。 在這里,我們使用JNA的方式戳表,JNI的方式基本廢棄,除非有特殊需要昼伴,在這里不多說匾旭,有需要可以聯(lián)系我討論。 新建Java工程圃郊,我使用的是Maven做包管理价涝,所以直接引用JNA的依賴:

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

如果你沒有使用包管理工具,可以直接下載Jar文件引入持舆,下載地址也貼一下吧色瘩,也是4.5.2版本的: http://central.maven.org/maven2/net/java/dev/jna/jna/4.5.2/jna-4.5.2.jar

B. 創(chuàng)建接口

我們需要?jiǎng)?chuàng)建一個(gè)interface來映射DLL中的函數(shù),之后我們可以通過interface的實(shí)例來訪問DLL中的函數(shù)吏廉。

package cn.lemonit.robot.runner.executor;

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

public interface LibHello extends Library {
    LibHello INSTANCE = (LibHello) Native.loadLibrary("E:/workspace/libhello", LibHello.class);

    int Sum(int a, int b);
}

注意泞遗,Sum是函數(shù)名,一定要與Go中事先寫好的函數(shù)名保持一致 Native.loadLibrary()的第一個(gè)參數(shù)是一個(gè)字符串席覆,要加載的動(dòng)態(tài)庫的名稱或全路徑史辙,后面不需要加.dll或者.so的后綴。第二個(gè)參數(shù)為interface的類名稱佩伤。

C. 調(diào)用

我們新建一個(gè)App類聊倔,作為main方法的入口類,在main方法中不需要多余的操作生巡,只需要調(diào)用即可耙蔑,在這里我們調(diào)用Sum方法,同時(shí)傳如222 孤荣, 333甸陌,可以看到控制臺(tái)輸出:555

package cn.lemonit.robot.runner.executor;

public class App {

    public static void main(String[] args) {
        System.out.println(LibHello.INSTANCE.Sum(222, 333));
    }
}

大功告成,我終于玩通了Java調(diào)用Go程序Q喂伞G怼!疯汁! 牲尺???不對(duì)勁谤碳,有點(diǎn)太過于幸災(zāi)樂禍了溃卡,往下繼續(xù)

5. 參數(shù)中包含字符串

A. 我真的大功告成了嗎?

我們的程序總不能只傳數(shù)值型的參數(shù)吧蜒简,我們把GO程序改一下瘸羡,換成一個(gè)一字符串作為參數(shù)的函數(shù),接受一個(gè)字符串參數(shù)臭蚁,然后從控制臺(tái)輸出:hello: xxx最铁,如下:

package main

import "fmt"

//export Hello
func Hello(msg string) {
    fmt.Print("hello: " + msg)
}

func main() {
}

按照上面2.B步驟中的寫法,我們將java的LibHello接口改成這個(gè)樣子:

package cn.lemonit.robot.runner.executor;

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

public interface LibHello extends Library {
    LibHello INSTANCE = (LibHello) Native.loadLibrary("E:/workspace/libhello", LibHello.class);

    void Hello(String msg);
}

接下來垮兑,我們調(diào)用這個(gè)接口冷尉,將 2.C 中的啟動(dòng)入口類App代碼改成這樣:

package cn.lemonit.robot.runner.executor;

public class App {

    public static void main(String[] args) {
        LibHello.INSTANCE.Hello("LemonIT.CN");
    }
}

運(yùn)行起來,咦系枪?報(bào)錯(cuò)了雀哨??私爷?

fatal error: string concatenation too long

goroutine 17 [running, locked to thread]:
runtime.throw(0x644c1d4f, 0x1d)
  xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (runtime報(bào)錯(cuò)雾棺,沒有意義,不貼了)

這是怎么回事衬浑,我傳的是一個(gè)很標(biāo)準(zhǔn)的String啊捌浩,怎么會(huì)報(bào)錯(cuò)呢? 在一陣無頭緒中工秩,發(fā)現(xiàn)剛才在調(diào)用go build -buildmode=c-shared -o libhello.dll .\libhello.go命令的時(shí)候在文件夾中除了libhello.dll被生成之外尸饺,還生成了一個(gè)libhello.h文件!V摇浪听!這不是C的頭文件么?出于好奇眉菱,打開看看有什么高大上的東西迹栓,這一打開還真是嚇到我了:

/* Created by "go tool cgo" - DO NOT EDIT. */

/* package command-line-arguments */

#line 1 "cgo-builtin-prolog"

#include <stddef.h> /* for ptrdiff_t below */

#ifndef GO_CGO_EXPORT_PROLOGUE_H
#define GO_CGO_EXPORT_PROLOGUE_H

typedef struct { const char *p; ptrdiff_t n; } _GoString_;

#endif

/* Start of preamble from import "C" comments.  */
/* End of preamble from import "C" comments.  */

/* Start of boilerplate cgo prologue.  */
#line 1 "cgo-gcc-export-header-prolog"

#ifndef GO_CGO_PROLOGUE_H
#define GO_CGO_PROLOGUE_H

typedef signed char GoInt8;
typedef unsigned char GoUint8;
typedef short GoInt16;
typedef unsigned short GoUint16;
typedef int GoInt32;
typedef unsigned int GoUint32;
typedef long long GoInt64;
typedef unsigned long long GoUint64;
typedef GoInt64 GoInt;
typedef GoUint64 GoUint;
typedef __SIZE_TYPE__ GoUintptr;
typedef float GoFloat32;
typedef double GoFloat64;
typedef float _Complex GoComplex64;
typedef double _Complex GoComplex128;

/*
  static assertion to make sure the file is being used on architecture
  at least with matching size of GoInt.
*/
typedef char _check_for_64_bit_pointer_matching_GoInt[sizeof(void*)==64/8 ? 1:-1];

typedef _GoString_ GoString;
typedef void *GoMap;
typedef void *GoChan;
typedef struct { void *t; void *v; } GoInterface;
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;

#endif

/* End of boilerplate cgo prologue.  */

#ifdef __cplusplus
extern "C" {
#endif

extern void Hello(GoString p0);

#ifdef __cplusplus
}
#endif

這么大一篇子,往下翻翻翻俭缓,找到了我們的Hello函數(shù)的定義:

extern void Hello(GoString p0);

發(fā)現(xiàn)問題了克伊,人家參數(shù)要的事GoString,而我們傳的是Java的String华坦,肯定類型不一致啊愿吹。那GoString是個(gè)什么東西呢,我該給他傳什么季春?往上翻洗搂,找到了這么兩行代碼:

typedef struct { const char *p; ptrdiff_t n; } _GoString_;
// .....
typedef _GoString_ GoString;

嗯嗯嗯消返,看來這個(gè)GoString不過就是個(gè)C里面的結(jié)構(gòu)體罷了载弄,結(jié)構(gòu)體里面一個(gè)char *一個(gè)ptrdiff_t耘拇,看來我們用java調(diào)用程序的時(shí)候,構(gòu)造個(gè)這么樣的結(jié)構(gòu)體給他傳進(jìn)來應(yīng)該就行了宇攻,好了惫叛,有思路了,開始折騰逞刷。

B. 創(chuàng)建GoString嘉涌!

我們首先用JNA構(gòu)建一個(gè)C的結(jié)構(gòu)體類型,那么問題來了夸浅,JNA中char *可以直接用java的String來代替仑最,那么ptrdiff_t這個(gè)玩意……有點(diǎn)無語,這是啥胺警医?經(jīng)過一頓操作百度和谷歌,終于知道了坯钦,這個(gè)類型實(shí)際上是兩個(gè)內(nèi)存地址之間的距離的值预皇,數(shù)據(jù)類型實(shí)際上就是C中的long int,在這里他表示的是字符串char *的長度婉刀,也就是字符串的長度唄~吟温,知道這個(gè)就好辦了,我們?cè)贘ava中直接用long類型來代替它突颊。 我們新建一個(gè)GoString類來對(duì)應(yīng)C中的GoString結(jié)構(gòu)體鲁豪,也就是Go程序中的string,這塊得說一下洋丐,有些人可能沒有用過JNA呈昔,在JNA中若想定義一個(gè)結(jié)構(gòu)體,需要?jiǎng)?chuàng)建一個(gè)類繼承自com.sun.jna.Structure友绝,熟悉C的人應(yīng)該知道(不知道也沒關(guān)系)堤尾,向C中傳值通常有兩種,一種是傳引用(就是傳指針類型)迁客,一種是傳真實(shí)值郭宝,在JNA里面做的話我們通常在這個(gè)結(jié)構(gòu)體類中創(chuàng)建兩個(gè)靜態(tài)的內(nèi)部類,這兩個(gè)內(nèi)部類繼承自這個(gè)結(jié)構(gòu)體類掷漱,并實(shí)現(xiàn)Structure.ByValue和Structure.ByReference接口粘室,其中ByValue就是傳真實(shí)值時(shí)候用的,ByReference就是傳引用的時(shí)候用的卜范,綜上所述衔统,我們的GoString類就應(yīng)該長成這個(gè)樣子:

package cn.lemonit.robot.runner.executor;

import com.sun.jna.Structure;

import java.util.ArrayList;
import java.util.List;

public class GoString extends Structure {

    public String str;
    public long length;

    public GoString() {
    }

    public GoString(String str) {
        this.str = str;
        this.length = str.length();
    }

    @Override
    protected List<String> getFieldOrder() {
        List<String> fields = new ArrayList<>();
        fields.add("str");
        fields.add("length");
        return fields;
    }

    public static class ByValue extends GoString implements Structure.ByValue {
        public ByValue() {
        }

        public ByValue(String str) {
            super(str);
        }
    }

    public static class ByReference extends GoString implements Structure.ByReference {
        public ByReference() {
        }

        public ByReference(String str) {
            super(str);
        }
    }
}

可以發(fā)現(xiàn),我們重寫了一個(gè)getFieldOrder方法,在里面新建一個(gè)list锦爵,然后把兩個(gè)屬性名作為字符串放到里面舱殿,然后當(dāng)做返回值返回了。這個(gè)操作實(shí)際是為了告訴JNA险掀,我這兩個(gè)變量和C結(jié)構(gòu)體中的變量是怎么個(gè)對(duì)應(yīng)關(guān)系的沪袭,我們?cè)賮砘仡櫼幌聞偛舕ibhello.h中定義的GoString結(jié)構(gòu)體(其實(shí)是省著你再往上翻看,費(fèi)勁樟氢,直接粘出來方便你看):

typedef struct { const char *p; ptrdiff_t n; } _GoString_;

我們的字符串叫str冈绊,而char *的名稱是p,我們的字符串長度叫l(wèi)ength埠啃,而結(jié)構(gòu)體中叫n死宣,JNA又不是人工智能框架,肯定猜不出來你想把str對(duì)應(yīng)到p碴开,length想對(duì)應(yīng)到n十电,所以我們?cè)谶@里通過list的形式把字段名在list中排一個(gè)順序,告訴JNA叹螟,我的str想對(duì)應(yīng)結(jié)構(gòu)體的第一個(gè)屬性鹃骂,length想對(duì)應(yīng)結(jié)構(gòu)體的第二個(gè)屬性。(你可以試試罢绽,讓fields.add的順序調(diào)換一下畏线,肯定會(huì)出問題)。

C. 有了GoString良价!我又可以幸災(zāi)樂禍了G夼埂?明垢?蚣常?

好了,GoString有了痊银,萬事俱備抵蚊,只欠東風(fēng)了!用一把溯革,我們把剛才0x05.A中的LibHello類改成這樣:

package cn.lemonit.robot.runner.executor;

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

public interface LibHello extends Library {
    LibHello INSTANCE = (LibHello) Native.loadLibrary("E:/workspace/libhello", LibHello.class);

    void Hello(GoString.ByValue msg);
}

App入口類代碼改成這樣:

package cn.lemonit.robot.runner.executor;

public class App {
    public static void main(String[] args) {
        LibHello.INSTANCE.Hello(new GoString.ByValue("LemonIT.CN"));
    }
}

運(yùn)行贞绳!控制臺(tái)成功輸出:

hello: LemonIT.CN

哈哈哈!成功了致稀,有點(diǎn)小激動(dòng)冈闭!把代碼發(fā)給朋友們看!6兜ァ萎攒!有一個(gè)朋友問我遇八,你這Hello函數(shù)的結(jié)果能不能不在Go中的控制臺(tái)打印,而是在Java中打印到控制臺(tái)耍休?額……我猶豫了一下押蚤,應(yīng)該能吧……!

6. 返回值中包含字符串

A. 做一個(gè)小實(shí)驗(yàn)~

我們把5中的Go函數(shù)Hello改一下羹应,讓結(jié)果通過返回值返回,而不是直接在控制臺(tái)打印次屠,變成這樣滴:

package main

import "C"

//export Hello
func Hello(msg string) string{
    return "hello:" + msg
}

func main() {
}

既然返回值也是string园匹,那JNA這邊也得小改一波,把0x05.C中的LibHello類改成這樣:

package cn.lemonit.robot.runner.executor;

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

public interface LibHello extends Library {
    LibHello INSTANCE = (LibHello) Native.loadLibrary("E:/workspace/libhello", LibHello.class);

    GoString.ByValue Hello(GoString.ByValue msg);
}

運(yùn)行入口類App也對(duì)應(yīng)修改一下:

package cn.lemonit.robot.runner.executor;

public class App {
    public static void main(String[] args) {
        System.out.println(LibHello.INSTANCE.Hello(new GoString.ByValue("LemonIT.CN")).str);
    }
}

大功告成劫灶,運(yùn)行一下裸违!

panic: runtime error: cgo result has Go pointer

goroutine 17 [running, locked to thread]:
main._cgoexpwrap_b02601c1465e_Hello.func1(0xc04203deb8)
    _cgo_gotypes.go:59 +0x6c
main._cgoexpwrap_b02601c1465e_Hello(0xbe3ce0, 0xa, 0xc042008050, 0x10)
    _cgo_gotypes.go:61 +0xa1

嗯?這啥本昏?我的LemonIT.CN呢供汛?在控制臺(tái)中并沒有找到啊涌穆!有點(diǎn)讓人發(fā)狂怔昨,怎么一步一個(gè)坎~不過想想比爾蓋茨,我還是決定做一名脾氣好的程序員宿稀,慢慢研究吧趁舀。

B. 事情總有解決辦法!(車到山前必有路祝沸,有路必有豐田車)

雖然沒有LemonIT.CN矮烹,但是看控制臺(tái)中的error,cgo result has Go pointer罩锐,還是找到了一絲線索奉狈。又開始一頓操作百度和谷歌。原來涩惑,Go有自己的GC(垃圾回收仁期,不解釋),通俗點(diǎn)說就是我Go語言的指針你們其他語言別想用竭恬!額蟀拷,那咋整!急的我連大學(xué)時(shí)候的課堂筆記都翻出來了萍聊。無意中看到了當(dāng)時(shí)寫的借助JNA與C通信问芬,C中將char *返回給Java,然后Java使用String即可接收寿桨。嗯此衅,嗯强戴?這條咋忘了呢?哈哈哈挡鞍,豈不是我把Go中的string轉(zhuǎn)成C的char *返回就可以了骑歹?好了,讓我們?cè)嚿夏敲匆辉嚹ⅲ褎偛诺腉o中的Hello函數(shù)再次修改一波:

package main

import "C"

//export Hello
func Hello(msg string) *C.char{
    return C.CString("hello : " + msg)  
}

func main() {
}

同樣滴道媚,我們的JNA這邊也得改一改,把LibHello類修改成這樣:

package cn.lemonit.robot.runner.executor;

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

public interface LibHello extends Library {
    LibHello INSTANCE = (LibHello) Native.loadLibrary("E:/workspace/libhello", LibHello.class);

    String Hello(GoString.ByValue msg);
}

LibHello既然改了翘县,那么入口類App也得對(duì)應(yīng)修改:

package cn.lemonit.robot.runner.executor;

public class App {
    public static void main(String[] args) {
        System.out.println(LibHello.INSTANCE.Hello(new GoString.ByValue("LemonIT.CN")));
    }
}

好了好了好了最域,運(yùn)行:

hello : LemonIT.CN

終于輸出出來了!

7. 總結(jié)

這個(gè)Go和Java的交互剛剛走了這一小小步就一步一個(gè)坎锈麸,看來真不能隨便的幸災(zāi)樂禍岸浦!忘伞!還得謙虛薄翅,路才能越走越遠(yuǎn)。雖然費(fèi)了這么大勁就解決了這么點(diǎn)小事氓奈,但是Go語言的優(yōu)勢(shì)是很大的翘魄,還是很值得我來折騰的,相信能讀到這里的朋友也是對(duì)Go語言非常的喜愛舀奶,大家一起加油吧熟丸,歡迎各位大佬來指正批評(píng)~

感謝作者:LemonITCN
原文鏈接:https://studygolang.com/articles/13646#reply1

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市伪节,隨后出現(xiàn)的幾起案子光羞,更是在濱河造成了極大的恐慌,老刑警劉巖怀大,帶你破解...
    沈念sama閱讀 216,496評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件纱兑,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡化借,警方通過查閱死者的電腦和手機(jī)潜慎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蓖康,“玉大人铐炫,你說我怎么就攤上這事∷夂福” “怎么了倒信?”我有些...
    開封第一講書人閱讀 162,632評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長泳梆。 經(jīng)常有香客問我鳖悠,道長榜掌,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,180評(píng)論 1 292
  • 正文 為了忘掉前任乘综,我火速辦了婚禮憎账,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘卡辰。我一直安慰自己胞皱,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評(píng)論 6 388
  • 文/花漫 我一把揭開白布九妈。 她就那樣靜靜地躺著反砌,像睡著了一般。 火紅的嫁衣襯著肌膚如雪允蚣。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,165評(píng)論 1 299
  • 那天呆贿,我揣著相機(jī)與錄音嚷兔,去河邊找鬼。 笑死做入,一個(gè)胖子當(dāng)著我的面吹牛冒晰,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播竟块,決...
    沈念sama閱讀 40,052評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼壶运,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了浪秘?” 一聲冷哼從身側(cè)響起蒋情,我...
    開封第一講書人閱讀 38,910評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎耸携,沒想到半個(gè)月后棵癣,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,324評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡夺衍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評(píng)論 2 332
  • 正文 我和宋清朗相戀三年狈谊,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片沟沙。...
    茶點(diǎn)故事閱讀 39,711評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡河劝,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出矛紫,到底是詐尸還是另有隱情赎瞎,我是刑警寧澤,帶...
    沈念sama閱讀 35,424評(píng)論 5 343
  • 正文 年R本政府宣布颊咬,位于F島的核電站煎娇,受9級(jí)特大地震影響二庵,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜缓呛,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評(píng)論 3 326
  • 文/蒙蒙 一催享、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧哟绊,春花似錦因妙、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至洽沟,卻和暖如春以故,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背裆操。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評(píng)論 1 269
  • 我被黑心中介騙來泰國打工怒详, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人踪区。 一個(gè)月前我還...
    沈念sama閱讀 47,722評(píng)論 2 368
  • 正文 我出身青樓昆烁,卻偏偏與公主長得像,于是被迫代替她去往敵國和親缎岗。 傳聞我的和親對(duì)象是個(gè)殘疾皇子静尼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容