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