《Chrome V8 源碼》41. Runtime_StringTrim 源碼、觸發條件
介紹
Runtime 是一系列采用 C++ 語言編寫的功能方法,它實現了大量 JavaScript 運行期間需要的 native 功能。接下來幾篇文章將介紹一些 Runtime 方法。本文分析 Runtime_StringTrim 方法的源碼和重要數據結構,講解 Runtime_StringTrim 方法的觸發條件。
注意: Runtime 方法的加載、調用以及 RUNTIME_FUNCTION 宏模板請參見第十六篇文章。—allow-natives-syntax 和 %-prefix 不是本文的講解重點。
StringTrim 測試用例
編寫可以觸發特定的 V8 內部功能的 JavaScript 測試用例,可以幫助我們更好地理解 V8 的內部工作原理,達到事半功倍的效果。下面講解 Runtime_StringTrim 測試用例的編寫思路:
字符串的 Trim 方法由 TF_BUILTIN(StringPrototypeTrim, StringTrimAssembler) 函數實現,這個函數設置了一些字符串檢測條件,如果滿足檢測條件就會啟動 Runtime_StringTrim 方法。因此,我們需要從 TF_BUILTIN(StringPrototypeTrim, StringTrimAssembler) 開始分析,源碼如下:
1. TF_BUILTIN(StringPrototypeTrim, StringTrimAssembler) {2. TNode<IntPtrT> argc =3. ChangeInt32ToIntPtr(Parameter(Descriptor::kJSActualArgumentsCount));4. TNode<Context> context = CAST(Parameter(Descriptor::kContext));5. Generate(String::kTrim, "String.prototype.trim", argc, context);6. }7. //分隔..................8. void StringTrimAssembler::Generate(String::TrimMode mode,9. const char* method_name, TNode<IntPtrT> argc,10. TNode<Context> context) {11. Label return_emptystring(this), if_runtime(this);12. CodeStubArguments arguments(this, argc);13. TNode<Object> receiver = arguments.GetReceiver();14. TNode<String> const string = ToThisString(context, receiver, method_name);15. TNode<IntPtrT> const string_length = LoadStringLengthAsWord(string);16. ToDirectStringAssembler to_direct(state(), string);17. to_direct.TryToDirect(&if_runtime);18. TNode<RawPtrT> const string_data = to_direct.PointerToData(&if_runtime);19. TNode<Int32T> const instance_type = to_direct.instance_type();20. TNode<BoolT> const is_stringonebyte =21. IsOneByteStringInstanceType(instance_type);22. TNode<IntPtrT> const string_data_offset = to_direct.offset();23. TVARIABLE(IntPtrT, var_start, IntPtrConstant(0));24. TVARIABLE(IntPtrT, var_end, IntPtrSub(string_length, IntPtrConstant(1)));25. //省略................26. arguments.PopAndReturn(27. SubString(string, var_start.value(),28. IntPtrAdd(var_end.value(), IntPtrConstant(1))));29. BIND(&if_runtime);30. arguments.PopAndReturn(31. CallRuntime(Runtime::kStringTrim, context, string, SmiConstant(mode)));32. BIND(&return_emptystring);33. arguments.PopAndReturn(EmptyStringConstant());34. }
上述代碼中,第 5 行代碼調用 Generate() 方法;
第 11 行代碼定義 runtime 標簽;
第 14-15 行代碼獲取字符串以及它的長度;
第 16-17 行 TryToDirect 把字符串轉換為直接字符串,如果 TryToDirect 失敗將采用 Runtime 方式處理;
第 29 行綁定 runtime 標簽;
第 31 行調用 Runtime::kStringTrim 方法。
runtime 標簽僅在第 17 行被使用一次,由此我們可知:構造一段 “TryToDirect 失敗” 的 JavaScript 源碼是觸發 Runtime 的條件。TryToDirect() 的原理和失敗條件在之前的文章中講過。V8 的字符串類型包括:SeqString、ConsString、SliceString、ThinString、ExternalString。直接給出結論:一個單字節串和兩個雙字節串組成的 ConsString 串可以導致 “TryToDirect 失敗”,源碼如下:
var str1 = " ~~~"; //前面有空格var str2 = "彼其之子、美如玉。";var str3 ="~~~ "; //后面有空格ConStr = str1+str2+str3;trimStr = ConStr.trim();console.log(trimStr);
圖 1 中可以看到 ConStr InstanceType 的值是 CONS_STRING_TYPE,它導致 “TryToDirect 失敗” 并啟動 Runtime。

StringTrim 源碼
源碼如下:
1. RUNTIME_FUNCTION(Runtime_StringTrim) {2. HandleScope scope(isolate);3. DCHECK_EQ(2, args.length());4. Handle string = args.at(0);5. CONVERT_SMI_ARG_CHECKED(mode, 1);6. String::TrimMode trim_mode = static_cast(mode);7. return *String::Trim(isolate, string, trim_mode);8. }9. //分隔線.............10. Handle String::Trim(Isolate* isolate, Handle string,11. TrimMode mode) {12. string = String::Flatten(isolate, string);13. int const length = string->length();14. // Perform left trimming if requested.15. int left = 0;16. if (mode == kTrim || mode == kTrimStart) {17. while (left < length && IsWhiteSpaceOrLineTerminator(string->Get(left))) {18. left++;19. }20. }21. // Perform right trimming if requested.22. int right = length;23. if (mode == kTrim || mode == kTrimEnd) {24. while (right > left &&25. IsWhiteSpaceOrLineTerminator(string->Get(right - 1))) {26. right--;27. }28. }29. return isolate->factory()->NewSubString(string, left, right);30. }
上述代碼中,第 4 行代碼獲取字符串,也就是測試用例的 ConStr;
第 6 行代碼調用 *String::Trim(isolate, string, trim_mode) 以完成 Trim 功能;
第 12 行代碼對 ConStr 進行 Flatten 處理,結果保存為連續存儲的字符串 string。因為 ConStr 由三個子串組成,所以 Flatten 方法中會使用遞歸調用來處理 ConStr,詳見上篇文章。
第 16-17 行代碼從 string 的頭部依次判斷每個字符是否為空格或行結尾符,記錄不是空格或行結尾符的位置 left;
第 24-26 行代碼從 string 的尾部依次判斷每個字符是否為空格或行結尾符,記錄不是空格或行結尾符的位置 right;
第 29 行代碼調用 NewSubString 生成新的字符串。正如 ECMA 所說的那樣:Trim 不會改變原字符串,而是生成新的字符串。
NewSubString 中調用 NewProperSubString 以生成最終的結果,NewProperSubString 源碼分析參見上一篇文章。
下面給出判斷空格和行結尾符的函數源碼:
bool IsWhiteSpaceOrLineTerminator(uc32 c) { if (!IsInRange(c, 0, 127)) return IsWhiteSpaceOrLineTerminatorSlow(c); DCHECK_EQ( IsWhiteSpaceOrLineTerminatorSlow(c), static_cast<bool>(kAsciiCharFlags[c] & kIsWhiteSpaceOrLineTerminator)); return kAsciiCharFlags[c] & kIsWhiteSpaceOrLineTerminator;}
首先判斷字符是否在 0-127 區間,如果不在區間內使用 Slow 方式判斷,源碼如下:
inline bool IsWhiteSpaceOrLineTerminatorSlow(uc32 c) { return IsWhiteSpaceSlow(c) || unibrow::IsLineTerminator(c);}//.....................分隔線................// ES#sec-white-space White Space// gC=Zs, U+0009, U+000B, U+000C, U+FEFFbool IsWhiteSpaceSlow(uc32 c) { return (u_charType(c) == U_SPACE_SEPARATOR) || (c < 0x0D && (c == 0x09 || c == 0x0B || c == 0x0C)) || c == 0xFEFF;}//....................分隔線...................// LineTerminator: 'JS_Line_Terminator' in point.properties// ES#sec-line-terminators lists exactly 4 code points:// LF (U+000A), CR (U+000D), LS(U+2028), PS(U+2029)V8_INLINE bool IsLineTerminator(uchar c) { return c == 0x000A || c == 0x000D || c == 0x2028 || c == 0x2029;}
上述代碼分為三部分,第二、三部實現 ECMA 規范,第一部分是他們的入口函數。
IsWhiteSpaceOrLineTerminator() 中的 kAsciiCharFlags 數組定義 Ascii 字符,kAsciiCharFlags 數組中又引用了 BuildAsciiCharFlags() 方法,該方法說明了 \t、\v 是空格、還是行結尾符,也就是 BuildAsciiCharFlags() 方法影響 Strint.trim() 的結果。源碼如下:
const constexpr uint8_t kAsciiCharFlags[128] = {#define BUILD_CHAR_FLAGS(N) BuildAsciiCharFlags(N), INT_0_TO_127_LIST(BUILD_CHAR_FLAGS)#undef BUILD_CHAR_FLAGS};//................分隔線.........................constexpr uint8_t BuildAsciiCharFlags(uc32 c) { return ((IsAsciiIdentifier(c) || c == '\\') ? (kIsIdentifierPart | (!IsDecimalDigit(c) ? kIsIdentifierStart : 0)) : 0) | ((c == ' ' || c == '\t' || c == '\v' || c == '\f') ? kIsWhiteSpace | kIsWhiteSpaceOrLineTerminator : 0) | ((c == '\r' || c == '') ? kIsWhiteSpaceOrLineTerminator : 0);}//...............分隔線.......................#define INT_0_TO_127_LIST(V) \V(0) V(1) V(2) V(3) V(4) V(5) V(6) V(7) V(8) V(9) \V(10) V(11) V(12) V(13) V(14) V(15) V(16) V(17) V(18) V(19) \V(20) V(21) V(22) V(23) V(24) V(25) V(26) V(27) V(28) V(29) \V(30) V(31) V(32) V(33) V(34) V(35) V(36) V(37) V(38) V(39) \V(40) V(41) V(42) V(43) V(44) V(45) V(46) V(47) V(48) V(49) \V(50) V(51) V(52) V(53) V(54) V(55) V(56) V(57) V(58) V(59) \V(60) V(61) V(62) V(63) V(64) V(65) V(66) V(67) V(68) V(69) \V(70) V(71) V(72) V(73) V(74) V(75) V(76) V(77) V(78) V(79) \V(80) V(81) V(82) V(83) V(84) V(85) V(86) V(87) V(88) V(89) \V(90) V(91) V(92) V(93) V(94) V(95) V(96) V(97) V(98) V(99) \V(100) V(101) V(102) V(103) V(104) V(105) V(106) V(107) V(108) V(109) \V(110) V(111) V(112) V(113) V(114) V(115) V(116) V(117) V(118) V(119) \V(120) V(121) V(122) V(123) V(124) V(125) V(126) V(127)
上述代碼分為三部分,他們共同完成 kAsciiCharFlags 數組的定義。
下面給出從字符串中讀取字符的函數源碼,也就是 IsWhiteSpaceOrLineTerminator(string->Get(left)) 中的 “Get” 方法,源碼如下:
uint16_t String::Get(int index) { DCHECK(index >= 0 && index < length());
class StringGetDispatcher : public AllStatic { public:#define DEFINE_METHOD(Type) \ static inline uint16_t Handle##Type(Type str, int index) { \ return str.Get(index); \ } STRING_CLASS_TYPES(DEFINE_METHOD)#undef DEFINE_METHOD static inline uint16_t HandleInvalidString(String str, int index) { UNREACHABLE(); } };
return StringShape(*this) .DispatchToSpecificTypeuint16_t>(*this, index);}
Get 方法用于讀取 index 位置的字符。從 String 中讀取字符時,要根據 String Header 的長度計算字符串的首位置,然后再加上 index 讀取相應的字符。
技術總結
(1) Runtime Trim 的效率比 TF_BUILTIN(StringPrototypeTrim) 低很多;
(2) 字符串的類型影響 TryToDirect 的成敗。
好了,今天到這里,下次見。
個人能力有限,有不足與紕漏,歡迎批評指正