1 | /* |
2 | * Copyright 2020 Google LLC |
3 | * |
4 | * Use of this source code is governed by a BSD-style license that can be |
5 | * found in the LICENSE file. |
6 | */ |
7 | |
8 | #include "src/sksl/SkSLDehydrator.h" |
9 | |
10 | #include "src/sksl/SkSLRehydrator.h" |
11 | #include "src/sksl/ir/SkSLBinaryExpression.h" |
12 | #include "src/sksl/ir/SkSLBreakStatement.h" |
13 | #include "src/sksl/ir/SkSLConstructor.h" |
14 | #include "src/sksl/ir/SkSLContinueStatement.h" |
15 | #include "src/sksl/ir/SkSLDiscardStatement.h" |
16 | #include "src/sksl/ir/SkSLDoStatement.h" |
17 | #include "src/sksl/ir/SkSLEnum.h" |
18 | #include "src/sksl/ir/SkSLExpressionStatement.h" |
19 | #include "src/sksl/ir/SkSLField.h" |
20 | #include "src/sksl/ir/SkSLFieldAccess.h" |
21 | #include "src/sksl/ir/SkSLForStatement.h" |
22 | #include "src/sksl/ir/SkSLFunctionCall.h" |
23 | #include "src/sksl/ir/SkSLFunctionDeclaration.h" |
24 | #include "src/sksl/ir/SkSLFunctionDefinition.h" |
25 | #include "src/sksl/ir/SkSLIfStatement.h" |
26 | #include "src/sksl/ir/SkSLIndexExpression.h" |
27 | #include "src/sksl/ir/SkSLIntLiteral.h" |
28 | #include "src/sksl/ir/SkSLInterfaceBlock.h" |
29 | #include "src/sksl/ir/SkSLNullLiteral.h" |
30 | #include "src/sksl/ir/SkSLPostfixExpression.h" |
31 | #include "src/sksl/ir/SkSLPrefixExpression.h" |
32 | #include "src/sksl/ir/SkSLProgramElement.h" |
33 | #include "src/sksl/ir/SkSLReturnStatement.h" |
34 | #include "src/sksl/ir/SkSLSetting.h" |
35 | #include "src/sksl/ir/SkSLStatement.h" |
36 | #include "src/sksl/ir/SkSLSwitchCase.h" |
37 | #include "src/sksl/ir/SkSLSwitchStatement.h" |
38 | #include "src/sksl/ir/SkSLSwizzle.h" |
39 | #include "src/sksl/ir/SkSLSymbol.h" |
40 | #include "src/sksl/ir/SkSLSymbolTable.h" |
41 | #include "src/sksl/ir/SkSLTernaryExpression.h" |
42 | #include "src/sksl/ir/SkSLUnresolvedFunction.h" |
43 | #include "src/sksl/ir/SkSLVarDeclarations.h" |
44 | #include "src/sksl/ir/SkSLVarDeclarationsStatement.h" |
45 | #include "src/sksl/ir/SkSLVariable.h" |
46 | #include "src/sksl/ir/SkSLWhileStatement.h" |
47 | |
48 | #ifdef SKSL_STANDALONE |
49 | |
50 | namespace SkSL { |
51 | |
52 | static constexpr int HEADER_SIZE = 2; |
53 | |
54 | class AutoDehydratorSymbolTable { |
55 | public: |
56 | AutoDehydratorSymbolTable(Dehydrator* dehydrator, const std::shared_ptr<SymbolTable>& symbols) |
57 | : fDehydrator(dehydrator) { |
58 | dehydrator->fSymbolMap.emplace_back(); |
59 | if (symbols) { |
60 | dehydrator->write(*symbols); |
61 | } else { |
62 | dehydrator->writeU8(Rehydrator::kVoid_Command); |
63 | } |
64 | } |
65 | |
66 | ~AutoDehydratorSymbolTable() { |
67 | fDehydrator->fSymbolMap.pop_back(); |
68 | } |
69 | |
70 | private: |
71 | Dehydrator* fDehydrator; |
72 | }; |
73 | |
74 | void Dehydrator::write(Layout l) { |
75 | if (l == Layout()) { |
76 | this->writeU8(Rehydrator::kDefaultLayout_Command); |
77 | } else if (l == Layout::builtin(l.fBuiltin)) { |
78 | this->writeS8(Rehydrator::kBuiltinLayout_Command); |
79 | this->writeS16(l.fBuiltin); |
80 | } else { |
81 | this->writeS8(Rehydrator::kLayout_Command); |
82 | fBody.write32(l.fFlags); |
83 | this->writeS8(l.fLocation); |
84 | this->writeS8(l.fOffset); |
85 | this->writeS8(l.fBinding); |
86 | this->writeS8(l.fIndex); |
87 | this->writeS8(l.fSet); |
88 | this->writeS16(l.fBuiltin); |
89 | this->writeS8(l.fInputAttachmentIndex); |
90 | this->writeS8((int) l.fFormat); |
91 | this->writeS8(l.fPrimitive); |
92 | this->writeS8(l.fMaxVertices); |
93 | this->writeS8(l.fInvocations); |
94 | this->write(l.fMarker); |
95 | this->write(l.fWhen); |
96 | this->writeS8(l.fKey); |
97 | this->writeS8((int) l.fCType); |
98 | } |
99 | } |
100 | |
101 | void Dehydrator::write(Modifiers m) { |
102 | if (m == Modifiers()) { |
103 | this->writeU8(Rehydrator::kDefaultModifiers_Command); |
104 | } else { |
105 | if (m.fFlags <= 255) { |
106 | this->writeU8(Rehydrator::kModifiers8Bit_Command); |
107 | this->write(m.fLayout); |
108 | this->writeU8(m.fFlags); |
109 | } else { |
110 | this->writeU8(Rehydrator::kModifiers_Command); |
111 | this->write(m.fLayout); |
112 | this->writeS32(m.fFlags); |
113 | } |
114 | } |
115 | } |
116 | |
117 | void Dehydrator::write(StringFragment s) { |
118 | this->write(String(s)); |
119 | } |
120 | |
121 | void Dehydrator::write(String s) { |
122 | auto found = fStrings.find(s); |
123 | int offset; |
124 | if (found == fStrings.end()) { |
125 | offset = fStringBuffer.str().length() + HEADER_SIZE; |
126 | fStrings.insert({ s, offset }); |
127 | SkASSERT(s.length() <= 255); |
128 | fStringBuffer.write8(s.length()); |
129 | fStringBuffer.writeString(s); |
130 | } else { |
131 | offset = found->second; |
132 | } |
133 | this->writeU16(offset); |
134 | } |
135 | |
136 | void Dehydrator::write(const Symbol& s) { |
137 | uint16_t id = this->symbolId(&s, false); |
138 | if (id) { |
139 | this->writeU8(Rehydrator::kSymbolRef_Command); |
140 | this->writeU16(id); |
141 | return; |
142 | } |
143 | switch (s.fKind) { |
144 | case Symbol::kFunctionDeclaration_Kind: { |
145 | const FunctionDeclaration& f = (const FunctionDeclaration&) s; |
146 | this->writeU8(Rehydrator::kFunctionDeclaration_Command); |
147 | this->writeId(&f); |
148 | this->write(f.fModifiers); |
149 | this->write(f.fName); |
150 | this->writeU8(f.fParameters.size()); |
151 | for (const Variable* p : f.fParameters) { |
152 | this->writeU16(this->symbolId(p)); |
153 | } |
154 | this->write(f.fReturnType); |
155 | break; |
156 | } |
157 | case Symbol::kUnresolvedFunction_Kind: { |
158 | const UnresolvedFunction& f = (const UnresolvedFunction&) s; |
159 | this->writeU8(Rehydrator::kUnresolvedFunction_Command); |
160 | this->writeId(&f); |
161 | this->writeU8(f.fFunctions.size()); |
162 | for (const FunctionDeclaration* f : f.fFunctions) { |
163 | this->write(*f); |
164 | } |
165 | break; |
166 | } |
167 | case Symbol::kType_Kind: { |
168 | const Type& t = (const Type&) s; |
169 | switch (t.kind()) { |
170 | case Type::kArray_Kind: |
171 | this->writeU8(Rehydrator::kArrayType_Command); |
172 | this->writeId(&t); |
173 | this->write(t.componentType()); |
174 | this->writeU8(t.columns()); |
175 | break; |
176 | case Type::kEnum_Kind: |
177 | this->writeU8(Rehydrator::kEnumType_Command); |
178 | this->writeId(&t); |
179 | this->write(t.fName); |
180 | break; |
181 | case Type::kNullable_Kind: |
182 | this->writeU8(Rehydrator::kNullableType_Command); |
183 | this->writeId(&t); |
184 | this->write(t.componentType()); |
185 | break; |
186 | case Type::kStruct_Kind: |
187 | this->writeU8(Rehydrator::kStructType_Command); |
188 | this->writeId(&t); |
189 | this->write(t.fName); |
190 | this->writeU8(t.fields().size()); |
191 | for (const Type::Field& f : t.fields()) { |
192 | this->write(f.fModifiers); |
193 | this->write(f.fName); |
194 | this->write(*f.fType); |
195 | } |
196 | break; |
197 | default: |
198 | this->writeU8(Rehydrator::kSystemType_Command); |
199 | this->writeId(&t); |
200 | this->write(t.fName); |
201 | } |
202 | break; |
203 | } |
204 | case Symbol::kVariable_Kind: { |
205 | Variable& v = (Variable&) s; |
206 | this->writeU8(Rehydrator::kVariable_Command); |
207 | this->writeId(&v); |
208 | this->write(v.fModifiers); |
209 | this->write(v.fName); |
210 | this->write(v.fType); |
211 | this->writeU8(v.fStorage); |
212 | break; |
213 | } |
214 | case Symbol::kField_Kind: { |
215 | Field& f = (Field&) s; |
216 | this->writeU8(Rehydrator::kField_Command); |
217 | this->writeU16(this->symbolId(&f.fOwner)); |
218 | this->writeU8(f.fFieldIndex); |
219 | break; |
220 | } |
221 | case Symbol::kExternal_Kind: |
222 | SkASSERT(false); |
223 | break; |
224 | } |
225 | } |
226 | |
227 | void Dehydrator::write(const SymbolTable& symbols) { |
228 | this->writeU8(Rehydrator::kSymbolTable_Command); |
229 | this->writeU16(symbols.fOwnedSymbols.size()); |
230 | for (const std::unique_ptr<const Symbol>& s : symbols.fOwnedSymbols) { |
231 | this->write(*s); |
232 | } |
233 | this->writeU16(symbols.fSymbols.size()); |
234 | std::map<StringFragment, const Symbol*> ordered; |
235 | for (std::pair<StringFragment, const Symbol*> p : symbols.fSymbols) { |
236 | ordered.insert(p); |
237 | } |
238 | for (std::pair<StringFragment, const Symbol*> p : ordered) { |
239 | this->write(p.first); |
240 | bool found = false; |
241 | for (size_t i = 0; i < symbols.fOwnedSymbols.size(); ++i) { |
242 | if (symbols.fOwnedSymbols[i].get() == p.second) { |
243 | this->writeU16(i); |
244 | found = true; |
245 | break; |
246 | } |
247 | } |
248 | SkASSERT(found); |
249 | } |
250 | } |
251 | |
252 | void Dehydrator::write(const Expression* e) { |
253 | if (e) { |
254 | switch (e->fKind) { |
255 | case Expression::kBinary_Kind: { |
256 | BinaryExpression& b = (BinaryExpression&) *e; |
257 | this->writeU8(Rehydrator::kBinary_Command); |
258 | this->write(b.fLeft.get()); |
259 | this->writeU8((int) b.fOperator); |
260 | this->write(b.fRight.get()); |
261 | this->write(b.fType); |
262 | break; |
263 | } |
264 | case Expression::kBoolLiteral_Kind: { |
265 | BoolLiteral& b = (BoolLiteral&) *e; |
266 | this->writeU8(Rehydrator::kBoolLiteral_Command); |
267 | this->writeU8(b.fValue); |
268 | break; |
269 | } |
270 | case Expression::kConstructor_Kind: { |
271 | Constructor& c = (Constructor&) *e; |
272 | this->writeU8(Rehydrator::kConstructor_Command); |
273 | this->write(c.fType); |
274 | this->writeU8(c.fArguments.size()); |
275 | for (const auto& a : c.fArguments) { |
276 | this->write(a.get()); |
277 | } |
278 | break; |
279 | } |
280 | case Expression::kExternalFunctionCall_Kind: |
281 | case Expression::kExternalValue_Kind: |
282 | // not implemented; doesn't seem like we'll ever need them from within an include |
283 | // file |
284 | SkASSERT(false); |
285 | break; |
286 | case Expression::kFieldAccess_Kind: { |
287 | FieldAccess& f = (FieldAccess&) *e; |
288 | this->writeU8(Rehydrator::kFieldAccess_Command); |
289 | this->write(f.fBase.get()); |
290 | this->writeU8(f.fFieldIndex); |
291 | this->writeU8(f.fOwnerKind); |
292 | break; |
293 | } |
294 | case Expression::kFloatLiteral_Kind: { |
295 | FloatLiteral& f = (FloatLiteral&) *e; |
296 | this->writeU8(Rehydrator::kFloatLiteral_Command); |
297 | FloatIntUnion u; |
298 | u.fFloat = f.fValue; |
299 | this->writeS32(u.fInt); |
300 | break; |
301 | } |
302 | case Expression::kFunctionCall_Kind: { |
303 | FunctionCall& f = (FunctionCall&) *e; |
304 | this->writeU8(Rehydrator::kFunctionCall_Command); |
305 | this->write(f.fType); |
306 | this->writeId(&f.fFunction); |
307 | this->writeU8(f.fArguments.size()); |
308 | for (const auto& a : f.fArguments) { |
309 | this->write(a.get()); |
310 | } |
311 | break; |
312 | } |
313 | case Expression::kIndex_Kind: { |
314 | IndexExpression& i = (IndexExpression&) *e; |
315 | this->writeU8(Rehydrator::kIndex_Command); |
316 | this->write(i.fBase.get()); |
317 | this->write(i.fIndex.get()); |
318 | break; |
319 | } |
320 | case Expression::kIntLiteral_Kind: { |
321 | IntLiteral& i = (IntLiteral&) *e; |
322 | this->writeU8(Rehydrator::kIntLiteral_Command); |
323 | this->writeS32(i.fValue); |
324 | break; |
325 | } |
326 | case Expression::kNullLiteral_Kind: |
327 | this->writeU8(Rehydrator::kNullLiteral_Command); |
328 | break; |
329 | case Expression::kPostfix_Kind: { |
330 | PostfixExpression& p = (PostfixExpression&) *e; |
331 | this->writeU8(Rehydrator::kPostfix_Command); |
332 | this->writeU8((int) p.fOperator); |
333 | this->write(p.fOperand.get()); |
334 | break; |
335 | } |
336 | case Expression::kPrefix_Kind: { |
337 | PrefixExpression& p = (PrefixExpression&) *e; |
338 | this->writeU8(Rehydrator::kPrefix_Command); |
339 | this->writeU8((int) p.fOperator); |
340 | this->write(p.fOperand.get()); |
341 | break; |
342 | } |
343 | case Expression::kSetting_Kind: { |
344 | Setting& s = (Setting&) *e; |
345 | this->writeU8(Rehydrator::kSetting_Command); |
346 | this->write(s.fName); |
347 | this->write(s.fValue.get()); |
348 | break; |
349 | } |
350 | case Expression::kSwizzle_Kind: { |
351 | Swizzle& s = (Swizzle&) *e; |
352 | this->writeU8(Rehydrator::kSwizzle_Command); |
353 | this->write(s.fBase.get()); |
354 | this->writeU8(s.fComponents.size()); |
355 | for (int c : s.fComponents) { |
356 | this->writeU8(c); |
357 | } |
358 | break; |
359 | } |
360 | case Expression::kTernary_Kind: { |
361 | TernaryExpression& t = (TernaryExpression&) *e; |
362 | this->writeU8(Rehydrator::kTernary_Command); |
363 | this->write(t.fTest.get()); |
364 | this->write(t.fIfTrue.get()); |
365 | this->write(t.fIfFalse.get()); |
366 | break; |
367 | } |
368 | case Expression::kVariableReference_Kind: { |
369 | VariableReference& v = (VariableReference&) *e; |
370 | this->writeU8(Rehydrator::kVariableReference_Command); |
371 | this->writeId(&v.fVariable); |
372 | this->writeU8(v.fRefKind); |
373 | break; |
374 | } |
375 | case Expression::kFunctionReference_Kind: |
376 | case Expression::kTypeReference_Kind: |
377 | case Expression::kDefined_Kind: |
378 | // shouldn't appear in finished code |
379 | SkASSERT(false); |
380 | break; |
381 | } |
382 | } else { |
383 | this->writeU8(Rehydrator::kVoid_Command); |
384 | } |
385 | } |
386 | |
387 | void Dehydrator::write(const Statement* s) { |
388 | if (s) { |
389 | switch (s->fKind) { |
390 | case Statement::kBlock_Kind: { |
391 | Block& b = (Block&) *s; |
392 | this->writeU8(Rehydrator::kBlock_Command); |
393 | AutoDehydratorSymbolTable symbols(this, b.fSymbols); |
394 | this->writeU8(b.fStatements.size()); |
395 | for (const auto& s : b.fStatements) { |
396 | this->write(s.get()); |
397 | } |
398 | this->writeU8(b.fIsScope); |
399 | break; |
400 | } |
401 | case Statement::kBreak_Kind: |
402 | this->writeU8(Rehydrator::kBreak_Command); |
403 | break; |
404 | case Statement::kContinue_Kind: |
405 | this->writeU8(Rehydrator::kContinue_Command); |
406 | break; |
407 | case Statement::kDiscard_Kind: |
408 | this->writeU8(Rehydrator::kDiscard_Command); |
409 | break; |
410 | case Statement::kDo_Kind: { |
411 | DoStatement& d = (DoStatement&) *s; |
412 | this->writeU8(Rehydrator::kDo_Command); |
413 | this->write(d.fStatement.get()); |
414 | this->write(d.fTest.get()); |
415 | break; |
416 | } |
417 | case Statement::kExpression_Kind: { |
418 | ExpressionStatement& e = (ExpressionStatement&) *s; |
419 | this->writeU8(Rehydrator::kExpressionStatement_Command); |
420 | this->write(e.fExpression.get()); |
421 | break; |
422 | } |
423 | case Statement::kFor_Kind: { |
424 | ForStatement& f = (ForStatement&) *s; |
425 | this->writeU8(Rehydrator::kFor_Command); |
426 | this->write(f.fInitializer.get()); |
427 | this->write(f.fTest.get()); |
428 | this->write(f.fNext.get()); |
429 | this->write(f.fStatement.get()); |
430 | this->write(f.fSymbols); |
431 | break; |
432 | } |
433 | case Statement::kIf_Kind: { |
434 | IfStatement& i = (IfStatement&) *s; |
435 | this->writeU8(Rehydrator::kIf_Command); |
436 | this->writeU8(i.fIsStatic); |
437 | this->write(i.fTest.get()); |
438 | this->write(i.fIfTrue.get()); |
439 | this->write(i.fIfFalse.get()); |
440 | break; |
441 | } |
442 | case Statement::kNop_Kind: |
443 | SkASSERT(false); |
444 | break; |
445 | case Statement::kReturn_Kind: { |
446 | ReturnStatement& r = (ReturnStatement&) *s; |
447 | this->writeU8(Rehydrator::kReturn_Command); |
448 | this->write(r.fExpression.get()); |
449 | break; |
450 | } |
451 | case Statement::kSwitch_Kind: { |
452 | SwitchStatement& ss = (SwitchStatement&) *s; |
453 | this->writeU8(Rehydrator::kSwitch_Command); |
454 | this->writeU8(ss.fIsStatic); |
455 | AutoDehydratorSymbolTable symbols(this, ss.fSymbols); |
456 | this->write(ss.fValue.get()); |
457 | this->writeU8(ss.fCases.size()); |
458 | for (const auto& sc : ss.fCases) { |
459 | this->write(sc->fValue.get()); |
460 | this->writeU8(sc->fStatements.size()); |
461 | for (const auto& stmt : sc->fStatements) { |
462 | this->write(stmt.get()); |
463 | } |
464 | } |
465 | break; |
466 | } |
467 | case Statement::kVarDeclaration_Kind: { |
468 | VarDeclaration& v = (VarDeclaration&) *s; |
469 | this->writeU8(Rehydrator::kVarDeclaration_Command); |
470 | this->writeU16(this->symbolId(v.fVar)); |
471 | this->writeU8(v.fSizes.size()); |
472 | for (const auto& s : v.fSizes) { |
473 | this->write(s.get()); |
474 | } |
475 | this->write(v.fValue.get()); |
476 | break; |
477 | } |
478 | case Statement::kVarDeclarations_Kind: { |
479 | VarDeclarationsStatement& v = (VarDeclarationsStatement&) *s; |
480 | this->write(*v.fDeclaration); |
481 | break; |
482 | } |
483 | case Statement::kWhile_Kind: { |
484 | WhileStatement& w = (WhileStatement&) *s; |
485 | this->writeU8(Rehydrator::kWhile_Command); |
486 | this->write(w.fTest.get()); |
487 | this->write(w.fStatement.get()); |
488 | break; |
489 | } |
490 | } |
491 | } else { |
492 | this->writeU8(Rehydrator::kVoid_Command); |
493 | } |
494 | } |
495 | |
496 | void Dehydrator::write(const ProgramElement& e) { |
497 | switch (e.fKind) { |
498 | case ProgramElement::kEnum_Kind: { |
499 | Enum& en = (Enum&) e; |
500 | this->writeU8(Rehydrator::kEnum_Command); |
501 | this->write(en.fTypeName); |
502 | AutoDehydratorSymbolTable symbols(this, en.fSymbols); |
503 | for (const auto& s : en.fSymbols->fOwnedSymbols) { |
504 | SkASSERT(s->fKind == Symbol::kVariable_Kind); |
505 | Variable& v = (Variable&) *s; |
506 | SkASSERT(v.fInitialValue && |
507 | v.fInitialValue->fKind == Expression::kIntLiteral_Kind); |
508 | IntLiteral& i = (IntLiteral&) *v.fInitialValue; |
509 | this->writeS32(i.fValue); |
510 | } |
511 | break; |
512 | } |
513 | case ProgramElement::kExtension_Kind: |
514 | SkASSERT(false); |
515 | break; |
516 | case ProgramElement::kFunction_Kind: { |
517 | FunctionDefinition& f = (FunctionDefinition&) e; |
518 | this->writeU8(Rehydrator::kFunctionDefinition_Command); |
519 | this->writeU16(this->symbolId(&f.fDeclaration)); |
520 | this->write(f.fBody.get()); |
521 | this->writeU8(f.fReferencedIntrinsics.size()); |
522 | std::set<uint16_t> ordered; |
523 | for (const FunctionDeclaration* ref : f.fReferencedIntrinsics) { |
524 | ordered.insert(this->symbolId(ref)); |
525 | } |
526 | for (uint16_t ref : ordered) { |
527 | this->writeU16(ref); |
528 | } |
529 | break; |
530 | } |
531 | case ProgramElement::kInterfaceBlock_Kind: { |
532 | InterfaceBlock& i = (InterfaceBlock&) e; |
533 | this->writeU8(Rehydrator::kInterfaceBlock_Command); |
534 | this->write(i.fVariable); |
535 | this->write(i.fTypeName); |
536 | this->write(i.fInstanceName); |
537 | this->writeU8(i.fSizes.size()); |
538 | for (const auto& s : i.fSizes) { |
539 | this->write(s.get()); |
540 | } |
541 | break; |
542 | } |
543 | case ProgramElement::kModifiers_Kind: |
544 | SkASSERT(false); |
545 | break; |
546 | case ProgramElement::kSection_Kind: |
547 | SkASSERT(false); |
548 | break; |
549 | case ProgramElement::kVar_Kind: { |
550 | VarDeclarations& v = (VarDeclarations&) e; |
551 | this->writeU8(Rehydrator::kVarDeclarations_Command); |
552 | this->write(v.fBaseType); |
553 | this->writeU8(v.fVars.size()); |
554 | for (const auto& v : v.fVars) { |
555 | this->write(v.get()); |
556 | } |
557 | break; |
558 | } |
559 | } |
560 | } |
561 | |
562 | void Dehydrator::write(const std::vector<std::unique_ptr<ProgramElement>>& elements) { |
563 | this->writeU8(Rehydrator::kElements_Command); |
564 | this->writeU8(elements.size()); |
565 | for (const auto& e : elements) { |
566 | this->write(*e); |
567 | } |
568 | } |
569 | |
570 | void Dehydrator::finish(OutputStream& out) { |
571 | out.write16(fStringBuffer.str().size()); |
572 | out.writeString(fStringBuffer.str()); |
573 | out.writeString(fBody.str()); |
574 | } |
575 | |
576 | } // namespace |
577 | |
578 | #endif |
579 | |