AssemblyScript介紹2

買賣虛擬貨幣

上一篇文章從整體上討論了AssemblyScript(後文簡稱AS)程式如何被編譯成WebAssembly(後文簡稱Wasm)模組,詳細介紹了AS語言各種要素如何對映到Wasm二進位制模組的各個段。這一篇文章將調整焦距,把焦點對準函式。我們將討論AS編譯器如何使用Wasm指令集來實現各種語法要素。在開始之前,我們先簡單回顧一下Wasm指令集(關於Wasm模組和指令集的詳細介紹可以參考之前的系列文章):

Wasm採用棧式虛擬機器(Stack Based Virtual Machine)以及位元組碼(Bytecode),其指令可以分為五大類:

控制指令(Control Instructions),包括結構化控制指令、跳轉指令、函式呼叫指令等。

引數指令(Parametric Instructions),只有兩條:drop和select。

變數指令(Variable Instructions),包括區域性變數指令和全域性變數指令。

記憶體指令(Memory Instructions),包括儲存指令、載入指令等。

數值指令(Numeric Instructions),包括常量指令、測試指令、比較指令、一元運算指令、二元運算指令、型別轉換指令。

接下來將結合例項程式碼詳細介紹AS編譯器如何利用這五類指令。為了便於測試,後文給出的部分示例程式碼呼叫了外部函式。這些外部函式只是為了配合示例程式碼,因此它們的實現並不重要。下面統一給出這些外部函式的宣告:

declare function printI32(n: i32): void;declare function printI64(n: i64): void;declare function printF32(n: f32): void;declare function printF64(n: f64): void;declare function randomI32(): i32;

控制指令

如前所述,Wasm控制指令包括結構化控制指令(block、loop、if-else)、跳轉指令(br、br_if、br_table、return)、函式呼叫指令(call、call_indirerct),以及nop和unreachable。其中結構化控制指令和跳轉指令配合可以實現AS語言的各種控制語句,例如if-else語句、for迴圈語句、switch-case語句等。call指令可以實現AS函式呼叫,call_indirerct指令則可以支援一等函式(First-class Function)。

AS語言的if-else語句可以直接使用Wasm的if-else指令實現,下面是一個例子:

export function printEven(n: i32): void {  if (n  % 2 == 0) {    printI32(1);  } else {    printI32(0);  }}

下面是編譯結果(已經將編譯後的函式位元組碼反編譯為WAT,後文不再贅述):

(func $printEven (type 0) (param i32)  (if    (i32.rem_s (local.get 0) (i32.const 2))    (then (call $printI32 (i32.const 0)))    (else (call $printI32 (i32.const 1)))  ))

上面例子也展示了call指令的用法,後面就不再單獨介紹了。順便說一下,一些簡單的if-else語句會被AS編譯器最佳化為select指令,下面是一個例子:

export function max(a: i32, b: i32): i32 {  if (a > b) {    return a;  } else {    return b;  }}

下面是編譯結果:

(func $max (type 2) (param i32 i32) (result i32)  (select    (local.get 0)    (local.get 1)    (i32.gt_s (local.get 0) (local.get 1))  ))

AS語言的for、while、do-while等迴圈語句可以用Wasm的loop指令實現。注意loop指令並不能自動形成迴圈,所以必須要和br、br_if或br_table跳轉指令一起使用。下面來看一個稍微複雜一點的例子:

export function printNums(n: i32): void {  for (let i: i32 = 0; i < n; i++) {    printI32(i);    if (i == 100) {      break;    }  }}

這個例子展示了loop、block、br和br_if指令的用法,下面是編譯結果:

(func $printNums (type 0) (param i32)  (local i32)  (loop  ;; label = @1    (if  ;; label = @2      (i32.lt_s (local.get 1) (local.get 0))      (then        (block  ;; label = @3          (call $printI32 (local.get 1))          (br_if 0 (;@3;)            (i32.eq (local.get 1) (i32.const 100)))          (local.set 1            (i32.add (local.get 1) (i32.const 1)))          (br 2 (;@1;))        ) ;; end of block      ) ;; end of then    ) ;; end of if  ) ;; end of loop)

AS語言的switch-case語句可以用Wasm的br_table指令來實現,下面是一個例子:

export function mul100(n: i32): i32 {  switch (n) {    case 1: return 100;    case 2: return 200;    case 3: return 300;    default: return n * 100;  }}

除了br_table指令,這個例子還展示了return指令的用法,下面是編譯結果:

(func $mul100 (type 1) (param i32) (result i32)  (block  ;; label = @1    (block  ;; label = @2      (block  ;; label = @3        (block  ;; label = @4          (br_table 0 (;@4;) 1 (;@3;) 2 (;@2;) 3 (;@1;)            (i32.sub (local.get 0) (i32.const 1))))        (return (i32.const 100)))      (return (i32.const 200)))    (return (i32.const 300)))  (i32.mul (local.get 0) (i32.const 100)))

AS語言裡的一等函式,和C/C++等語言中函式指標概念比較類似,可以用call_indirect指令實現。下面來看一個例子:

type OP = (a: i32, b: i32) => i32;function add(a: i32, b: i32): i32 { return a + b; }function sub(a: i32, b: i32): i32 { return a - b; }function mul(a: i32, b: i32): i32 { return a * b; }function div(a: i32, b: i32): i32 { return a / b; }export function calc(a: i32, b: i32, op: i32): i32 {  return getOp(op)(a, b);}function getOp(op: i32): OP {  switch (op) {    case 1: return add;    case 2: return sub;    case 3: return mul;    case 4: return div;    default: return add;  }}

下面是編譯結果,請注意觀察table和elem段的內容,以及calc()函式位元組碼:

(module  (type (;0;) (func (param i32 i32) (result i32)))  (type (;1;) (func (param i32) (result i32)))  (type (;2;) (func (param i32 i32 i32) (result i32)))  (func $add (type 0) (i32.add (local.get 0) (local.get 1)))  (func $sub (type 0) (i32.sub (local.get 0) (local.get 1)))  (func $mul (type 0) (i32.mul (local.get 0) (local.get 1)))  (func $div (type 0) (i32.div_s (local.get 0) (local.get 1)))  (func $getOp (type 1) (param i32) (result i32) (;; 省略 ;;))  (func $calc (type 2) (param i32 i32 i32) (result i32)    (call_indirect (type 0)      (local.get 0)      (local.get 1)      (call $getOp (local.get 2))    )  )  (table (;0;) 5 funcref)  (memory (;0;) 0)  (export "memory" (memory 0))  (export "calc" (func $calc))  (export "getOp" (func $getOp))  (elem (;0;) (i32.const 1) func $add $sub $mul $div))

最後讓我們來看看unreachable指令。AS計劃在Wasm異常處理提案透過後再支援異常處理,目前丟擲異常會導致abort()函式被呼叫。我們可以透過新增編譯器選項--use abort=來禁用abort,這樣編譯器就會將abort()函式呼叫替換為一條unreachable指令。除此之外,我們也可以透過直接呼叫低階的unreachable()函式來顯式插入一條unreachable指令,下面是一個例子:

export function crash2(): void {  unreachable();}

編譯結果也很簡單:

(func $crash2 (type 1)  (unreachable))

引數指令

引數指令較為簡單,只有drop和select兩條。其中select指令在前面介紹if-else語句時已經提到過了,這裡就不再單獨介紹了。drop指令可以將運算元棧頂多餘的運算元彈出扔掉,下面來看一個簡單的例子:

export function dropRandom(): void {  randomI32();}

編譯結果也很簡單:

(func $dropRandom (type 0)  (drop (call $randomI32)))

變數指令

區域性變數指令共三條:local.get、local.set和local.tee。如果不考慮最佳化,每個AS函式都可以被編譯器編譯成一個Wasm函式。函式引數和區域性變數的讀寫操作可以透過區域性變數指令來完成,下面來看一個例子:

export function addLocals(a: i32, b: i32): i32 {  let c: i32 = a + b;  return c;}

下面是編譯結果(為了便於觀察結果,在編譯部分示例程式碼時關閉了編譯器最佳化,後文不再贅述):

(func $addLocals (type 1) (param i32 i32) (result i32)  (local i32)  (local.set 2 (i32.add (local.get 0) (local.get 1)))  (local.get 2))

全域性變數指令只有兩條:global.get和global.set。AS語言的全域性變數可以直接用Wasm全域性變數來實現,全域性變數的讀寫操作可以透過全域性變數指令來完成,下面來看一個例子:

let a: i32;let b: i32;let c: i32;export function addGlobals(): void {  c = a + b;}

下面是完整的編譯結果:

(module  (type (;0;) (func))  (func $addGlobals (type 0)    (global.set 2 (i32.add (global.get 0) (global.get 1)))  )  (global (;0;) (mut i32) (i32.const 0))  (global (;1;) (mut i32) (i32.const 0))  (global (;2;) (mut i32) (i32.const 0))  (export "addGlobals" (func $addGlobals)))

記憶體指令

Wasm虛擬機器可以附帶一塊虛擬記憶體,並且提供了豐富的指令來操作這塊記憶體。其中load系列指令可以從記憶體載入資料,放入操作樹棧。store系列指令可以從運算元棧拿出資料,存入記憶體。此外,透過memory.size指令可以獲取記憶體的當前頁數,透過memory.grow指令可以按頁擴充套件記憶體。我們將透過一個簡單的結構體來幫助我們觀察記憶體指令的使用,下面是這個結構體的定義:

class S {  a: i8; b: u8; c: i16; d: u16; e: i32; f: u32; g: i64; h: u64;  i: f32; j: f64;}

下面這個函式展示了i32型別load指令的用法:

export function loadI32(s: S): void {  printI32(s.a as i32); // i32.load8_s  printI32(s.b as i32); // i32.load8_u  printI32(s.c as i32); // i32.load16_s  printI32(s.d as i32); // i32.load16_u  printI32(s.e as i32); // i32.load  printI32(s.f as i32); // i32.load}

下面是編譯結果。透過load指令的offset立即數可以看出,AS編譯器並沒有對結構體欄位進行重新排列,但是進行了適當的對齊。

(func $loadI32 (type 0) (param i32)  (call $printI32 (i32.load8_s            (local.get 0)))  (call $printI32 (i32.load8_u  offset=1  (local.get 0)))  (call $printI32 (i32.load16_s offset=2  (local.get 0)))  (call $printI32 (i32.load16_u offset=4  (local.get 0)))  (call $printI32 (i32.load     offset=8  (local.get 0)))  (call $printI32 (i32.load     offset=12 (local.get 0))))

下面這個函式展示了i64型別load指令的用法:

export function loadI64(s: S): void {  printI64(s.a as i64); // i64.load8_s?  printI64(s.b as i64); // i64.load8_u?  printI64(s.c as i64); // i64.load16_s?  printI64(s.d as i64); // i64.load16_u?  printI64(s.e as i64); // i64.load32_s?  printI64(s.f as i64); // i64.load32_u?  printI64(s.g as i64); // i64.load  printI64(s.h as i64); // i64.load}

下面是編譯結果。可以看到,預期使用i64型別load指令的地方,AS編譯器使用了i32型別load指令並透過extend指令進行整數拉昇。

(func $loadI64 (type 0) (param i32)  (call $printI64 (i64.extend_i32_s (i32.load8_s            (local.get 0))))  (call $printI64 (i64.extend_i32_u (i32.load8_u  offset=1  (local.get 0))))  (call $printI64 (i64.extend_i32_s (i32.load16_s offset=2  (local.get 0))))  (call $printI64 (i64.extend_i32_u (i32.load16_u offset=4  (local.get 0))))  (call $printI64 (i64.extend_i32_s (i32.load     offset=8  (local.get 0))))  (call $printI64 (i64.extend_i32_u (i32.load     offset=12 (local.get 0))))  (call $printI64 (i64.load offset=16 (local.get 0)))  (call $printI64 (i64.load offset=24 (local.get 0))))

下面這個函式展示了float型別load指令的用法:

export function loadF(s: S): void {  printF32(s.i); // f32.load  printF64(s.j); // f64.load}

下面是編譯結果:

(func $loadF (type 0) (param i32)  (call $printF32 (f32.load offset=32 (local.get 0)))  (call $printF64 (f64.load offset=40 (local.get 0))))

相比load指令,store指令較為簡單。下面的例子展示了store指令的用法:

export function store(s: S, v: i64): void {  s.a = v as i8;  // i32.store8  s.b = v as u8;  // i32.store8  s.c = v as i16; // i32.store16  s.d = v as u16; // i32.store16  s.e = v as i32; // i32.store  s.f = v as u32; // i32.store  s.g = v as i64; // i64.store  s.h = v as u64; // i64.store  s.i = v as f32; // f32.store  s.j = v as f64; // f64.store}

下面是編譯結果:

(func $store (type 1) (param i32 i64)  (i32.store8            (local.get 0) (i32.wrap_i64 (local.get 1)))  (i32.store8  offset=1  (local.get 0) (i32.wrap_i64 (local.get 1)))  (i32.store16 offset=2  (local.get 0) (i32.wrap_i64 (local.get 1)))  (i32.store16 offset=4  (local.get 0) (i32.wrap_i64 (local.get 1)))  (i32.store   offset=8  (local.get 0) (i32.wrap_i64 (local.get 1)))  (i32.store   offset=12 (local.get 0) (i32.wrap_i64 (local.get 1)))  (i64.store   offset=16 (local.get 0) (local.get 1))  (i64.store   offset=24 (local.get 0) (local.get 1))  (f32.store   offset=32 (local.get 0) (f32.convert_i64_s (local.get 1)))  (f64.store   offset=40 (local.get 0) (f64.convert_i64_s (local.get 1))))

和前面介紹過的unreachable指令一樣,memory.size和memory.grow指令也可以透過內建函式來生成,下面是一個簡單的例子:

export function sizeAndGrow(n: i32): void {  printI32(memory.size());  printI32(memory.grow(n));}

下面是編譯結果:

(func $sizeAndGrow (type 0) (param i32)  (call $printI32 (memory.size))  (call $printI32 (memory.grow (local.get 0))))

數值指令

如前文所述,數值指令又可以分為常量指令、測試指令、比較指令、一元和二元運算指令,以及型別轉換指令。其中常量指令共四條,AS語言裡的數值字面量(Literals)可以用常量指令實現,下面是一個例子:

export function consts(): void {  printI32(1234); // i32.const  printI64(5678); // i64.const  printF32(3.14); // f32.const  printF64(2.71); // f64.const}

下面是編譯結果:

(func consts (type 1)  (call $printI32 (i32.const 1234))  (call $printI64 (i64.const 5678))  (call $printF32 (f32.const 0x1.91eb86p+1 (;=3.14;)))  (call $printF64 (f64.const 0x1.5ae147ae147aep+1 (;=2.71;))))

測試指令只有兩條:i32.eqz和i64.eqz。下面的例子展示了i32.eqz指令的用法:

export function testOps(a: i32): void {  if (a == 0) { // i32.eqz    printI32(123);  }}

下面是編譯結果:

(func $testOps (type 0) (param i32)  (if (i32.eqz (local.get 0))    (then (call $printI32 (i32.const 123)))  ))

AS語言支援的關係運算子可以用比較指令實現,下面的例子展示了i32型別比較指令的用法:

export function relOps(a: i32, b: i32, c: u32, d:  u32): void {  if (a == b) { printI32(0); } // i32.eq  if (a != b) { printI32(1); } // i32.ne  if (a <  b) { printI32(2); } // i32.lt_s  if (c <  d) { printI32(3); } // i32.lt_u  if (a >  b) { printI32(4); } // i32.gt_s  if (c >  d) { printI32(5); } // i32.gt_u  if (a <= b) { printI32(6); } // i32.le_s  if (c <= d) { printI32(7); } // i32.le_u  if (a >= b) { printI32(8); } // i32.ge_s  if (c >= d) { printI32(9); } // i32.ge_u}

下面是編譯結果:

(func relOps (type 2) (param i32 i32 i32 i32)  (if (i32.eq (local.get 0) (local.get 1))    (then (call $printI32 (i32.const 0))))  (if (i32.ne (local.get 0) (local.get 1))    (then (call $printI32 (i32.const 1))))  (if (i32.lt_s (local.get 0) (local.get 1))    (then (call $printI32 (i32.const 2))))  (if (i32.lt_u (local.get 2) (local.get 3))    (then (call $printI32 (i32.const 3))))  (if (i32.gt_s (local.get 0) (local.get 1))    (then (call $printI32 (i32.const 4))))  (if (i32.gt_u (local.get 2) (local.get 3))    (then (call $printI32 (i32.const 5))))  (if (i32.le_s (local.get 0) (local.get 1))    (then (call $printI32 (i32.const 6))))  (if (i32.le_u (local.get 2) (local.get 3))    (then (call $printI32 (i32.const 7))))  (if (i32.ge_s (local.get 0) (local.get 1))    (then (call $printI32 (i32.const 8))))  (if (i32.ge_u (local.get 2) (local.get 3))    (then (call $printI32 (i32.const 9)))))

除了浮點數取反運算以外,其他一元運算指令並沒有直接被AS編譯器使用,但是可以透過內建函式生成。下面的例子展示了i32和f32型別一元運算指令的用法:

export function unOps(a: i32, b: f32): void {  printI32(clz<i32>(a));     // i32.clz  printI32(ctz<i32>(a));     // i32.ctz  printI32(popcnt<i32>(a));  // i32.popcnt  printF32(abs<f32>(b));     // f32.abs  printF32(-b);              // f32.neg  printF32(sqrt<f32>(b));    // f32.sqrt  printF32(floor<f32>(b));   // f32.floor  printF32(trunc<f32>(b));   // f32.trunc  printF32(nearest<f32>(b)); // f32.nearest}

下面是編譯結果:

(func unOps (type 3) (param i32 f32 f32)  (call $printI32 (i32.clz     (local.get 0)))  (call $printI32 (i32.ctz     (local.get 0)))  (call $printI32 (i32.popcnt  (local.get 0)))  (call $printF32 (f32.abs     (local.get 1)))  (call $printF32 (f32.neg     (local.get 1)))  (call $printF32 (f32.sqrt    (local.get 1)))  (call $printF32 (f32.floor   (local.get 1)))  (call $printF32 (f32.trunc   (local.get 1)))  (call $printF32 (f32.nearest (local.get 1))))

AS語言支援的二元運算子可以用二元運算指令實現,下面的例子展示了i32型別二元運算指令的用法:

export function binOps(a: i32, b: i32, c: u32, d: u32, e: f32, f: f32): void {  printI32(a + b);           // i32.add  printI32(a - b);           // i32.sub  printI32(a * b);           // i32.mul  printI32(a / b);           // i32.div_s  printI32(c / d);           // i32.div_u  printI32(a % b);           // i32.rem_s  printI32(c % d);           // i32.rem_u  printI32(a & b);           // i32.and  printI32(a | b);           // i32.or  printI32(a ^ b);           // i32.xor  printI32(a << b);          // i32.shl  printI32(a >> b);          // i32.shr_s  printI32(a >>> b);         // i32.shr_u  printI32(rotl<i32>(a, b)); // i32.rotl  printI32(rotr<i32>(a, b)); // i32.rotr}

由於AS語言沒有“迴圈位移”運算子,所以我們只能透過內建函式來生成迴圈位移指令。下面是編譯結果:

(func binOps (type 3) (param i32 i32 i32 i32 f32 f32)  (call $printI32 (i32.add      (local.get 0) (local.get 1)))  (call $printI32 (i32.sub      (local.get 0) (local.get 1)))  (call $printI32 (i32.mul      (local.get 0) (local.get 1)))  (call $printI32 (i32.div_s    (local.get 0) (local.get 1)))  (call $printI32 (i32.div_s    (local.get 2) (local.get 3)))  (call $printI32 (i32.rem_s    (local.get 0) (local.get 1)))  (call $printI32 (i32.rem_s    (local.get 2) (local.get 3)))  (call $printI32 (i32.and      (local.get 0) (local.get 1)))  (call $printI32 (i32.or       (local.get 0) (local.get 1)))  (call $printI32 (i32.xor      (local.get 0) (local.get 1)))  (call $printI32 (i32.shl      (local.get 0) (local.get 1)))  (call $printI32 (i32.shr_s    (local.get 0) (local.get 1)))  (call $printI32 (i32.shr_u    (local.get 0) (local.get 1)))  (call $printI32 (i32.rotl     (local.get 0) (local.get 1)))  (call $printI32 (i32.rotr     (local.get 0) (local.get 1))))

AS語言中的型別轉換操作可以透過型別轉換指令實現,下面是一個例子:

export function cvtOps(a: i32, b: i64, c: u32, d: u64, e: f32, f: f64): void {  printI32(b as i32); // i32.wrap_i64  printI32(e as i32); // i32.trunc_f32_s  printI32(e as u32); // i32.trunc_f32_u  printI32(f as i32); // i32.trunc_f64_s  printI32(f as u32); // i32.trunc_f64_u  printI64(a);        // i64.extend_i32_s  printI64(a as u32); // i64.extend_i32_u  printI64(e as i64); // i64.trunc_f32_s  printI64(e as u64); // i64.trunc_f32_u  printI64(f as i64); // i64.trunc_f64_s  printI64(f as u64); // i64.trunc_f64_u  printF32(a as f32); // f32.convert_i32_s  printF32(c as f32); // f32.convert_i32_u  printF32(b as f32); // f32.convert_i64_s  printF32(d as f32); // f32.convert_i64_u  printF32(f as f32); // f32.demote_f64  printF64(a as f64); // f64.convert_i32_s  printF64(c as f64); // f64.convert_i32_u  printF64(b as f64); // f64.convert_i64_s  printF64(d as f64); // f64.convert_i64_u  printF64(e);        // f64.promote_f32  printI32(reinterpret<i32>(e)); // i32.reinterpret_f32  printI64(reinterpret<i64>(f)); // i64.reinterpret_f64  printF32(reinterpret<f32>(a)); // f32.reinterpret_i32  printF64(reinterpret<f64>(b)); // f64.reinterpret_i64}

下面是編譯結果:

(func cvtOps (type 4) (param i32 i64 i32 i64 f32 f64)  (call $printI32 (i32.wrap_i64        (local.get 1)))  (call $printI32 (i32.trunc_f32_s     (local.get 4)))  (call $printI32 (i32.trunc_f32_u     (local.get 4)))  (call $printI32 (i32.trunc_f64_s     (local.get 5)))  (call $printI32 (i32.trunc_f64_u     (local.get 5)))  (call $printI64 (i64.extend_i32_s    (local.get 0)))  (call $printI64 (i64.extend_i32_u    (local.get 0)))  (call $printI64 (i64.trunc_f32_s     (local.get 4)))  (call $printI64 (i64.trunc_f32_u     (local.get 4)))  (call $printI64 (i64.trunc_f64_s     (local.get 5)))  (call $printI64 (i64.trunc_f64_u     (local.get 5)))  (call $printF32 (f32.convert_i32_s   (local.get 0)))  (call $printF32 (f32.convert_i32_u   (local.get 2)))  (call $printF32 (f32.convert_i64_s   (local.get 1)))  (call $printF32 (f32.convert_i64_u   (local.get 3)))  (call $printF32 (f32.demote_f64      (local.get 5)))  (call $printF64 (f64.convert_i32_s   (local.get 0)))  (call $printF64 (f64.convert_i32_u   (local.get 2)))  (call $printF64 (f64.convert_i64_s   (local.get 1)))  (call $printF64 (f64.convert_i64_u   (local.get 3)))  (call $printF64 (f64.promote_f32     (local.get 4)))  (call $printI32 (i32.reinterpret_f32 (local.get 4)))  (call $printI64 (i64.reinterpret_f64 (local.get 5)))  (call $printF32 (f32.reinterpret_i32 (local.get 0)))  (call $printF64 (f64.reinterpret_i64 (local.get 1))))

總結

本文討論了AS編譯器如何透過各種Wasm指令來實現AS語法要素,簡單來說:各種控制結構透過控制指令來實現、區域性變數和全域性變數的讀寫透過變數指令來實現、記憶體操作透過記憶體指令來實現、各種運算子和型別轉換透過數值指令來實現。在後面的文章中,我們還將深入討論AS如何實現物件導向程式設計和自動記憶體管理。

*本文由CoinEx Chain開發團隊成員Chase撰寫。CoinEx Chain是全球首條基於Tendermint共識協議和Cosmos SDK開發的DEX專用公鏈,藉助IBC來實現DEX公鏈、智慧合約鏈、隱私鏈三條鏈合一的方式去解決可擴充套件性(Scalability)、去中心化(Decentralization)、安全性(security)區塊鏈不可能三角的問題,能夠高效能的支援數字資產的交易以及基於智慧合約的Defi應用。

免責聲明:

  1. 本文版權歸原作者所有,僅代表作者本人觀點,不代表鏈報觀點或立場。
  2. 如發現文章、圖片等侵權行爲,侵權責任將由作者本人承擔。
  3. 鏈報僅提供相關項目信息,不構成任何投資建議

推荐阅读

;