1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one 3 * or more contributor license agreements. See the NOTICE file 4 * distributed with this work for additional information 5 * regarding copyright ownership. The ASF licenses this file 6 * to you under the Apache License, Version 2.0 (the 7 * "License"); you may not use this file except in compliance 8 * with the License. You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, 13 * software distributed under the License is distributed on an 14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 * KIND, either express or implied. See the License for the 16 * specific language governing permissions and limitations 17 * under the License. 18 */ 19 module thrift.codegen.client; 20 21 import std.algorithm : find; 22 import std.array : empty, front; 23 import std.conv : to; 24 import std.traits : isSomeFunction, ParameterStorageClass, 25 ParameterStorageClassTuple, ParameterTypeTuple, ReturnType; 26 import std.traits : isArray; 27 import thrift.codegen.base; 28 import thrift.internal.codegen; 29 import thrift.internal.ctfe; 30 import thrift.protocol.base; 31 32 /** 33 * Thrift service client, which implements an interface by synchronously 34 * calling a server over a TProtocol. 35 * 36 * TClientBase simply extends Interface with generic input/output protocol 37 * properties to serve as a supertype for all TClients for the same service, 38 * which might be instantiated with different concrete protocol types (there 39 * is no covariance for template type parameters). If Interface is derived 40 * from another interface BaseInterface, it also extends 41 * TClientBase!BaseInterface. 42 * 43 * TClient is the class that actually implements TClientBase. Just as 44 * TClientBase, it is also derived from TClient!BaseInterface for inheriting 45 * services. 46 * 47 * TClient takes two optional template arguments which can be used for 48 * specifying the actual TProtocol implementation used for optimization 49 * purposes, as virtual calls can completely be eliminated then. If 50 * OutputProtocol is not specified, it is assumed to be the same as 51 * InputProtocol. The protocol properties defined by TClientBase are exposed 52 * with their concrete type (return type covariance). 53 * 54 * In addition to implementing TClientBase!Interface, TClient offers the 55 * following constructors: 56 * --- 57 * this(InputProtocol iprot, OutputProtocol oprot); 58 * // Only if is(InputProtocol == OutputProtocol), to use the same protocol 59 * // for both input and output: 60 * this(InputProtocol prot); 61 * --- 62 * 63 * The sequence id of the method calls starts at zero and is automatically 64 * incremented. 65 */ 66 interface TClientBase(Interface) if (isBaseService!Interface) : Interface { 67 /** 68 * The input protocol used by the client. 69 */ 70 TProtocol inputProtocol() @property; 71 72 /** 73 * The output protocol used by the client. 74 */ 75 TProtocol outputProtocol() @property; 76 } 77 78 /// Ditto 79 interface TClientBase(Interface) if (isDerivedService!Interface) : 80 TClientBase!(BaseService!Interface), Interface {} 81 82 /// Ditto 83 template TClient(Interface, InputProtocol = TProtocol, OutputProtocol = void) if ( 84 isService!Interface && isTProtocol!InputProtocol && 85 (isTProtocol!OutputProtocol || is(OutputProtocol == void)) 86 ) { 87 mixin({ 88 static if (isDerivedService!Interface) { 89 string code = "class TClient : TClient!(BaseService!Interface, " ~ 90 "InputProtocol, OutputProtocol), TClientBase!Interface {\n"; 91 code ~= q{ 92 this(IProt iprot, OProt oprot) { 93 super(iprot, oprot); 94 } 95 96 static if (is(IProt == OProt)) { 97 this(IProt prot) { 98 super(prot); 99 } 100 } 101 102 // DMD @@BUG@@: If these are not present in this class (would be) 103 // inherited anyway, »not implemented« errors are raised. 104 override IProt inputProtocol() @property { 105 return super.inputProtocol; 106 } 107 override OProt outputProtocol() @property { 108 return super.outputProtocol; 109 } 110 }; 111 } else { 112 string code = "class TClient : TClientBase!Interface {"; 113 code ~= q{ 114 alias InputProtocol IProt; 115 static if (isTProtocol!OutputProtocol) { 116 alias OutputProtocol OProt; 117 } else { 118 static assert(is(OutputProtocol == void)); 119 alias InputProtocol OProt; 120 } 121 122 this(IProt iprot, OProt oprot) { 123 iprot_ = iprot; 124 oprot_ = oprot; 125 } 126 127 static if (is(IProt == OProt)) { 128 this(IProt prot) { 129 this(prot, prot); 130 } 131 } 132 133 IProt inputProtocol() @property { 134 return iprot_; 135 } 136 137 OProt outputProtocol() @property { 138 return oprot_; 139 } 140 141 protected IProt iprot_; 142 protected OProt oprot_; 143 protected int seqid_; 144 }; 145 } 146 147 foreach (methodName; __traits(derivedMembers, Interface)) { 148 static if (isSomeFunction!(mixin("Interface." ~ methodName))) { 149 bool methodMetaFound; 150 TMethodMeta methodMeta; 151 static if (is(typeof(Interface.methodMeta) : TMethodMeta[])) { 152 enum meta = find!`a.name == b`(Interface.methodMeta, methodName); 153 if (!meta.empty) { 154 methodMetaFound = true; 155 methodMeta = meta.front; 156 } 157 } 158 159 // Generate the code for sending. 160 string[] paramList; 161 string paramAssignCode; 162 foreach (i, _; ParameterTypeTuple!(mixin("Interface." ~ methodName))) { 163 // Use the param name speficied in the meta information if any – 164 // just cosmetics in this case. 165 string paramName; 166 if (methodMetaFound && i < methodMeta.params.length) { 167 paramName = methodMeta.params[i].name; 168 } else { 169 paramName = "param" ~ to!string(i + 1); 170 } 171 172 immutable storage = ParameterStorageClassTuple!( 173 mixin("Interface." ~ methodName))[i]; 174 paramList ~= ((storage & ParameterStorageClass.ref_) ? "ref " : "") ~ 175 "ParameterTypeTuple!(Interface." ~ methodName ~ ")[" ~ 176 to!string(i) ~ "] " ~ paramName; 177 paramAssignCode ~= "args." ~ paramName ~ " = &" ~ paramName ~ ";\n"; 178 } 179 code ~= "ReturnType!(Interface." ~ methodName ~ ") " ~ methodName ~ 180 "(" ~ ctfeJoin(paramList) ~ ") {\n"; 181 182 code ~= "immutable methodName = `" ~ methodName ~ "`;\n"; 183 184 immutable paramStructType = 185 "TPargsStruct!(Interface, `" ~ methodName ~ "`)"; 186 code ~= paramStructType ~ " args = " ~ paramStructType ~ "();\n"; 187 code ~= paramAssignCode; 188 code ~= "oprot_.writeMessageBegin(TMessage(`" ~ methodName ~ 189 "`, TMessageType.CALL, ++seqid_));\n"; 190 code ~= "args.write(oprot_);\n"; 191 code ~= "oprot_.writeMessageEnd();\n"; 192 code ~= "oprot_.transport.flush();\n"; 193 194 // If this is not a oneway method, generate the recieving code. 195 if (!methodMetaFound || methodMeta.type != TMethodType.ONEWAY) { 196 code ~= "TPresultStruct!(Interface, `" ~ methodName ~ "`) result;\n"; 197 198 if (!is(ReturnType!(mixin("Interface." ~ methodName)) == void)) { 199 code ~= "ReturnType!(Interface." ~ methodName ~ ") _return;\n"; 200 code ~= "result.success = &_return;\n"; 201 } 202 203 // TODO: The C++ implementation checks for matching method name here, 204 // should we do as well? 205 code ~= q{ 206 auto msg = iprot_.readMessageBegin(); 207 scope (exit) { 208 iprot_.readMessageEnd(); 209 iprot_.transport.readEnd(); 210 } 211 212 if (msg.type == TMessageType.EXCEPTION) { 213 auto x = new TApplicationException(null); 214 x.read(iprot_); 215 iprot_.transport.readEnd(); 216 throw x; 217 } 218 if (msg.type != TMessageType.REPLY) { 219 skip(iprot_, TType.STRUCT); 220 iprot_.transport.readEnd(); 221 } 222 if (msg.seqid != seqid_) { 223 throw new TApplicationException( 224 methodName ~ " failed: Out of sequence response.", 225 TApplicationException.Type.BAD_SEQUENCE_ID 226 ); 227 } 228 result.read(iprot_); 229 }; 230 231 if (methodMetaFound) { 232 foreach (e; methodMeta.exceptions) { 233 code ~= "if (result.isSet!`" ~ e.name ~ "`) throw result." ~ 234 e.name ~ ";\n"; 235 } 236 } 237 238 if (!is(ReturnType!(mixin("Interface." ~ methodName)) == void)) { 239 code ~= q{ 240 if (result.isSet!`success`) return _return; 241 throw new TApplicationException( 242 methodName ~ " failed: Unknown result.", 243 TApplicationException.Type.MISSING_RESULT 244 ); 245 }; 246 } 247 } 248 code ~= "}\n"; 249 } 250 } 251 252 code ~= "}\n"; 253 return code; 254 }()); 255 } 256 257 /** 258 * TClient construction helper to avoid having to explicitly specify 259 * the protocol types, i.e. to allow the constructor being called using IFTI 260 * (see $(DMDBUG 6082, D Bugzilla enhancement requet 6082)). 261 */ 262 TClient!(Interface, Prot) tClient(Interface, Prot)(Prot prot) if ( 263 isService!Interface && isTProtocol!Prot 264 ) { 265 return new TClient!(Interface, Prot)(prot); 266 } 267 268 /// Ditto 269 TClient!(Interface, IProt, Oprot) tClient(Interface, IProt, OProt) 270 (IProt iprot, OProt oprot) if ( 271 isService!Interface && isTProtocol!IProt && isTProtocol!OProt 272 ) { 273 return new TClient!(Interface, IProt, OProt)(iprot, oprot); 274 } 275 276 /** 277 * Represents the arguments of a Thrift method call, as pointers to the (const) 278 * parameter type to avoid copying. 279 * 280 * There should usually be no reason to use this struct directly without the 281 * help of TClient, but it is documented publicly to help debugging in case 282 * of CTFE errors. 283 * 284 * Consider this example: 285 * --- 286 * interface Foo { 287 * int bar(string a, bool b); 288 * 289 * enum methodMeta = [ 290 * TMethodMeta("bar", [TParamMeta("a", 1), TParamMeta("b", 2)]) 291 * ]; 292 * } 293 * 294 * alias TPargsStruct!(Foo, "bar") FooBarPargs; 295 * --- 296 * 297 * The definition of FooBarPargs is equivalent to (ignoring the necessary 298 * metadata to assign the field IDs): 299 * --- 300 * struct FooBarPargs { 301 * const(string)* a; 302 * const(bool)* b; 303 * 304 * void write(Protocol)(Protocol proto) const if (isTProtocol!Protocol); 305 * } 306 * --- 307 */ 308 template TPargsStruct(Interface, string methodName) { 309 static assert(is(typeof(mixin("Interface." ~ methodName))), 310 "Could not find method '" ~ methodName ~ "' in '" ~ Interface.stringof ~ "'."); 311 mixin({ 312 bool methodMetaFound; 313 TMethodMeta methodMeta; 314 static if (is(typeof(Interface.methodMeta) : TMethodMeta[])) { 315 auto meta = find!`a.name == b`(Interface.methodMeta, methodName); 316 if (!meta.empty) { 317 methodMetaFound = true; 318 methodMeta = meta.front; 319 } 320 } 321 322 string memberCode; 323 string[] fieldMetaCodes; 324 foreach (i, _; ParameterTypeTuple!(mixin("Interface." ~ methodName))) { 325 // If we have no meta information, just use param1, param2, etc. as 326 // field names, it shouldn't really matter anyway. 1-based »indexing« 327 // is used to match the common scheme in the Thrift world. 328 string memberId; 329 string memberName; 330 if (methodMetaFound && i < methodMeta.params.length) { 331 memberId = to!string(methodMeta.params[i].id); 332 memberName = methodMeta.params[i].name; 333 } else { 334 memberId = to!string(i + 1); 335 memberName = "param" ~ to!string(i + 1); 336 } 337 338 // Workaround for DMD @@BUG@@ 6056: make an intermediary alias for the 339 // parameter type, and declare the member using const(memberNameType)*. 340 memberCode ~= "alias ParameterTypeTuple!(Interface." ~ methodName ~ 341 ")[" ~ to!string(i) ~ "] " ~ memberName ~ "Type;\n"; 342 memberCode ~= "const(" ~ memberName ~ "Type)* " ~ memberName ~ ";\n"; 343 344 fieldMetaCodes ~= "TFieldMeta(`" ~ memberName ~ "`, " ~ memberId ~ 345 ", TReq.OPT_IN_REQ_OUT)"; 346 } 347 348 string code = "struct TPargsStruct {\n"; 349 code ~= memberCode; 350 version (TVerboseCodegen) { 351 if (!methodMetaFound && 352 ParameterTypeTuple!(mixin("Interface." ~ methodName)).length > 0) 353 { 354 code ~= "pragma(msg, `[thrift.codegen.base.TPargsStruct] Warning: No " ~ 355 "meta information for method '" ~ methodName ~ "' in service '" ~ 356 Interface.stringof ~ "' found.`);\n"; 357 } 358 } 359 code ~= "void write(P)(P proto) const if (isTProtocol!P) {\n"; 360 code ~= "writeStruct!(typeof(this), P, [" ~ ctfeJoin(fieldMetaCodes) ~ 361 "], true)(this, proto);\n"; 362 code ~= "}\n"; 363 code ~= "}\n"; 364 return code; 365 }()); 366 } 367 368 /** 369 * Represents the result of a Thrift method call, using a pointer to the return 370 * value to avoid copying. 371 * 372 * There should usually be no reason to use this struct directly without the 373 * help of TClient, but it is documented publicly to help debugging in case 374 * of CTFE errors. 375 * 376 * Consider this example: 377 * --- 378 * interface Foo { 379 * int bar(string a); 380 * 381 * alias .FooException FooException; 382 * 383 * enum methodMeta = [ 384 * TMethodMeta("bar", 385 * [TParamMeta("a", 1)], 386 * [TExceptionMeta("fooe", 1, "FooException")] 387 * ) 388 * ]; 389 * } 390 * alias TPresultStruct!(Foo, "bar") FooBarPresult; 391 * --- 392 * 393 * The definition of FooBarPresult is equivalent to (ignoring the necessary 394 * metadata to assign the field IDs): 395 * --- 396 * struct FooBarPresult { 397 * int* success; 398 * Foo.FooException fooe; 399 * 400 * struct IsSetFlags { 401 * bool success; 402 * } 403 * IsSetFlags isSetFlags; 404 * 405 * bool isSet(string fieldName)() const @property; 406 * void read(Protocol)(Protocol proto) if (isTProtocol!Protocol); 407 * } 408 * --- 409 */ 410 template TPresultStruct(Interface, string methodName) { 411 static assert(is(typeof(mixin("Interface." ~ methodName))), 412 "Could not find method '" ~ methodName ~ "' in '" ~ Interface.stringof ~ "'."); 413 414 mixin({ 415 string code = "struct TPresultStruct {\n"; 416 417 string[] fieldMetaCodes; 418 419 alias ReturnType!(mixin("Interface." ~ methodName)) ResultType; 420 static if (!is(ResultType == void)) { 421 code ~= q{ 422 ReturnType!(mixin("Interface." ~ methodName))* success; 423 }; 424 fieldMetaCodes ~= "TFieldMeta(`success`, 0, TReq.OPTIONAL)"; 425 426 static if (!isNullable!ResultType) { 427 code ~= q{ 428 struct IsSetFlags { 429 bool success; 430 } 431 IsSetFlags isSetFlags; 432 }; 433 fieldMetaCodes ~= "TFieldMeta(`isSetFlags`, 0, TReq.IGNORE)"; 434 } 435 } 436 437 bool methodMetaFound; 438 static if (is(typeof(Interface.methodMeta) : TMethodMeta[])) { 439 auto meta = find!`a.name == b`(Interface.methodMeta, methodName); 440 if (!meta.empty) { 441 foreach (e; meta.front.exceptions) { 442 code ~= "Interface." ~ e.type ~ " " ~ e.name ~ ";\n"; 443 fieldMetaCodes ~= "TFieldMeta(`" ~ e.name ~ "`, " ~ to!string(e.id) ~ 444 ", TReq.OPTIONAL)"; 445 } 446 methodMetaFound = true; 447 } 448 } 449 450 version (TVerboseCodegen) { 451 if (!methodMetaFound && 452 ParameterTypeTuple!(mixin("Interface." ~ methodName)).length > 0) 453 { 454 code ~= "pragma(msg, `[thrift.codegen.base.TPresultStruct] Warning: No " ~ 455 "meta information for method '" ~ methodName ~ "' in service '" ~ 456 Interface.stringof ~ "' found.`);\n"; 457 } 458 } 459 460 code ~= q{ 461 bool isSet(string fieldName)() const @property if ( 462 is(MemberType!(typeof(this), fieldName)) 463 ) { 464 static if (fieldName == "success") { 465 static if (isNullable!(typeof(*success))) { 466 //In D. empty array is null. Sometimes it's confused 467 static if (isArray!(typeof(*success))) { 468 return true; 469 } else { 470 return *success !is null; 471 } 472 } else { 473 return isSetFlags.success; 474 } 475 } else { 476 // We are dealing with an exception member, which, being a nullable 477 // type (exceptions are always classes), has no isSet flag. 478 return __traits(getMember, this, fieldName) !is null; 479 } 480 } 481 }; 482 483 code ~= "void read(P)(P proto) if (isTProtocol!P) {\n"; 484 code ~= "readStruct!(typeof(this), P, [" ~ ctfeJoin(fieldMetaCodes) ~ 485 "], true)(this, proto);\n"; 486 code ~= "}\n"; 487 code ~= "}\n"; 488 return code; 489 }()); 490 }