diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 844fed72..bfa00e90 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,28 +6,44 @@ on: pull_request: branches: [ master ] - jobs: build: runs-on: ubuntu-latest + env: + PKG_CONFIG_PATH: /home/runner/lib/pkgconfig steps: - uses: actions/checkout@v2 - - name: Install Cap'n Proto run: | export DEBIAN_FRONTEND=noninteractive - sudo apt-get install -y capnproto libcapnp-dev - - - name: Set up JDK 1.8 + env + sudo apt-get install -y gcc-7 g++-7 + curl -O https://capnproto.org/capnproto-c++-0.8.0.tar.gz + tar zxf capnproto-c++-0.8.0.tar.gz + cd capnproto-c++-0.8.0 + ./configure --prefix=$HOME CC=gcc-7 CXX=g++-7 + make -j + make install + cd .. + + - name: Set up JDK 14 uses: actions/setup-java@v1 with: - java-version: 1.8 + java-version: 14 - name: Build with Maven + env: + LD_LIBRARY_PATH: /home/runner/lib run: | - make - mvn compile + env + make CC=gcc-7 CXX=g++-7 + env PATH="${PATH}:/home/runner/bin" mvn -e -X compile - name: Run tests - run: mvn test + env: + LD_LIBRARY_PATH: /home/runner/lib + run: | + env PATH="${PATH}:/home/runner/bin" + env PATH="${PATH}:/home/runner/bin" mvn -e -X test + diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 00000000..b0988158 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,18 @@ +image: capnproto-gitlab-builder + +stages: + - build + - quality + +build: + stage: build + script: + - mvn -e -X clean compile + +test: + stage: quality + dependencies: + - build + script: + - mvn -e -X test + diff --git a/.run/Calculator.run.xml b/.run/Calculator.run.xml new file mode 100644 index 00000000..17b1ab4b --- /dev/null +++ b/.run/Calculator.run.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.run/CalculatorClient.run.xml b/.run/CalculatorClient.run.xml new file mode 100644 index 00000000..87af5d16 --- /dev/null +++ b/.run/CalculatorClient.run.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/.run/CalculatorServer.run.xml b/.run/CalculatorServer.run.xml new file mode 100644 index 00000000..d90a6d44 --- /dev/null +++ b/.run/CalculatorServer.run.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/README.md b/README.md index 88f19402..2123a2f3 100644 --- a/README.md +++ b/README.md @@ -6,3 +6,15 @@ and capabilities, and capnproto-java is a pure Java implementation. [Read more here.](https://dwrensha.github.io/capnproto-java/index.html) + +This repository clone adds an implementation of the RPC framework for Java. + +Promise pipelining is provided via java.util.concurrent.CompletableFuture. Unlike the KJ asynchronous model, which completes promises +only when they are waited upon, a CompletableFuture can complete immediately. This may break E-ordering, as the C++ implementation +relies on kj::evalLater() to defer method calls and this implementation may have subtle differences. + +Most of the C++ RPC test cases have been ported to this implementation, which gives me some comfort that the implementation logic is +correct, but more extensive testing is required. + +This implementation does not support generic interfaces. Extending the schema compiler to output code for generic interfaces is an +exercise I leave to the reader. diff --git a/benchmark/pom.xml b/benchmark/pom.xml index 9186ee28..03578d58 100644 --- a/benchmark/pom.xml +++ b/benchmark/pom.xml @@ -31,9 +31,9 @@ UTF-8 - 1.8 - 1.8 - 8 + 14 + 14 + 14 @@ -45,6 +45,16 @@ + + org.apache.maven.plugins + maven-compiler-plugin + 3.3 + + -Xlint:unchecked + 11 + 11 + + maven-antrun-plugin 3.0.0 diff --git a/compiler/pom.xml b/compiler/pom.xml index b5609283..f6cb60c9 100644 --- a/compiler/pom.xml +++ b/compiler/pom.xml @@ -41,7 +41,6 @@ 5.11.4 test - org.capnproto runtime @@ -56,6 +55,8 @@ maven-compiler-plugin 3.3 + 11 + 11 -Xlint:unchecked @@ -79,21 +80,21 @@ run - generate-test-sources generate-test-sources - + - + + diff --git a/compiler/src/main/cpp/capnpc-java.c++ b/compiler/src/main/cpp/capnpc-java.c++ index 16c21280..439ee1c2 100644 --- a/compiler/src/main/cpp/capnpc-java.c++ +++ b/compiler/src/main/cpp/capnpc-java.c++ @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -193,8 +194,9 @@ private: SchemaLoader schemaLoader; std::unordered_set usedImports; bool hasInterfaces = false; + bool liteMode = false; - kj::StringTree javaFullName(Schema schema) { + kj::StringTree javaFullName(Schema schema, kj::Maybe method = nullptr) { auto node = schema.getProto(); if (node.getScopeId() == 0) { usedImports.insert(node.getId()); @@ -240,10 +242,12 @@ private: Schema parent = schemaLoader.get(node.getScopeId()); result = getTypeArguments(leaf, parent, kj::str(suffix)); } - auto brandArguments = leaf.getBrandArgumentsAtScope(node.getId()); - auto parameters = node.getParameters(); - for (int ii = 0; ii < parameters.size(); ++ii) { - result.add(typeName(brandArguments[ii], kj::str(suffix)).flatten()); + if (node.getIsGeneric()) { + auto brandArguments = leaf.getBrandArgumentsAtScope(node.getId()); + auto parameters = node.getParameters(); + for (int ii = 0; ii < parameters.size(); ++ii) { + result.add(typeName(brandArguments[ii], kj::str(suffix)).flatten()); + } } return kj::mv(result); } @@ -326,9 +330,28 @@ private: return kj::strTree(javaFullName(type.asStruct()), ".", suffix); } } - case schema::Type::INTERFACE: - return javaFullName(type.asInterface()); - + case schema::Type::INTERFACE: { + if (liteMode) { + return kj::strTree("org.capnproto.Capability.", suffix); + } + auto interfaceSuffix = kj::str(suffix); + if(suffix == kj::str("Builder") || suffix == kj::str("Reader")) { + interfaceSuffix = kj::str("Client"); + } + auto interfaceSchema = type.asInterface(); + if (interfaceSchema.getProto().getIsGeneric()) { + auto typeArgs = getTypeArguments(interfaceSchema, interfaceSchema, kj::str(suffix)); + return kj::strTree( + javaFullName(interfaceSchema), ".", interfaceSuffix, "<", + kj::StringTree(KJ_MAP(arg, typeArgs){ + return kj::strTree(arg); + }, ", "), + ">" + ); + } else { + return kj::strTree(javaFullName(type.asInterface()), ".", interfaceSuffix); + } + } case schema::Type::LIST: { auto elementType = type.asList().getElementType(); @@ -373,6 +396,7 @@ private: return kj::strTree("org.capnproto.ListList.", suffix, "<", kj::mv(inner), ">"); } case schema::Type::INTERFACE: + return kj::strTree("org.capnproto.CapabilityList.", suffix); case schema::Type::ANY_POINTER: KJ_FAIL_REQUIRE("unimplemented"); } @@ -385,7 +409,16 @@ private: "_", kj::hex(brandParam->scopeId), "_", suffix); } else { - return kj::strTree("org.capnproto.AnyPointer.", suffix); + switch (type.whichAnyPointerKind()) { + case schema::Type::AnyPointer::Unconstrained::CAPABILITY: + return kj::strTree("org.capnproto.Capability.", suffix); + case schema::Type::AnyPointer::Unconstrained::STRUCT: + return kj::strTree("org.capnproto.AnyStruct.", suffix); + case schema::Type::AnyPointer::Unconstrained::LIST: + return kj::strTree("org.capnproto.AnyList.", suffix); + default: + return kj::strTree("org.capnproto.AnyPointer.", suffix); + } } } } @@ -703,6 +736,7 @@ private: struct FieldText { kj::StringTree readerMethodDecls; kj::StringTree builderMethodDecls; + kj::StringTree pipelineMethodDecls; }; enum class FieldKind { @@ -711,7 +745,8 @@ private: STRUCT, LIST, INTERFACE, - ANY_POINTER + ANY_POINTER, + BRAND_PARAMETER }; kj::StringTree makeEnumGetter(EnumSchema schema, uint offset, kj::String defaultMaskParam, int indent) { @@ -743,7 +778,16 @@ private: "_", kj::hex(brandParam->scopeId), "_Factory"); } else { - return kj::str("org.capnproto.AnyPointer.factory"); + switch (type.whichAnyPointerKind()) { + case schema::Type::AnyPointer::Unconstrained::CAPABILITY: + return kj::str("org.capnproto.Capability.factory"); + case schema::Type::AnyPointer::Unconstrained::STRUCT: + return kj::str("org.capnproto.AnyStruct.factory"); + case schema::Type::AnyPointer::Unconstrained::LIST: + return kj::str("org.capnproto.AnyList.factory"); + default: + return kj::str("org.capnproto.AnyPointer.factory"); + } } } case schema::Type::STRUCT : { @@ -802,6 +846,23 @@ private: return kj::str(typeName(type, kj::str("factory"))); } } + case schema::Type::INTERFACE: { + auto interfaceSchema = type.asInterface(); + auto node = interfaceSchema.getProto(); + if (node.getIsGeneric()) { + auto factoryArgs = getFactoryArguments(interfaceSchema, interfaceSchema); + return kj::strTree( + javaFullName(interfaceSchema), ".newFactory(", + kj::StringTree( + KJ_MAP(arg, factoryArgs) { + return kj::strTree(arg); + }, ","), + ")" + ).flatten(); + } else { + return kj::str(typeName(type, kj::str("factory"))); + } + } default: KJ_UNREACHABLE; } @@ -861,7 +922,15 @@ private: " return new ", scope, titleCase, ".Builder(segment, data, pointers, dataSize, pointerCount);\n", spaces(indent), " }\n", - "\n") + "\n"), + + (hasDiscriminantValue(proto) || liteMode) + ? kj::strTree() + : kj::strTree( + spaces(indent), " default ", titleCase, ".Pipeline get", titleCase, "() {\n", + spaces(indent), " var pipeline = this.typelessPipeline().noop();\n", + spaces(indent), " return () -> pipeline;\n", + spaces(indent), " }\n") }; } } @@ -962,10 +1031,28 @@ private: kind = FieldKind::INTERFACE; break; case schema::Type::ANY_POINTER: - kind = FieldKind::ANY_POINTER; if (defaultBody.hasAnyPointer()) { defaultOffset = field.getDefaultValueSchemaOffset(); } + if (field.getType().getBrandParameter() != nullptr && false) { + kind = FieldKind::BRAND_PARAMETER; + } else { + kind = FieldKind::ANY_POINTER; + switch (field.getType().whichAnyPointerKind()) { + case schema::Type::AnyPointer::Unconstrained::ANY_KIND: + kind = FieldKind::ANY_POINTER; + break; + case schema::Type::AnyPointer::Unconstrained::STRUCT: + kind = FieldKind::ANY_POINTER; + break; + case schema::Type::AnyPointer::Unconstrained::LIST: + kind = FieldKind::ANY_POINTER; + break; + case schema::Type::AnyPointer::Unconstrained::CAPABILITY: + kind = FieldKind::INTERFACE; + break; + } + } break; } @@ -1018,8 +1105,57 @@ private: }; } else if (kind == FieldKind::INTERFACE) { - KJ_FAIL_REQUIRE("interfaces unimplemented"); + if (liteMode) { + return {}; + } + auto factoryArg = makeFactoryArg(field.getType()); + auto clientType = typeName(field.getType(), kj::str("Client")).flatten(); + auto serverType = typeName(field.getType(), kj::str("Server")).flatten(); + + return FieldText { + kj::strTree( + kj::mv(unionDiscrim.readerIsDef), + spaces(indent), " public boolean has", titleCase, "() {\n", + unionDiscrim.has, + spaces(indent), " return !_pointerFieldIsNull(", offset, ");\n", + spaces(indent), " }\n", + spaces(indent), " public ", clientType, " get", titleCase, "() {\n", + unionDiscrim.check, + spaces(indent), " return _getPointerField(", factoryArg, ", ", offset, ");\n", + spaces(indent), " }\n"), + + kj::strTree( + kj::mv(unionDiscrim.builderIsDef), + spaces(indent), " public final boolean has", titleCase, "() {\n", + spaces(indent), " return !_pointerFieldIsNull(", offset, ");\n", + spaces(indent), " }\n", + + spaces(indent), " public ", clientType, " get", titleCase, "() {\n", + unionDiscrim.check, + spaces(indent), " return _getPointerField(", factoryArg, ", ", offset, ");\n", + spaces(indent), " }\n", + + spaces(indent), " public void set", titleCase, "(", clientType, " value) {\n", + unionDiscrim.set, + spaces(indent), " _setPointerField(", factoryArg, ", ", offset, ", value);\n", + spaces(indent), " }\n", + spaces(indent), " public void set", titleCase, "(", serverType, " value) {\n", + spaces(indent), " this.set", titleCase, "(new ", clientType, "(value));\n", + spaces(indent), " }\n", + spaces(indent), " public void set", titleCase, "(java.util.concurrent.CompletableFuture value) {\n", + spaces(indent), " this.set", titleCase, "(new ", clientType, "(value));\n", + spaces(indent), " }\n" + ), + + kj::strTree( + spaces(indent), " default ", clientType, " get", titleCase, "() {\n", + spaces(indent), " return new ", clientType, "(\n", + spaces(indent), " this.typelessPipeline().getPointerField((short)", offset, ").asCap()\n", + spaces(indent), " );\n", + spaces(indent), " }\n" + ) + }; } else if (kind == FieldKind::ANY_POINTER) { auto factoryArg = makeFactoryArg(field.getType()); @@ -1078,6 +1214,24 @@ private: auto typeParamVec = getTypeParameters(field.getContainingStruct()); auto factoryArg = makeFactoryArg(field.getType()); + kj::String pipelineType; + if (field.getType().asStruct().getProto().getIsGeneric()) { + auto typeArgs = getTypeArguments(structSchema, structSchema, kj::str("Reader")); + if (typeArgs.size() > 0) { + pipelineType = kj::strTree( + javaFullName(structSchema), ".Pipeline<", + kj::StringTree(KJ_MAP(arg, typeArgs){ + return kj::strTree(arg); + }, ", "), + ">").flatten(); + } + else { + pipelineType = typeName(field.getType(), kj::str("Pipeline")).flatten(); + } + } else { + pipelineType = typeName(field.getType(), kj::str("Pipeline")).flatten(); + } + return FieldText { kj::strTree( kj::mv(unionDiscrim.readerIsDef), @@ -1126,6 +1280,15 @@ private: spaces(indent), " return ", "_initPointerField(", factoryArg, ",", offset, ", 0);\n", spaces(indent), " }\n"), + + // Pipeline accessors + ((liteMode || field.getType().asStruct().getProto().getIsGeneric()) + ? kj::strTree() // No generics for you, sorry. + : kj::strTree( + spaces(indent), " default ", pipelineType, " get", titleCase, "() {\n", + spaces(indent), " var pipeline = this.typelessPipeline().getPointerField((short)", offset, ");\n", + spaces(indent), " return () -> pipeline;\n", + spaces(indent), " }\n")) }; } else if (kind == FieldKind::BLOB) { @@ -1306,8 +1469,6 @@ private: spaces(indent), " }\n") ) ), - - }; } else { KJ_UNREACHABLE; @@ -1393,6 +1554,14 @@ private: return kj::strTree(spaces(indent), " final org.capnproto.PointerFactory<", p, "_Builder, ", p, "_Reader> ", p, "_Factory;\n"); }); + kj::String factoryRef = hasTypeParams + ? kj::str(kj::strTree("newFactory(", + kj::StringTree(KJ_MAP(p, typeParamVec) { + return kj::strTree(p, "_Factory"); + }, ", ") + , ")")) + : kj::str("factory"); + return StructText { kj::strTree( spaces(indent), "public static class ", name, " {\n", @@ -1437,7 +1606,6 @@ private: (hasTypeParams ? kj::strTree("this") : kj::strTree()), ");\n", spaces(indent), " }\n", - spaces(indent), " }\n", (hasTypeParams ? kj::strTree( @@ -1523,15 +1691,388 @@ private: spaces(indent), " _NOT_IN_SCHEMA,\n", spaces(indent), " }\n"), KJ_MAP(n, nestedTypeDecls) { return kj::mv(n); }, - spaces(indent), "}\n" - "\n", - "\n"), + (liteMode ? kj::strTree() + : kj::strTree( + spaces(indent), " public interface Pipeline", readerTypeParams, " extends org.capnproto.Pipeline {\n", + KJ_MAP(f, fieldTexts) { + return kj::mv(f.pipelineMethodDecls); + }, + spaces(indent), " }\n") + ), + spaces(indent), "}\n"), kj::strTree(), kj::strTree() }; } + // ----------------------------------------------------------------- + + struct InterfaceText { + kj::StringTree outerTypeDef; + kj::StringTree clientServerDefs; + }; + + struct ExtendInfo { + kj::String typeName; + uint64_t id; + }; + + + void getTransitiveSuperclasses(InterfaceSchema schema, std::map& map) { + if (map.insert(std::make_pair(schema.getProto().getId(), schema)).second) { + for (auto sup: schema.getSuperclasses()) { + getTransitiveSuperclasses(sup, map); + } + } + } + + InterfaceText makeInterfaceText(kj::StringPtr scope, kj::StringPtr name, InterfaceSchema schema, + kj::Array nestedTypeDecls, int indent) { + + if (liteMode) { + return {}; + } + + auto sp = spaces(indent); + auto fullName = kj::str(scope, name); + auto methods = KJ_MAP(m, schema.getMethods()) { + return makeMethodText(fullName, m, indent+2); + }; + + auto proto = schema.getProto(); + auto hexId = kj::hex(proto.getId()); + + auto superclasses = KJ_MAP(superclass, schema.getSuperclasses()) { + return ExtendInfo { + kj::str(javaFullName(superclass, nullptr)), + superclass.getProto().getId() + }; + }; + + kj::Array transitiveSuperclasses; + { + std::map map; + getTransitiveSuperclasses(schema, map); + map.erase(schema.getProto().getId()); + transitiveSuperclasses = KJ_MAP(entry, map) { + return ExtendInfo { + kj::str(javaFullName(entry.second, nullptr)), + entry.second.getProto().getId() + }; + }; + } + + auto typeNameVec = javaFullName(schema); + auto typeParamVec = getTypeParameters(schema); + bool hasTypeParams = typeParamVec.size() > 0; + + auto params = [&](auto& prefix, auto& sep, auto& suffix, auto item) { + return kj::strTree(prefix, kj::StringTree(KJ_MAP(p, typeParamVec) { return item(p); }, sep), suffix).flatten(); + }; + + auto genericParamTypes = proto.getIsGeneric() + ? params("<", ", ", ">", [](auto& p) { return kj::strTree(p); }) + : kj::str(); + + auto readerTypeParamsInferred = hasTypeParams ? "<>" : ""; + + auto factoryRef = hasTypeParams + ? params("newFactory(", ", ", ")", [](auto& p) { return kj::strTree(p, "_Factory"); }) + : kj::str("factory"); + + auto factoryTypeParams = hasTypeParams + ? params("<", ", ", ">", [](auto& p) { return kj::strTree(p, "_Builder, ", p, "_Reader"); }) + : kj::str(); + + auto factoryArgs = kj::StringTree(KJ_MAP(p, typeParamVec) { + return kj::strTree("org.capnproto.PointerFactory<", p, "_Builder, ", p, "_Reader> ", p, "_Factory"); + }, ", ").flatten(); + + auto factoryMembers = kj::strTree(KJ_MAP(p, typeParamVec) { + return kj::strTree(spaces(indent), " final org.capnproto.PointerFactory<", p, "_Builder, ", p, "_Reader> ", p, "_Factory;\n"); + }).flatten(); + + return InterfaceText { + kj::strTree( + sp, "public static class ", name, genericParamTypes, " {\n", + sp, " public static final class Factory", factoryTypeParams, "\n", + sp, " extends org.capnproto.Capability.Factory {\n", + factoryMembers, + sp, " public Factory(", + factoryArgs, + ") {\n", + KJ_MAP(p, typeParamVec) { + return kj::strTree(sp, " this.", p, "_Factory = ", p, "_Factory;\n"); + }, + sp, " }\n", + sp, " public final Client newClient(org.capnproto.ClientHook hook) {\n", + sp, " return new Client(hook);\n", + sp, " }\n", + sp, " }\n", + "\n", + (hasTypeParams + ? kj::strTree( + sp, " public static ", factoryTypeParams, "Factory", factoryTypeParams, " newFactory(", factoryArgs, ") {\n", + sp, " return new Factory<>(", + params("", ", ", "", [](auto& p) { return kj::strTree(p, "_Factory"); }), ");\n", + sp, " }\n") + : kj::strTree(sp, " public static final Factory factory = new Factory();\n") + ), + "\n", + sp, " public static class Client\n", + (superclasses.size() == 0 + ? kj::str(sp, " extends org.capnproto.Capability.Client ") + : kj::str( + KJ_MAP(s, superclasses) { + return kj::strTree(sp, " extends ", s.typeName, ".Client "); + }) + ), + "{\n", + sp, " public Client() {}\n", + sp, " public Client(org.capnproto.ClientHook hook) { super(hook); }\n", + sp, " public Client(org.capnproto.Capability.Client cap) { super(cap); }\n", + sp, " public Client(Server server) { super(server); }\n", + sp, " public Client(java.util.concurrent.CompletionStage promise) {\n", + sp, " super(promise);\n", + sp, " }\n", + sp, " public static final class Methods {\n", + KJ_MAP(m, methods) { return kj::mv(m.clientMethodDefs); }, + sp, " }\n", + KJ_MAP(m, methods) { return kj::mv(m.clientCalls); }, + sp, " }\n", + sp, " public static abstract class Server\n", + (superclasses.size() == 0 + ? kj::str(sp, " extends org.capnproto.Capability.Server ") + : kj::str( + KJ_MAP(s, superclasses) { + return kj::strTree(sp, " extends ", s.typeName, ".Server "); + }) + ), + "{\n", + sp, " protected org.capnproto.DispatchCallResult dispatchCall(\n", + sp, " long interfaceId, short methodId,\n", + sp, " org.capnproto.CallContext context) {\n", + sp, " if (interfaceId == 0x", hexId, "L) {\n", + sp, " return this.dispatchCallInternal(methodId, context);\n", + sp, " }\n", + KJ_MAP(s, transitiveSuperclasses) { + return kj::strTree( + sp, " else if (interfaceId == 0x", kj::hex(s.id), "L) {\n", + sp, " return super.dispatchCall(interfaceId, methodId, context);\n", + sp, " }\n"); + }, + sp, " else {\n", + sp, " return org.capnproto.Capability.Server.result(\n", + sp, " org.capnproto.Capability.Server.internalUnimplemented(\"", name, "\", interfaceId));\n", + sp, " }\n", + sp, " }\n", + sp, " private org.capnproto.DispatchCallResult dispatchCallInternal(short methodId, org.capnproto.CallContext context) {\n", + sp, " switch (methodId) {\n", + KJ_MAP(m, methods) { return kj::mv(m.dispatchCase); }, + sp, " default:\n", + sp, " return org.capnproto.Capability.Server.result(\n", + sp, " org.capnproto.Capability.Server.internalUnimplemented(\"", name, "\", 0x", hexId, "L, methodId));\n", + sp, " }\n", + sp, " }\n", + KJ_MAP(m, methods) { return kj::mv(m.serverDefs); }, + sp, " protected Client thisCap() { return new Client(super.thisCap()); }\n", + sp, " }\n", + KJ_MAP(n, nestedTypeDecls) { return kj::mv(n); }, + sp, "}\n", + "\n") + }; + } + + // ----------------------------------------------------------------- + + struct MethodText { + kj::StringTree clientMethodDefs; + kj::StringTree clientCalls; + kj::StringTree serverDefs; + kj::StringTree dispatchCase; + }; + + MethodText makeMethodText(kj::StringPtr interfaceName, InterfaceSchema::Method method, int indent = 0) { + + auto sp = spaces(indent); + auto proto = method.getProto(); + auto methodName = proto.getName(); + auto titleCase = toTitleCase(methodName); + auto paramSchema = method.getParamType(); + auto resultSchema = method.getResultType(); + auto identifierName = safeIdentifier(methodName); + + auto paramProto = paramSchema.getProto(); + auto resultProto = resultSchema.getProto(); + + bool isStreaming = method.isStreaming(); + + auto implicitParamsReader = proto.getImplicitParameters(); + + auto interfaceTypeName = javaFullName(method.getContainingInterface()); + kj::String paramType; + kj::String genericParamType; + + if (paramProto.getScopeId() == 0) { + paramType = kj::str(interfaceTypeName); + if (implicitParamsReader.size() == 0) { + paramType = kj::str(titleCase, "Params"); + genericParamType = kj::str(paramType); + } else { + genericParamType = kj::str(paramType); + //genericParamType.addMemberTemplate(kj::str(titleCase, "Params"), nullptr); + //paramType.addMemberTemplate(kj::str(titleCase, "Params"), + // kj::heapArray(implicitParams.asPtr())); + } + } else { + paramType = kj::str(javaFullName(paramSchema, method)); + genericParamType = kj::str(javaFullName(paramSchema, nullptr)); + } + + kj::String resultType; + kj::String genericResultType; + if (isStreaming) { + // We don't use resultType or genericResultType in this case. We want to avoid computing them + // at all so that we don't end up marking stream.capnp.h in usedImports. + } else if (resultProto.getScopeId() == 0) { + resultType = kj::str(interfaceTypeName); + if (implicitParamsReader.size() == 0) { + resultType = kj::str(titleCase, "Results"); + genericResultType = kj::str(resultType); + } else { + genericResultType = kj::str(resultType); + //genericResultType.addMemberTemplate(kj::str(titleCase, "Results"), nullptr); + //resultType.addMemberTemplate(kj::str(titleCase, "Results"), + // kj::heapArray(implicitParams.asPtr())); + } + } else { + resultType = kj::str(javaFullName(resultSchema, method)); + genericResultType = kj::str(javaFullName(resultSchema, nullptr)); + } + + kj::String shortParamType = paramProto.getScopeId() == 0 ? + kj::str(titleCase, "Params") : kj::str(genericParamType); + kj::String shortResultType = resultProto.getScopeId() == 0 || isStreaming ? + kj::str(titleCase, "Results") : kj::str(genericResultType); + + if (paramProto.getScopeId() == 0) { + paramType = kj::str(javaFullName(paramSchema, method)); + } + + if (resultProto.getScopeId() == 0) { + paramType = kj::str(javaFullName(resultSchema, method)); + } + + kj::String paramFactory = kj::str(shortParamType, ".factory"); + kj::String resultFactory = kj::str(shortResultType, ".factory"); + + if (paramProto.getIsGeneric()) { + auto paramFactoryArgs = getFactoryArguments(paramSchema, paramSchema); + paramFactory = paramFactoryArgs.size() == 0 + ? kj::str(shortParamType, ".factory") + : kj::strTree("newFactory(", + kj::StringTree(KJ_MAP(arg, paramFactoryArgs) { + return kj::strTree(arg); + }, ", "), + ")").flatten(); + } + + if (resultProto.getIsGeneric()) { + auto resultFactoryArgs = getFactoryArguments(resultSchema, paramSchema); + resultFactory = resultFactoryArgs.size() == 0 + ? kj::str(shortResultType, ".factory") + : kj::strTree("newFactory(", + kj::StringTree(KJ_MAP(arg, resultFactoryArgs) { + return kj::strTree(arg); + }, ", "), + ")").flatten(); + } + + auto paramBuilder = kj::str(shortParamType, ".Builder"); + auto resultReader = kj::str(shortResultType, ".Reader"); + + auto interfaceProto = method.getContainingInterface().getProto(); + uint64_t interfaceId = interfaceProto.getId(); + auto interfaceIdHex = kj::hex(interfaceId); + uint16_t methodId = method.getIndex(); + + if (isStreaming) { + return MethodText { + // client method defs + kj::strTree(), + + // client call + kj::strTree( + sp, "public org.capnproto.StreamingRequest<", shortParamType, ".Builder> ", methodName, "Request() {\n", + sp, " return newStreamingCall(", paramFactory, ", 0x", interfaceIdHex, "L, (short)", methodId, ");\n", + sp, "}\n" + ), + + // server defs + kj::strTree( + sp, "protected java.util.concurrent.CompletableFuture ", identifierName, "(org.capnproto.StreamingCallContext<", shortParamType, ".Reader> context) {\n", + sp, " return org.capnproto.Capability.Server.internalUnimplemented(\n", + sp, " \"", interfaceProto.getDisplayName(), "\", \"", methodName, "\",\n", + sp, " 0x", interfaceIdHex, "L, (short)", methodId, ");\n", + sp, "}\n" + ), + + // dispatch + kj::strTree( + sp, "case ", methodId, ":\n", + sp, " return org.capnproto.Capability.Server.streamResult(\n", + sp, " this.", identifierName, "(org.capnproto.Capability.Server.internalGetTypedStreamingContext(\n", + sp, " ", paramFactory, ", context)));\n" + ) + }; + } else { + return MethodText { + // client method defs + kj::strTree( + sp, " public static final class ", methodName, " {\n", + sp, " public interface Request extends org.capnproto.Request<", paramBuilder, "> {\n", + sp, " default Response send() {\n", + sp, " return new Response(this.sendInternal());\n", + sp, " }\n", + sp, " }\n", + sp, " public static final class Response\n", + sp, " extends org.capnproto.RemotePromise<", resultReader, ">\n", + sp, " implements ", shortResultType, ".Pipeline {\n", + sp, " public Response(org.capnproto.RemotePromise response) {\n", + sp, " super(", resultFactory, ", response);\n", + sp, " }\n", + sp, " public org.capnproto.AnyPointer.Pipeline typelessPipeline() {\n", + sp, " return this.pipeline();\n", + sp, " }\n", + sp, " }\n", + sp, " }\n" + ), + + // client call + kj::strTree( + sp, "public Methods.", methodName, ".Request ", methodName, "Request() {\n", + sp, " var result = newCall(", paramFactory, ", 0x", interfaceIdHex, "L, (short)", methodId, ");\n", + sp, " return () -> result;\n", + sp, "}\n" + ), + + // server defs + kj::strTree( + sp, "protected java.util.concurrent.CompletableFuture ", identifierName, "(org.capnproto.CallContext<", shortParamType, ".Reader, ", shortResultType, ".Builder> context) {\n", + sp, " return org.capnproto.Capability.Server.internalUnimplemented(\n", + sp, " \"", interfaceProto.getDisplayName(), "\", \"", methodName, "\", 0x", interfaceIdHex, "L, (short)", methodId, ");\n", + sp, "}\n"), + + // dispatch + kj::strTree( + sp, "case ", methodId, ":\n", + sp, " return org.capnproto.Capability.Server.result(\n", + sp, " this.", identifierName, "(org.capnproto.Capability.Server.internalGetTypedContext(\n", + sp, " ", paramFactory, ", ", resultFactory, ", context)));\n") + }; + } + } // ----------------------------------------------------------------- @@ -1664,7 +2205,24 @@ private: } } } else if (proto.isInterface()) { - KJ_FAIL_REQUIRE("interfaces not implemented"); + for (auto method: proto.getInterface().getMethods()) { + { + Schema params = schemaLoader.getUnbound(method.getParamStructType()); + auto paramsProto = schemaLoader.getUnbound(method.getParamStructType()).getProto(); + if (paramsProto.getScopeId() == 0) { + nestedTexts.add(makeNodeText(subScope, + toTitleCase(kj::str(method.getName(), "Params")), params, indent + 1)); + } + } + { + Schema results = schemaLoader.getUnbound(method.getResultStructType()); + auto resultsProto = schemaLoader.getUnbound(method.getResultStructType()).getProto(); + if (resultsProto.getScopeId() == 0) { + nestedTexts.add(makeNodeText(subScope, + toTitleCase(kj::str(method.getName(), "Results")), results, indent + 1)); + } + } + } } // Convert the encoded schema to a literal byte array. @@ -1828,7 +2386,16 @@ private: case schema::Node::INTERFACE: { hasInterfaces = true; - KJ_FAIL_REQUIRE("unimplemented"); + auto interfaceText = + makeInterfaceText(scope, name, schema.asInterface(), kj::mv(nestedTypeDecls), indent); + + return NodeTextNoSchema { + kj::mv(interfaceText.outerTypeDef), + kj::mv(interfaceText.clientServerDefs), + kj::strTree(), + kj::strTree(), + kj::strTree() + }; } case schema::Node::CONST: { @@ -1935,6 +2502,11 @@ private: } kj::MainBuilder::Validity run() { + + if (::getenv("CAPNP_LITE") != nullptr) { + liteMode = true; + } + ReaderOptions options; options.traversalLimitInWords = 1 << 30; // Don't limit. StreamFdMessageReader reader(0, options); diff --git a/compiler/src/test/java/org/capnproto/test/EncodingTest.java b/compiler/src/test/java/org/capnproto/test/EncodingTest.java index 3b653a8e..1d3ce095 100644 --- a/compiler/src/test/java/org/capnproto/test/EncodingTest.java +++ b/compiler/src/test/java/org/capnproto/test/EncodingTest.java @@ -903,17 +903,24 @@ public void testSetWithCaveats() { TestUtil.checkTestMessage(listReader.get(1)); } - @Test - public void testCopyAnyPointer() { - MessageBuilder message1 = new MessageBuilder(); - org.capnproto.test.Test.TestAllTypes.Builder root1 = message1.initRoot(org.capnproto.test.Test.TestAllTypes.factory); - TestUtil.initTestMessage(root1); + @Test + public void testAnyStruct() { + MessageBuilder builder = new MessageBuilder(); + var root = builder.initRoot(org.capnproto.test.Test.TestAnyOthers.factory); + var anyStruct = root.initAnyStructField(); + } - MessageBuilder message2 = new MessageBuilder(); - AnyPointer.Builder root2 = message2.initRoot(AnyPointer.factory); - root2.setAs(AnyPointer.factory, message1.getRoot(AnyPointer.factory).asReader()); + @Test + public void testCopyAnyPointer() { + MessageBuilder message1 = new MessageBuilder(); + org.capnproto.test.Test.TestAllTypes.Builder root1 = message1.initRoot(org.capnproto.test.Test.TestAllTypes.factory); + TestUtil.initTestMessage(root1); + + MessageBuilder message2 = new MessageBuilder(); + AnyPointer.Builder root2 = message2.initRoot(AnyPointer.factory); + root2.setAs(AnyPointer.factory, message1.getRoot(AnyPointer.factory).asReader()); - TestUtil.checkTestMessage(root2.getAs(org.capnproto.test.Test.TestAllTypes.factory)); + TestUtil.checkTestMessage(root2.getAs(org.capnproto.test.Test.TestAllTypes.factory)); } @Test diff --git a/compiler/src/test/schema/test.capnp b/compiler/src/test/schema/test.capnp index bbc84133..55226537 100644 --- a/compiler/src/test/schema/test.capnp +++ b/compiler/src/test/schema/test.capnp @@ -148,6 +148,12 @@ struct TestAnyPointer { # in the struct. } +struct TestAnyOthers { + anyStructField @0 :AnyStruct; + #anyListField @1 :AnyPointer; # not currently implemented + #capabilityField @2 :Capability; +} + struct TestOutOfOrder { foo @3 :Text; bar @2 :Text; diff --git a/examples/pom.xml b/examples/pom.xml index 0746576b..1b5df474 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -31,9 +31,9 @@ UTF-8 - 1.8 - 1.8 - 8 + 10 + 10 + 10 @@ -41,10 +41,32 @@ runtime 0.1.17-SNAPSHOT + + + org.capnproto + runtime-rpc + 0.1.17-SNAPSHOT + + + + junit + junit + 4.13.1 + compile + + + org.apache.maven.plugins + maven-compiler-plugin + 3.3 + + -Xlint:unchecked + + + maven-antrun-plugin 3.0.0 @@ -54,15 +76,24 @@ generate-sources - + - + + + + + + + + + + diff --git a/examples/src/main/java/org/capnproto/examples/CalculatorClient.java b/examples/src/main/java/org/capnproto/examples/CalculatorClient.java new file mode 100644 index 00000000..e79b8f89 --- /dev/null +++ b/examples/src/main/java/org/capnproto/examples/CalculatorClient.java @@ -0,0 +1,98 @@ +package org.capnproto.examples; + +import org.capnproto.*; +import org.junit.Assert; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.channels.AsynchronousSocketChannel; +import java.util.concurrent.ExecutionException; + +public class CalculatorClient { + + public static void usage() { + System.out.println("usage: host:port"); + } + + public static void main(String[] args) { + if (args.length < 1) { + usage(); + return; + } + + var endpoint = args[0].split(":"); + var address = new InetSocketAddress(endpoint[0], Integer.parseInt(endpoint[1])); + try { + var clientSocket = AsynchronousSocketChannel.open(); + clientSocket.connect(address).get(); + var rpcClient = new TwoPartyClient(clientSocket); + var calculator = new org.capnproto.examples.Calc.Calculator.Client(rpcClient.bootstrap()); + + { + System.out.println("Evaluating a literal..."); + var request = calculator.evaluateRequest(); + request.getParams().getExpression().setLiteral(123); + var evalPromise = request.send(); + var readPromise = evalPromise.getValue().readRequest().send(); + + var response = rpcClient.runUntil(readPromise); + Assert.assertTrue(response.get().getValue() == 123); + } + + { + // Make a request to evaluate 123 + 45 - 67. + // + // The Calculator interface requires that we first call getOperator() to + // get the addition and subtraction functions, then call evaluate() to use + // them. But, once again, we can get both functions, call evaluate(), and + // then read() the result -- four RPCs -- in the time of *one* network + // round trip, because of promise pipelining. + + System.out.println("Using add and subtract... "); + + Calc.Calculator.Function.Client add; + Calc.Calculator.Function.Client subtract; + + { + // Get the "add" function from the server. + var request = calculator.getOperatorRequest(); + request.getParams().setOp(Calc.Calculator.Operator.ADD); + add = request.send().getFunc(); + } + + { + // Get the "subtract" function from the server. + var request = calculator.getOperatorRequest(); + request.getParams().setOp(Calc.Calculator.Operator.SUBTRACT); + subtract = request.send().getFunc(); + } + + // Build the request to evaluate 123 + 45 - 67. + var request = calculator.evaluateRequest(); + + var subtractCall = request.getParams().getExpression().initCall(); + subtractCall.setFunction(subtract); + var subtractParams = subtractCall.initParams(2); + subtractParams.get(1).setLiteral(67); + + var addCall = subtractParams.get(0).initCall(); + addCall.setFunction(add); + var addParams = addCall.initParams(2); + addParams.get(0).setLiteral(123); + addParams.get(1).setLiteral(45); + + var evalPromise = request.send(); + var readPromise = evalPromise.getValue().readRequest().send(); + + // run the RPC system until the read request completes. + var response = rpcClient.runUntil(readPromise).join(); + Assert.assertEquals(101, response.getValue(), 0.0001); + + System.out.println("PASS"); + } + } + catch (IOException | InterruptedException | ExecutionException e) { + e.printStackTrace(); + } + } +} diff --git a/examples/src/main/java/org/capnproto/examples/CalculatorServer.java b/examples/src/main/java/org/capnproto/examples/CalculatorServer.java new file mode 100644 index 00000000..b40bbf39 --- /dev/null +++ b/examples/src/main/java/org/capnproto/examples/CalculatorServer.java @@ -0,0 +1,188 @@ +package org.capnproto.examples; + +import org.capnproto.*; + +import java.io.IOException; +import java.lang.Void; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.List; +import java.util.ArrayList; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.stream.Collectors; + + +public class CalculatorServer { + + // https://www.nurkiewicz.com/2013/05/java-8-completablefuture-in-action.html + private static CompletableFuture> sequence(List> futures) { + var done = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + return done.thenApply(void_ -> + futures.stream(). + map(CompletableFuture::join). + collect(Collectors.toList()) + ); + } + + static CompletableFuture readValue(org.capnproto.examples.Calc.Calculator.Value.Client value) { + return value.readRequest().send().thenApply(result -> result.getValue()); + } + + static CompletableFuture evaluateImpl(org.capnproto.examples.Calc.Calculator.Expression.Reader expression, + PrimitiveList.Double.Reader params) { + switch (expression.which()) { + case LITERAL: + return CompletableFuture.completedFuture(expression.getLiteral()); + + case PREVIOUS_RESULT: + return readValue(expression.getPreviousResult()); + + case PARAMETER: + return CompletableFuture.completedFuture(params.get(expression.getParameter())); + + case CALL: { + var call = expression.getCall(); + var func = call.getFunction(); + + // Evaluate each parameter. + var paramPromises = new ArrayList>(); + for (var param: call.getParams()) { + paramPromises.add(evaluateImpl(param, params)); + } + + // When the parameters are complete, call the function. + var joinedParams = sequence(paramPromises); + + // When the parameters are complete, call the function. + return joinedParams.thenCompose(paramValues -> { + var request = func.callRequest(); + var funcParams = request.getParams().initParams(paramValues.size()); + for (int ii = 0; ii < paramValues.size(); ++ii) { + funcParams.set(ii, paramValues.get(ii)); + } + return request.send().thenApply(result -> result.getValue()); + }); + } + + default: + return CompletableFuture.failedFuture(RpcException.failed("Unknown expression type.")); + } + } + + static class ValueImpl extends Calc.Calculator.Value.Server { + private final double value; + + public ValueImpl(double value) { + this.value = value; + } + + @Override + protected CompletableFuture read(CallContext context) { + context.getResults().setValue(this.value); + return READY_NOW; + } + } + + static class FunctionImpl extends Calc.Calculator.Function.Server { + + private final int paramCount; + private final Calc.Calculator.Expression.Reader expression; + + public FunctionImpl(int paramCount, Calc.Calculator.Expression.Reader body) { + this.paramCount = paramCount; + this.expression = body; + } + + @Override + protected CompletableFuture call(CallContext context) { + var params = context.getParams().getParams(); + if (params.size() != this.paramCount) { + return CompletableFuture.failedFuture(RpcException.failed("Wrong number of parameters")); + } + + return evaluateImpl(expression, params) + .thenAccept(value -> context.getResults().setValue(value)); + } + } + + static class OperatorImpl extends Calc.Calculator.Function.Server { + private final Calc.Calculator.Operator op; + + public OperatorImpl(Calc.Calculator.Operator op) { + this.op = op; + } + + @Override + protected CompletableFuture call(CallContext context) { + var params = context.getParams().getParams(); + if (params.size() != 2) { + return CompletableFuture.failedFuture(RpcException.failed("Wrong number of parameters")); + } + + var x = params.get(0); + var y = params.get(1); + + double result; + switch (op) { + case ADD: + result = x + y; + break; + case SUBTRACT: + result = x - y; + break; + case MULTIPLY: + result = x * y; + break; + case DIVIDE: + result = x / y; + break; + default: + result = Double.NaN; + }; + + context.getResults().setValue(result); + return READY_NOW; + } + } + + static class CalculatorImpl extends Calc.Calculator.Server { + @Override + protected CompletableFuture evaluate(CallContext context) { + return evaluateImpl(context.getParams().getExpression(), null).thenAccept(value -> { + context.getResults().setValue(new ValueImpl(value)); + }); + } + + @Override + protected CompletableFuture defFunction(CallContext context) { + var params = context.getParams(); + context.getResults().setFunc(new FunctionImpl(params.getParamCount(), params.getBody())); + return READY_NOW; + } + + @Override + protected CompletableFuture getOperator(CallContext context) { + context.getResults().setFunc(new OperatorImpl(context.getParams().getOp())); + return READY_NOW; + } + } + + public static void main(String[] args) { + if (args.length < 1) { + return; + } + + var hostPort = args[0].split(":"); + var address = new InetSocketAddress(hostPort[0], Integer.parseInt(hostPort[1])); + try { + var server = new EzRpcServer(new CalculatorImpl(), address); + var port = server.getPort(); + System.out.println("Listening on port " + port + "..."); + server.start().join(); + } + catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/examples/src/main/schema/calculator.capnp b/examples/src/main/schema/calculator.capnp new file mode 100644 index 00000000..cfd9ac48 --- /dev/null +++ b/examples/src/main/schema/calculator.capnp @@ -0,0 +1,122 @@ +# Copyright (c) 2013-2014 Sandstorm Development Group, Inc. and contributors +# Licensed under the MIT License: +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +@0x85150b117366d14b; + +using Java = import "/capnp/java.capnp"; +$Java.package("org.capnproto.examples"); +$Java.outerClassname("Calc"); + +interface Calculator { + # A "simple" mathematical calculator, callable via RPC. + # + # But, to show off Cap'n Proto, we add some twists: + # + # - You can use the result from one call as the input to the next + # without a network round trip. To accomplish this, evaluate() + # returns a `Value` object wrapping the actual numeric value. + # This object may be used in a subsequent expression. With + # promise pipelining, the Value can actually be used before + # the evaluate() call that creates it returns! + # + # - You can define new functions, and then call them. This again + # shows off pipelining, but it also gives the client the + # opportunity to define a function on the client side and have + # the server call back to it. + # + # - The basic arithmetic operators are exposed as Functions, and + # you have to call getOperator() to obtain them from the server. + # This again demonstrates pipelining -- using getOperator() to + # get each operator and then using them in evaluate() still + # only takes one network round trip. + + evaluate @0 (expression :Expression) -> (value :Value); + # Evaluate the given expression and return the result. The + # result is returned wrapped in a Value interface so that you + # may pass it back to the server in a pipelined request. To + # actually get the numeric value, you must call read() on the + # Value -- but again, this can be pipelined so that it incurs + # no additional latency. + + struct Expression { + # A numeric expression. + + union { + literal @0 :Float64; + # A literal numeric value. + + previousResult @1 :Value; + # A value that was (or, will be) returned by a previous + # evaluate(). + + parameter @2 :UInt32; + # A parameter to the function (only valid in function bodies; + # see defFunction). + + call :group { + # Call a function on a list of parameters. + function @3 :Function; + params @4 :List(Expression); + } + } + } + + interface Value { + # Wraps a numeric value in an RPC object. This allows the value + # to be used in subsequent evaluate() requests without the client + # waiting for the evaluate() that returns the Value to finish. + + read @0 () -> (value :Float64); + # Read back the raw numeric value. + } + + defFunction @1 (paramCount :Int32, body :Expression) + -> (func :Function); + # Define a function that takes `paramCount` parameters and returns the + # evaluation of `body` after substituting these parameters. + + interface Function { + # An algebraic function. Can be called directly, or can be used inside + # an Expression. + # + # A client can create a Function that runs on the server side using + # `defFunction()` or `getOperator()`. Alternatively, a client can + # implement a Function on the client side and the server will call back + # to it. However, a function defined on the client side will require a + # network round trip whenever the server needs to call it, whereas + # functions defined on the server and then passed back to it are called + # locally. + + call @0 (params :List(Float64)) -> (value :Float64); + # Call the function on the given parameters. + } + + getOperator @2 (op :Operator) -> (func :Function); + # Get a Function representing an arithmetic operator, which can then be + # used in Expressions. + + enum Operator { + add @0; + subtract @1; + multiply @2; + divide @3; + } +} diff --git a/foo.raw b/foo.raw new file mode 100644 index 00000000..fe793207 Binary files /dev/null and b/foo.raw differ diff --git a/gen b/gen new file mode 100755 index 00000000..dad800b5 --- /dev/null +++ b/gen @@ -0,0 +1,27 @@ +#!/bin/bash + +set -eux + +export PATH=/usr/local/bin:/usr/bin:/bin + +make CXX=g++-8 capnpc-java + +capnp compile -I./compiler/src/main/schema/ -o/bin/cat ./runtime/src/test/schema/test.capnp > ./runtime/src/test/schema/test.raw +capnp compile -I./compiler/src/main/schema/ -oc++ ./runtime/src/test/schema/test.capnp +env CAPNP_LITE=1 capnp compile -I./compiler/src/main/schema/ -o./capnpc-java ./runtime/src/test/schema/test.capnp +cp ./runtime/src/test/schema/Test.java ./runtime/src/test/schema/TestLite.java +capnp compile -I./compiler/src/main/schema/ -o./capnpc-java ./runtime/src/test/schema/test.capnp +#cp ./runtime/src/test/schema/Test.java ./runtime/src/test/java/org/capnproto/test/ + +#capnp compile -I./compiler/src/main/schema/ -oc++ ./runtime/src/test/schema/demo.capnp +#capnp compile -I./compiler/src/main/schema/ -o./capnpc-java ./runtime/src/test/schema/demo.capnp +#cp ./runtime/src/test/schema/Demo.java ./runtime/src/test/java/org/capnproto/demo/ + +#capnp compile -I./compiler/src/main/schema/ -o/bin/cat ./runtime/src/test/schema/generics.capnp > ./runtime/src/test/schema/generics.raw +#capnp compile -I./compiler/src/main/schema/ -oc++ ./runtime/src/test/schema/generics.capnp +#capnp compile -I./compiler/src/main/schema/ -o./capnpc-java ./runtime/src/test/schema/generics.capnp +#cp ./runtime/src/test/schema/TestGenerics.java ./runtime/src/test/java/org/capnproto/demo/ + +#capnp compile -I./compiler/src/main/schema/ -o./capnpc-java ./examples/src/main/schema/addressbook.capnp +#cp ./examples/src/main/schema/Addressbook.java ./examples/src/main/java/org/capnproto/examples/ + diff --git a/jitpack.yml b/jitpack.yml new file mode 100644 index 00000000..adb3fe10 --- /dev/null +++ b/jitpack.yml @@ -0,0 +1,2 @@ +jdk: + - openjdk11 diff --git a/pom.xml b/pom.xml index e0ee84ee..654bd567 100644 --- a/pom.xml +++ b/pom.xml @@ -12,6 +12,7 @@ compiler examples benchmark + runtime-rpc diff --git a/runtime-rpc/pom.xml b/runtime-rpc/pom.xml new file mode 100644 index 00000000..057fbe11 --- /dev/null +++ b/runtime-rpc/pom.xml @@ -0,0 +1,230 @@ + + + 4.0.0 + org.capnproto + runtime-rpc + jar + runtime-rpc + 0.1.17-SNAPSHOT + Cap'n Proto RPC runtime library + + org.capnproto + + https://capnproto.org/ + + + MIT + http://opensource.org/licenses/MIT + repo + + + + git@github.com:capnproto/capnproto-java.git + scm:git@github.com:capnproto/capnproto-java.git + + + + dwrensha + David Renshaw + https://github.com/dwrensha + + + vaci + Vaci Koblizek + https://github.com/vaci + + + + UTF-8 + 11 + 11 + + + + junit + junit + 4.13.1 + test + + + org.capnproto + runtime + 0.1.17-SNAPSHOT + + + org.capnproto + compiler + 0.1.17-SNAPSHOT + + + + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.3 + + -Xlint:unchecked + 11 + 11 + + + + + maven-antrun-plugin + 3.0.0 + + + generate-rpc-sources + generate-sources + + + + + + + + + + + + + + + + + run + + + + generate-rpc-test-sources + generate-test-sources + + + + + + + + + + + + + + + run + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.1.0 + + + add-generated-sources + generate-sources + + add-source + + + + src/main/generated + + + + + add-generated-test-sources + generate-test-sources + + add-test-source + + + + src/test/generated + + + + + + + + + + + release + + + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.10.3 + + + attach-javadocs + + jar + + + false + + + + + + maven-gpg-plugin + 1.6 + + + sign-artifacts + verify + + sign + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.6 + true + + ossrh + https://oss.sonatype.org/ + true + + + + + + + + diff --git a/runtime-rpc/src/main/java/org/capnproto/EzRpcClient.java b/runtime-rpc/src/main/java/org/capnproto/EzRpcClient.java new file mode 100644 index 00000000..64ba8306 --- /dev/null +++ b/runtime-rpc/src/main/java/org/capnproto/EzRpcClient.java @@ -0,0 +1,23 @@ +package org.capnproto; + +import java.nio.channels.AsynchronousByteChannel; +import java.util.concurrent.CompletableFuture; + +public class EzRpcClient { + + private final TwoPartyClient twoPartyRpc; + private final Capability.Client client; + + public EzRpcClient(AsynchronousByteChannel socket) { + this.twoPartyRpc = new TwoPartyClient(socket); + this.client = this.twoPartyRpc.bootstrap(); + } + + public Capability.Client getMain() { + return this.client; + } + + public CompletableFuture runUntil(CompletableFuture done) { + return this.twoPartyRpc.runUntil(done); + } +} diff --git a/runtime-rpc/src/main/java/org/capnproto/EzRpcServer.java b/runtime-rpc/src/main/java/org/capnproto/EzRpcServer.java new file mode 100644 index 00000000..f8116c12 --- /dev/null +++ b/runtime-rpc/src/main/java/org/capnproto/EzRpcServer.java @@ -0,0 +1,36 @@ +package org.capnproto; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.channels.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executors; + +public class EzRpcServer { + + private final AsynchronousChannelGroup channelgroup; + private final AsynchronousServerSocketChannel serverAcceptSocket; + private final TwoPartyServer twoPartyRpc; + private final int port; + + public EzRpcServer(Capability.Server bootstrapInterface, InetSocketAddress address) throws IOException { + this(new Capability.Client(bootstrapInterface), address); + } + + public EzRpcServer(Capability.Client bootstrapInterface, InetSocketAddress address) throws IOException { + this.channelgroup = AsynchronousChannelGroup.withThreadPool(Executors.newFixedThreadPool(1)); + this.serverAcceptSocket = AsynchronousServerSocketChannel.open(this.channelgroup); + this.serverAcceptSocket.bind(address); + var localAddress = (InetSocketAddress) this.serverAcceptSocket.getLocalAddress(); + this.port = localAddress.getPort(); + this.twoPartyRpc = new TwoPartyServer(bootstrapInterface); + } + + public int getPort() { + return this.port; + } + + public CompletableFuture start() { + return this.twoPartyRpc.listen(this.serverAcceptSocket); + } +} diff --git a/runtime-rpc/src/main/java/org/capnproto/RpcDumper.java b/runtime-rpc/src/main/java/org/capnproto/RpcDumper.java new file mode 100644 index 00000000..e7871459 --- /dev/null +++ b/runtime-rpc/src/main/java/org/capnproto/RpcDumper.java @@ -0,0 +1,167 @@ +package org.capnproto; + +import java.util.HashMap; +import java.util.Map; + +public class RpcDumper { + + //private final Map schemas = new HashMap<>(); + private final Map clientReturnTypes = new HashMap<>(); + private final Map serverReturnTypes = new HashMap<>(); + + /*void addSchema(long schemaId, Schema.Node.Reader node) { + this.schemas.put(schemaId, node); + }*/ + + private void setReturnType(RpcTwoPartyProtocol.Side side, int schemaId, long schema) { + switch (side) { + case CLIENT: + clientReturnTypes.put(schemaId, schema); + break; + case SERVER: + serverReturnTypes.put(schemaId, schema); + default: + break; + } + } + + private Long getReturnType(RpcTwoPartyProtocol.Side side, int schemaId) { + switch (side) { + case CLIENT: + return clientReturnTypes.get(schemaId); + case SERVER: + return serverReturnTypes.get(schemaId); + default: + break; + } + return -1L; + } + + private String dumpCap(RpcProtocol.CapDescriptor.Reader cap) { + return cap.which().toString(); + } + private String dumpCaps(StructList.Reader capTable) { + switch (capTable.size()) { + case 0: + return ""; + case 1: + return dumpCap(capTable.get(0)); + default: + { + var text = dumpCap(capTable.get(0)); + for (int ii = 1; ii< capTable.size(); ++ii) { + text += ", " + dumpCap(capTable.get(ii)); + } + return text; + } + } + } + + String dump(RpcProtocol.Message.Reader message, RpcTwoPartyProtocol.Side sender) { + switch (message.which()) { + case CALL: { + var call = message.getCall(); + var iface = call.getInterfaceId(); + + var interfaceName = String.format("0x%x", iface); + var methodName = String.format("method#%d", call.getMethodId()); + var payload = call.getParams(); + var params = payload.getContent(); + var sendResultsTo = call.getSendResultsTo(); +/* + var schema = this.schemas.get(iface); + if (schema != null) { + interfaceName = schema.getDisplayName().toString(); + if (schema.isInterface()) { + + interfaceName = schema.getDisplayName().toString(); + var interfaceSchema = schema.getInterface(); + + var methods = interfaceSchema.getMethods(); + if (call.getMethodId() < methods.size()) { + var method = methods.get(call.getMethodId()); + methodName = method.getName().toString(); + var paramType = method.getParamStructType(); + var resultType = method.getResultStructType(); + + if (call.getSendResultsTo().isCaller()) { + var questionId = call.getQuestionId(); + setReturnType(sender, call.getQuestionId(), resultType); + } + + } + } + }*/ + + return sender.name() + "(" + call.getQuestionId() + "): call " + + call.getTarget() + " <- " + interfaceName + "." + + methodName + " " + params.getClass().getName() + " caps:[" + + dumpCaps(payload.getCapTable()) + "]" + + (sendResultsTo.isCaller() ? "" : (" sendResultsTo:" + sendResultsTo)); + } + + case RETURN: { + var ret = message.getReturn(); + var text = sender.name() + "(" + ret.getAnswerId() + "): "; + var returnType = getReturnType( + sender == RpcTwoPartyProtocol.Side.CLIENT + ? RpcTwoPartyProtocol.Side.SERVER + : RpcTwoPartyProtocol.Side.CLIENT, + ret.getAnswerId()); + switch (ret.which()) { + case RESULTS: { + var payload = ret.getResults(); + return text + "return " + payload + + " caps:[" + dumpCaps(payload.getCapTable()) + "]"; + } + case EXCEPTION: { + var exc = ret.getException(); + return text + "exception " + + exc.getType().toString() + + " " + exc.getReason(); + } + default: { + return text + ret.which().name(); + } + } + } + + case BOOTSTRAP: { + var restore = message.getBootstrap(); + setReturnType(sender, restore.getQuestionId(), 0); + return sender.name() + "(" + restore.getQuestionId() + "): bootstrap " + + restore.getDeprecatedObjectId(); + } + + case ABORT: { + var abort = message.getAbort(); + return sender.name() + ": abort " + + abort.getType().toString() + + " \"" + abort.getReason().toString() + "\""; + } + + case RESOLVE: { + var resolve = message.getResolve(); + var id = resolve.getPromiseId(); + String text; + switch (resolve.which()) { + case CAP: { + var cap = resolve.getCap(); + text = cap.which().toString(); + break; + } + case EXCEPTION: { + var exc = resolve.getException(); + text = exc.getType().toString() + ": " + exc.getReason().toString(); + break; + } + default: text = resolve.which().toString(); break; + }; + return sender.name() + "(" + id + "): resolve " + text; + } + + default: + return sender.name() + ": " + message.which().name(); + } + } +} diff --git a/runtime-rpc/src/main/java/org/capnproto/RpcState.java b/runtime-rpc/src/main/java/org/capnproto/RpcState.java new file mode 100644 index 00000000..c671c3dd --- /dev/null +++ b/runtime-rpc/src/main/java/org/capnproto/RpcState.java @@ -0,0 +1,2139 @@ +package org.capnproto; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.nio.channels.ClosedChannelException; +import java.util.*; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.CompletionStage; +import java.util.logging.*; + +final class RpcState { + + private static final Logger LOGGER = Logger.getLogger(RpcState.class.getName()); + + private static int messageSizeHint() { + return 1 + RpcProtocol.Message.factory.structSize().total(); + } + + private static int messageSizeHint(StructFactory factory) { + return messageSizeHint() + factory.structSize().total(); + } + + private static int exceptionSizeHint(Throwable exc) { + return RpcProtocol.Exception.factory.structSize().total() + exc.getMessage().length(); + } + + private static final int MESSAGE_TARGET_SIZE_HINT + = RpcProtocol.MessageTarget.factory.structSize().total() + + RpcProtocol.PromisedAnswer.factory.structSize().total() + + 16; + + private static final int CAP_DESCRIPTOR_SIZE_HINT + = RpcProtocol.CapDescriptor.factory.structSize().total() + + RpcProtocol.PromisedAnswer.factory.structSize().total(); + + static class DisconnectInfo { + + final CompletableFuture shutdownPromise; + // Task which is working on sending an abort message and cleanly ending the connection. + + DisconnectInfo(CompletableFuture shutdownPromise) { + this.shutdownPromise = shutdownPromise; + } + } + + private final class Question { + + final int id; + boolean skipFinish; + boolean isAwaitingReturn; + int[] paramExports = new int[0]; + boolean isTailCall = false; + QuestionRef selfRef; + private final WeakReference disposer; + + Question(int id) { + this.id = id; + this.selfRef = new QuestionRef(this.id); + this.disposer = new QuestionDisposer(this.selfRef); + } + + void finish() { + if (isConnected() && !this.skipFinish) { + var sizeHint = messageSizeHint(RpcProtocol.Finish.factory); + var message = connection.newOutgoingMessage(sizeHint); + var builder = message.getBody().getAs(RpcProtocol.Message.factory).initFinish(); + builder.setQuestionId(this.id); + builder.setReleaseResultCaps(this.isAwaitingReturn); + LOGGER.fine(() -> RpcState.this.toString() + ": > FINISH question=" + this.id); + message.send(); + } + this.skipFinish = true; + + // Check if the question has returned and, if so, remove it from the table. + // Remove question ID from the table. Must do this *after* sending `Finish` to ensure that + // the ID is not re-allocated before the `Finish` message can be sent. + if (!this.isAwaitingReturn) { + questions.erase(this.id, this); + } + } + } + + + /** + * A reference to an entry on the question table. + */ + private final class QuestionRef { + + private final int questionId; + CompletableFuture response = new CompletableFuture<>(); + + QuestionRef(int questionId) { + this.questionId = questionId; + } + + void fulfill(Throwable exc) { + this.response.completeExceptionally(exc); + this.finish(); + } + + void fulfill(RpcResponse response) { + this.response.complete(response); + this.finish(); + } + + private void finish() { + // We no longer need access to the questionRef in order to complete it. + // Dropping the selfRef releases the question for disposal once all other + // references are gone. + var question = questions.find(this.questionId); + if (question != null) { + question.selfRef = null; + } + } + } + + private final class QuestionDisposer extends WeakReference { + private final int questionId; + + QuestionDisposer(QuestionRef questionRef) { + super(questionRef, questionRefs); + this.questionId = questionRef.questionId; + } + + void dispose() { + var question = questions.find(this.questionId); + if (question != null) { + question.finish(); + } + } + } + + final class Answer { + final int answerId; + boolean active = false; + PipelineHook pipeline; + CompletableFuture redirectedResults; + RpcCallContext callContext; + int[] resultExports; + + Answer(int answerId) { + this.answerId = answerId; + } + } + + static final class Export { + final int exportId; + int refcount; + ClientHook clientHook; + CompletionStage resolveOp; + + Export(int exportId) { + this.exportId = exportId; + } + } + + final class Import { + final int importId; + ImportDisposer disposer; + FileDescriptor fd; + int remoteRefCount; + RpcClient appClient; + CompletableFuture promise; + // If non-null, the import is a promise. + + Import(int importId) { + this.importId = importId; + } + + void addRemoteRef() { + this.remoteRefCount++; + } + + void setFdIfMissing(FileDescriptor fd) { + if (this.fd == null) { + this.fd = fd; + } + } + + public void dispose() { + // Remove self from the import table. + var imp = imports.find(importId); + if (imp == this) { + imports.erase(importId, imp); + } + + // Send a message releasing our remote references. + if (this.remoteRefCount > 0 && isConnected()) { + int sizeHint = messageSizeHint(RpcProtocol.Release.factory); + var message = connection.newOutgoingMessage(sizeHint); + var builder = message.getBody().initAs(RpcProtocol.Message.factory).initRelease(); + builder.setId(importId); + builder.setReferenceCount(remoteRefCount); + LOGGER.fine(() -> this.toString() + ": > RELEASE import=" + importId); + message.send(); + } + } + } + + final static class Embargo { + final int id; + final CompletableFuture disembargo = new CompletableFuture<>(); + + Embargo(int id) { + this.id = id; + } + } + + private final ExportTable exports = new ExportTable<>() { + @Override + Export newExportable(int id) { + return new Export(id); + } + }; + + private final ExportTable questions = new ExportTable<>() { + @Override + Question newExportable(int id) { + return new Question(id); + } + }; + + private final ImportTable answers = new ImportTable<>() { + @Override + protected Answer newImportable(int answerId) { + return new Answer(answerId); + } + }; + + private final ImportTable imports = new ImportTable<>() { + @Override + protected Import newImportable(int importId) { + return new Import(importId); + } + }; + + private final ExportTable embargos = new ExportTable<>() { + @Override + Embargo newExportable(int id) { + return new Embargo(id); + } + }; + + private final Map exportsByCap = new HashMap<>(); + private final BootstrapFactory bootstrapFactory; + private final VatNetwork.Connection connection; + private final CompletableFuture disconnectFulfiller; + private Throwable disconnected = null; + private final CompletableFuture messageLoop = new CompletableFuture<>(); + // completes when the message loop exits + private final ReferenceQueue questionRefs = new ReferenceQueue<>(); + private final ReferenceQueue importRefs = new ReferenceQueue<>(); + private final Queue> lastEvals = new ArrayDeque<>(); + + RpcState(BootstrapFactory bootstrapFactory, + VatNetwork.Connection connection, + CompletableFuture disconnectFulfiller) { + this.bootstrapFactory = bootstrapFactory; + this.connection = connection; + this.disconnectFulfiller = disconnectFulfiller; + } + + @Override + public String toString() { + return super.toString() + ": " + this.connection.toString(); + } + + CompletableFuture onDisconnection() { + return this.messageLoop; + } + + void disconnect(Throwable exc) { + if (isDisconnected()) { + // Already disconnected. + return; + } + + var networkExc = RpcException.disconnected(exc.getMessage()); + + // All current questions complete with exceptions. + for (var question: questions) { + var questionRef = question.selfRef; + if (questionRef != null) { + questionRef.fulfill(networkExc); + } + } + + List pipelinesToRelease = new ArrayList<>(); + List clientsToRelease = new ArrayList<>(); + List> tailCallsToRelease = new ArrayList<>(); + List> resolveOpsToRelease = new ArrayList<>(); + + for (var answer: answers) { + if (answer.redirectedResults != null) { + tailCallsToRelease.add(answer.redirectedResults); + answer.redirectedResults = null; + } + + if (answer.pipeline != null) { + pipelinesToRelease.add(answer.pipeline); + answer.pipeline = null; + } + + if (answer.callContext != null) { + answer.callContext.requestCancel(); + } + } + + for (var export: exports) { + clientsToRelease.add(export.clientHook); + resolveOpsToRelease.add(export.resolveOp); + export.clientHook = null; + export.resolveOp = null; + export.refcount = 0; + } + + for (var imp: imports) { + if (imp.promise != null) { + imp.promise.completeExceptionally(networkExc); + } + } + + for (var embargo: embargos) { + embargo.disembargo.completeExceptionally(networkExc); + } + + // Send an abort message, but ignore failure. + try { + int sizeHint = messageSizeHint() + exceptionSizeHint(exc); + var message = this.connection.newOutgoingMessage(sizeHint); + var abort = message.getBody().getAs(RpcProtocol.Message.factory).initAbort(); + FromException(exc, abort); + LOGGER.log(Level.FINE, this.toString() + ": > ABORT", exc); + message.send(); + } + catch (Exception ignored) { + } + + var shutdownPromise = this.connection.shutdown() + .exceptionally(ioExc -> { + + assert !(ioExc instanceof IOException); + + if (ioExc instanceof RpcException) { + var rpcExc = (RpcException)exc; + + // Don't report disconnects as an error + if (rpcExc.getType() == RpcException.Type.DISCONNECTED) { + return null; + } + } + else if (ioExc instanceof CompletionException) { + var compExc = (CompletionException)ioExc; + if (compExc.getCause() instanceof ClosedChannelException) { + return null; + } + } + + return null; + //return CompletableFuture.failedFuture(ioExc); + }); + + this.disconnected = networkExc; + this.disconnectFulfiller.complete(new DisconnectInfo(shutdownPromise)); + + for (var pipeline: pipelinesToRelease) { + pipeline.cancel(networkExc); + } + } + + final boolean isDisconnected() { + return this.disconnected != null; + } + + final boolean isConnected() { + return !isDisconnected(); + } + + // Run func() before the next IO event. + private void evalLast(Callable func) { + this.lastEvals.add(func); + } + + ClientHook restore() { + var question = questions.next(); + question.isAwaitingReturn = true; + var questionRef = question.selfRef; + var pipeline = new RpcPipeline(questionRef, questionRef.response); + + int sizeHint = messageSizeHint(RpcProtocol.Bootstrap.factory); + var message = connection.newOutgoingMessage(sizeHint); + var builder = message.getBody().initAs(RpcProtocol.Message.factory).initBootstrap(); + builder.setQuestionId(question.id); + LOGGER.fine(() -> this.toString() + ": > BOOTSTRAP question=" + question.id); + message.send(); + + return pipeline.getPipelinedCap(new short[0]); + } + + /** + * Returns a CompletableFuture that, when complete, has processed one message. + */ + public CompletableFuture pollOnce() { + if (isDisconnected()) { + this.messageLoop.completeExceptionally(this.disconnected); + return CompletableFuture.failedFuture(this.disconnected); + } + + return this.connection.receiveIncomingMessage() + .thenAccept(message -> { + if (message == null) { + this.disconnect(RpcException.disconnected("Peer disconnected")); + this.messageLoop.complete(null); + return; + } + try { + this.handleMessage(message); + while (!this.lastEvals.isEmpty()) { + this.lastEvals.remove().call(); + } + } + catch (Throwable rpcExc) { + // either we received an Abort message from peer + // or internal RpcState is bad. + this.disconnect(rpcExc); + } + }); + } + + public void runMessageLoop() { + this.pollOnce().thenRun(this::runMessageLoop).exceptionally(exc -> { + LOGGER.log(Level.FINE, "Event loop exited", exc); + return null; + }); + } + + private void handleMessage(IncomingRpcMessage message) throws RpcException { + var reader = message.getBody().getAs(RpcProtocol.Message.factory); + LOGGER.fine(() -> this.toString() + ": < RPC message: " + reader.which().toString()); + switch (reader.which()) { + case UNIMPLEMENTED: handleUnimplemented(reader.getUnimplemented()); break; + case ABORT: handleAbort(reader.getAbort()); break; + case BOOTSTRAP: handleBootstrap(reader.getBootstrap()); break; + case CALL: handleCall(message, reader.getCall()); break; + case RETURN: handleReturn(message, reader.getReturn()); break; + case FINISH: handleFinish(reader.getFinish()); break; + case RESOLVE: handleResolve(message, reader.getResolve()); break; + case DISEMBARGO: handleDisembargo(reader.getDisembargo()); break; + case RELEASE: handleRelease(reader.getRelease()); break; + default: { + LOGGER.warning(() -> this.toString() + ": < Unhandled RPC message: " + reader.which().toString()); + if (!isDisconnected()) { + // boomin' back atcha + var msg = connection.newOutgoingMessage(); + msg.getBody().initAs(RpcProtocol.Message.factory).setUnimplemented(reader); + LOGGER.fine(() -> this.toString() + ": > UNIMPLEMENTED"); + msg.send(); + } + } + } + + this.cleanupReferences(); + } + + void handleUnimplemented(RpcProtocol.Message.Reader message) { + LOGGER.fine(() -> this.toString() + ": < UNIMPLEMENTED"); + + switch (message.which()) { + case RESOLVE: + var resolve = message.getResolve(); + switch (resolve.which()) { + case CAP: + var cap = resolve.getCap(); + switch (cap.which()) { + case SENDER_HOSTED: + releaseExport(cap.getSenderHosted(), 1); + break; + case SENDER_PROMISE: + releaseExport(cap.getSenderPromise(), 1); + break; + case THIRD_PARTY_HOSTED: + releaseExport(cap.getThirdPartyHosted().getVineId(), 1); + break; + case NONE: + // Should never happen. + case RECEIVER_ANSWER: + case RECEIVER_HOSTED: + // Nothing to do. + break; + } + break; + case EXCEPTION: + // Nothing to do + break; + } + break; + default: + assert false: "Peer did not implement required RPC message type. " + message.which().name(); + break; + } + } + + void handleAbort(RpcProtocol.Exception.Reader abort) throws RpcException { + var exc = ToException(abort); + LOGGER.log(Level.FINE, this.toString() + ": < ABORT ", exc); + throw exc; + } + + void handleBootstrap(RpcProtocol.Bootstrap.Reader bootstrap) { + LOGGER.fine(() -> this.toString() + ": < BOOTSTRAP question=" + bootstrap.getQuestionId()); + if (isDisconnected()) { + return; + } + + var answerId = bootstrap.getQuestionId(); + var answer = answers.put(answerId); + if (answer.active) { + assert false: "bootstrap questionId is already in use: " + answerId; + return; + } + answer.active = true; + + var capTable = new BuilderCapabilityTable(); + int sizeHint = messageSizeHint(RpcProtocol.Return.factory) + + RpcProtocol.Payload.factory.structSize().total(); + var response = connection.newOutgoingMessage(sizeHint); + + var ret = response.getBody().getAs(RpcProtocol.Message.factory).initReturn(); + ret.setAnswerId(answerId); + + var payload = ret.initResults(); + var content = payload.getContent().imbue(capTable); + var cap = this.bootstrapFactory.createFor(connection.getPeerVatId()); + content.setAs(Capability.factory, cap); + var caps = capTable.getTable(); + var capHook = caps.length != 0 + ? caps[0] + : Capability.newNullCap(); + + var fds = List.of(); + response.setFds(List.of()); + + answer.resultExports = writeDescriptors(caps, payload, fds); + assert answer.pipeline == null; + answer.pipeline = ops -> ops.length == 0 + ? capHook + : Capability.newBrokenCap("Invalid pipeline transform."); + + LOGGER.fine(() -> this.toString() + ": > RETURN answer=" + answerId); + response.send(); + + assert answer.active; + assert answer.resultExports != null; + assert answer.pipeline != null; + } + + void handleCall(IncomingRpcMessage message, RpcProtocol.Call.Reader call) { + LOGGER.fine(() -> this.toString() + ": < CALL question=" + call.getQuestionId()); + + var cap = getMessageTarget(call.getTarget()); + if (cap == null) { + return; + } + + boolean redirectResults; + switch (call.getSendResultsTo().which()) { + case CALLER: redirectResults = false; break; + case YOURSELF: redirectResults = true; break; + default: { + assert false : "Unsupported 'Call.sendResultsTo'."; + return; + } + } + + var payload = call.getParams(); + var capTableArray = receiveCaps(payload.getCapTable(), message.getAttachedFds()); + var answerId = call.getQuestionId(); + var context = new RpcCallContext( + answerId, message, capTableArray, + payload.getContent(), redirectResults, + call.getInterfaceId(), call.getMethodId()); + + { + var answer = answers.put(answerId); + if (answer.active) { + assert false: "questionId is already in use"; + return; + } + + answer.active = true; + answer.callContext = context; + } + + var pap = startCall(call.getInterfaceId(), call.getMethodId(), cap, context); + + // Things may have changed -- in particular if startCall() immediately called + // context->directTailCall(). + + { + var answer = answers.find(answerId); + assert answer != null; + assert answer.pipeline == null; + answer.pipeline = pap.pipeline; + + var callReady = pap.promise; + + if (redirectResults) { + answer.redirectedResults = callReady.thenApply(void_ -> + context.consumeRedirectedResponse()); + } + else { + callReady.whenComplete((void_, exc) -> { + if (exc == null) { + context.sendReturn(); + } + else { + context.sendErrorReturn(exc); + } + }); + } + + context.whenCancelled().thenRun(() -> { + callReady.cancel(false); + }); + } + } + + private ClientHook.VoidPromiseAndPipeline startCall(long interfaceId, short methodId, ClientHook cap, RpcCallContext context) { + // TODO gateways...? + return cap.call(interfaceId, methodId, context); + } + + void handleReturn(IncomingRpcMessage message, RpcProtocol.Return.Reader callReturn) { + LOGGER.fine(() -> this.toString() + ": < RETURN answer=" + callReturn.getAnswerId()); + + var question = questions.find(callReturn.getAnswerId()); + if (question == null) { + assert false: "Invalid question ID in Return message."; + return; + } + + if (!question.isAwaitingReturn) { + assert false: "Duplicate Return"; + return; + } + question.isAwaitingReturn = false; + + int[] exportsToRelease = null; + if (callReturn.getReleaseParamCaps()) { + exportsToRelease = question.paramExports; + question.paramExports = null; + } + + var questionRef = question.selfRef; + if (questionRef == null) { + if (callReturn.isTakeFromOtherQuestion()) { + var answer = this.answers.find(callReturn.getTakeFromOtherQuestion()); + if (answer != null) { + answer.redirectedResults = null; + } + } + + // Looks like this question was canceled earlier, so `Finish` was already sent, with + // `releaseResultCaps` set true so that we don't have to release them here. We can go + // ahead and delete it from the table. + // TODO Should we do this? + questions.erase(callReturn.getAnswerId(), question); + + if (exportsToRelease != null) { + this.releaseExports(exportsToRelease); + } + return; + } + + switch (callReturn.which()) { + case RESULTS: + if (question.isTailCall) { + assert false: "Tail call `Return` must set `resultsSentElsewhere`, not `results`."; + break; + } + + var payload = callReturn.getResults(); + var capTable = receiveCaps(payload.getCapTable(), message.getAttachedFds()); + var response = new RpcResponseImpl(questionRef, message, capTable, payload.getContent()); + questionRef.fulfill(response); + break; + + case EXCEPTION: + if (question.isTailCall) { + assert false: "Tail call `Return` must set `resultsSentElsewhere`, not `exception`."; + break; + } + questionRef.fulfill(ToException(callReturn.getException())); + break; + + case CANCELED: + assert false : "Return message falsely claims call was canceled."; + break; + + case RESULTS_SENT_ELSEWHERE: + if (!question.isTailCall) { + assert false: "`Return` had `resultsSentElsewhere` but this was not a tail call."; + break; + } + // Tail calls are fulfilled with a null pointer. + questionRef.fulfill(() -> null); + break; + + case TAKE_FROM_OTHER_QUESTION: + var other = callReturn.getTakeFromOtherQuestion(); + var answer = answers.find(other); + if (answer == null) { + assert false: "`Return.takeFromOtherQuestion` had invalid answer ID."; + break; + } + if (answer.redirectedResults == null) { + assert false: "`Return.takeFromOtherQuestion` referenced a call that did not use `sendResultsTo.yourself`."; + break; + } + questionRef.response = answer.redirectedResults; + answer.redirectedResults = null; + break; + + default: + assert false : "Unknown 'Return' type."; + break; + } + + if (exportsToRelease != null) { + this.releaseExports(exportsToRelease); + } + } + + void handleFinish(RpcProtocol.Finish.Reader finish) { + LOGGER.fine(() -> this.toString() + ": < FINISH question=" + finish.getQuestionId()); + + var answer = answers.find(finish.getQuestionId()); + if (answer == null || !answer.active) { + assert false: "'Finish' for invalid question ID."; + return; + } + + var exportsToRelease = finish.getReleaseResultCaps() + ? answer.resultExports + : null; + + answer.resultExports = null; + + // If the call isn't actually done yet, cancel it. Otherwise, we can go ahead and erase the + // question from the table. + var ctx = answer.callContext; + if (ctx != null) { + ctx.requestCancel(); + } + + // Remove question id + // this is a different then c++ implementation, but it is required as java's promises doesn't support + // all features of kj's promises + var questionId = finish.getQuestionId(); + answers.erase(questionId); + + if (exportsToRelease != null) { + this.releaseExports(exportsToRelease); + } + } + + private void handleResolve(IncomingRpcMessage message, RpcProtocol.Resolve.Reader resolve) { + LOGGER.fine(() -> this.toString() + ": < RESOLVE promise=" + resolve.getPromiseId()); + + var importId = resolve.getPromiseId(); + var imp = this.imports.find(importId); + if (imp == null) { + return; + } + + if (imp.promise == null) { + assert imp.disposer != null: "Import already resolved."; + // It appears this is a valid entry on the import table, but was not expected to be a + // promise. + return; + } + + // This import is an unfulfilled promise. + switch (resolve.which()) { + case CAP:{ + var cap = receiveCap(resolve.getCap(), message.getAttachedFds()); + imp.promise.complete(cap); + break; + } + case EXCEPTION: { + var exc = ToException(resolve.getException()); + imp.promise.completeExceptionally(exc); + break; + } + default: { + assert false : "Unknown 'Resolve' type."; + } + } + } + + private void handleRelease(RpcProtocol.Release.Reader release) { + LOGGER.fine(() -> this.toString() + ": < RELEASE promise=" + release.getId()); + this.releaseExport(release.getId(), release.getReferenceCount()); + } + + private void handleDisembargo(RpcProtocol.Disembargo.Reader disembargo) { + LOGGER.fine(() -> this.toString() + ": < DISEMBARGO"); + + var ctx = disembargo.getContext(); + switch (ctx.which()) { + case SENDER_LOOPBACK: + var target = getMessageTarget(disembargo.getTarget()); + if (target == null) { + // Exception already reported. + return; + } + for (; ; ) { + var resolved = target.getResolved(); + if (resolved == null) { + break; + } + target = resolved; + } + + if (target.getBrand() != this) { + assert false: "'Disembargo' of type 'senderLoopback' sent to an object that does not point back to the sender."; + return; + } + + var embargoId = ctx.getSenderLoopback(); + var rpcTarget = (RpcClient) target; + + Callable sendDisembargo = () -> { + if (isDisconnected()) { + return null; + } + + int sizeHint = messageSizeHint(RpcProtocol.Disembargo.factory) + MESSAGE_TARGET_SIZE_HINT; + var message = connection.newOutgoingMessage(sizeHint); + var builder = message.getBody().initAs(RpcProtocol.Message.factory).initDisembargo(); + var redirect = rpcTarget.writeTarget(builder.initTarget()); + // Disembargoes should only be sent to capabilities that were previously the subject of + // a `Resolve` message. But `writeTarget` only ever returns non-null when called on + // a PromiseClient. The code which sends `Resolve` and `Return` should have replaced + // any promise with a direct node in order to solve the Tribble 4-way race condition. + // See the documentation of Disembargo in rpc.capnp for more. + if (redirect != null) { + assert false : "'Disembargo' of type 'senderLoopback' sent to an object that does not appear to have been the subject of a previous 'Resolve' message."; + return null; + } + builder.getContext().setReceiverLoopback(embargoId); + LOGGER.fine(() -> this.toString() + ": > DISEMBARGO"); + message.send(); + return null; + }; + this.evalLast(sendDisembargo); + break; + + case RECEIVER_LOOPBACK: + var embargo = this.embargos.find(ctx.getReceiverLoopback()); + if (embargo == null) { + assert false: "Invalid embargo ID in 'Disembargo.context.receiverLoopback'."; + return; + } + embargo.disembargo.complete(null); + embargos.erase(ctx.getReceiverLoopback(), embargo); + break; + + default: + assert false: "Unimplemented Disembargo type. " + ctx.which(); + break; + } + } + + private int[] writeDescriptors(ClientHook[] capTable, RpcProtocol.Payload.Builder payload, List fds) { + if (capTable.length == 0) { + return new int[0]; + } + + var capTableBuilder = payload.initCapTable(capTable.length); + var exports = new ArrayList(); + for (int ii = 0; ii < capTable.length; ++ii) { + var cap = capTable[ii]; + if (cap == null) { + capTableBuilder.get(ii).setNone(null); + continue; + } + + var exportId = writeDescriptor(cap, capTableBuilder.get(ii), fds); + if (exportId != null) { + exports.add(exportId); + } + } + + return exports.stream() + .mapToInt(Integer::intValue) + .toArray(); + } + + private Integer writeDescriptor(ClientHook cap, RpcProtocol.CapDescriptor.Builder descriptor, List fds) { + ClientHook inner = cap; + for (;;) { + var resolved = inner.getResolved(); + if (resolved != null) { + inner = resolved; + } + else { + break; + } + } + + var fd = inner.getFd(); + if (fd != null) { + fds.add(fd); + } + + if (inner.getBrand() == this) { + return ((RpcClient) inner).writeDescriptor(descriptor, fds); + } + + var exportId = exportsByCap.get(inner); + if (exportId != null) { + // We've already seen and exported this capability. + var export = exports.find(exportId); + export.refcount++; + descriptor.setSenderHosted(exportId); + return exportId; + } + + // This is the first time we've seen this capability. + var export = exports.next(); + export.refcount = 1; + export.clientHook = inner; + + var wrapped = inner.whenMoreResolved(); + if (wrapped != null) { + // This is a promise. Arrange for the `Resolve` message to be sent later. + export.resolveOp = this.resolveExportedPromise(export.exportId, wrapped); + descriptor.setSenderPromise(export.exportId); + } + else { + descriptor.setSenderHosted(export.exportId); + } + return export.exportId; + } + + CompletionStage resolveExportedPromise(int exportId, CompletionStage promise) { + return promise.thenCompose(resolution -> { + if (isDisconnected()) { + return CompletableFuture.completedFuture(null); + } + + resolution = this.getInnermostClient(resolution); + + var exp = exports.find(exportId); + assert exp != null; + exportsByCap.remove(exp.clientHook); + exp.clientHook = resolution; + + if (exp.clientHook.getBrand() != this) { + // We're resolving to a local capability. If we're resolving to a promise, we might be + // able to reuse our export table entry and avoid sending a message. + var more = exp.clientHook.whenMoreResolved(); + if (more != null) { + // We're replacing a promise with another local promise. In this case, we might actually + // be able to just reuse the existing export table entry to represent the new promise -- + // unless it already has an entry. Let's check. + + var insertResult = exportsByCap.put(exp.clientHook, exportId); + // TODO check this behaviour + if (insertResult == null) { + // The new promise was not already in the table, therefore the existing export table + // entry has now been repurposed to represent it. There is no need to send a resolve + // message at all. We do, however, have to start resolving the next promise. + return this.resolveExportedPromise(exportId, more); + } + } + } + + // send a Resolve message + int sizeHint = messageSizeHint(RpcProtocol.Resolve.factory) + CAP_DESCRIPTOR_SIZE_HINT; + var message = connection.newOutgoingMessage(sizeHint); + var resolve = message.getBody().initAs(RpcProtocol.Message.factory).initResolve(); + resolve.setPromiseId(exportId); + var fds = List.of(); + writeDescriptor(exp.clientHook, resolve.initCap(), fds); + message.setFds(fds); + LOGGER.fine(() -> this.toString() + ": > RESOLVE export=" + exportId); + message.send(); + return CompletableFuture.completedFuture(null); + }).whenComplete((value, exc) -> { + if (exc == null) { + return; + } + int sizeHint = messageSizeHint(RpcProtocol.Resolve.factory) + exceptionSizeHint(exc); + var message = connection.newOutgoingMessage(sizeHint); + var resolve = message.getBody().initAs(RpcProtocol.Message.factory).initResolve(); + resolve.setPromiseId(exportId); + FromException(exc, resolve.initException()); + LOGGER.fine(() -> this.toString() + ": > RESOLVE FAILED export=" + exportId + " msg=" + exc.getMessage()); + message.send(); + }); + } + + void releaseExports(int[] exports) { + for (var exportId: exports) { + this.releaseExport(exportId, 1); + } + } + + void releaseExport(int exportId, int refcount) { + var export = exports.find(exportId); + if (export == null) { + assert false: "Cannot release unknown export"; + return; + } + + if (export.refcount < refcount) { + assert false: "Over-reducing export refcount. exported=" + export.refcount + ", requested=" + refcount; + return; + } + + export.refcount -= refcount; + if (export.refcount == 0) { + exportsByCap.remove(exportId, export.clientHook); + exports.erase(exportId, export); + } + } + + private List receiveCaps(StructList.Reader capTable, List fds) { + var result = new ArrayList(); + for (var cap: capTable) { + result.add(receiveCap(cap, fds)); + } + return result; + } + + private ClientHook receiveCap(RpcProtocol.CapDescriptor.Reader descriptor, List fds) { + FileDescriptor fd = null; + int fdIndex = descriptor.getAttachedFd(); + if (fdIndex >= 0 && fdIndex < fds.size()) { + fd = fds.get(fdIndex); + if (fd != null) { + fds.set(fdIndex, null); + } + } + + switch (descriptor.which()) { + case NONE: + return null; + + case SENDER_HOSTED: + return importCap(descriptor.getSenderHosted(), false, fd); + + case SENDER_PROMISE: + return importCap(descriptor.getSenderPromise(), true, fd); + + case RECEIVER_HOSTED: { + var exp = exports.find(descriptor.getReceiverHosted()); + if (exp == null) { + return Capability.newBrokenCap("invalid 'receiverHosted' export ID"); + } + else if (exp.clientHook.getBrand() == this) { + return new TribbleRaceBlocker(exp.clientHook); + } + else { + return exp.clientHook; + } + } + + case RECEIVER_ANSWER: { + var promisedAnswer = descriptor.getReceiverAnswer(); + var answer = answers.find(promisedAnswer.getQuestionId()); + var ops = ToPipelineOps(promisedAnswer); + + if (answer == null || !answer.active || answer.pipeline == null || ops == null) { + return Capability.newBrokenCap("invalid 'receiverAnswer'"); + } + + var result = answer.pipeline.getPipelinedCap(ops); + if (result == null) { + return Capability.newBrokenCap("Unrecognised pipeline ops"); + } else if (result.getBrand() == this) { + return new TribbleRaceBlocker(result); + } else { + return result; + } + } + + case THIRD_PARTY_HOSTED: + return Capability.newBrokenCap("Third party caps not supported"); + + default: + return Capability.newBrokenCap("unknown CapDescriptor type"); + } + } + + private ClientHook importCap(int importId, boolean isPromise, FileDescriptor fd) { + // Receive a new import. + var imp = imports.put(importId); + + ImportClient importClient; + + // new import + if (imp.disposer == null) { + var importRef = new ImportRef(importId); + imp.disposer = new ImportDisposer(importRef); + importClient = new ImportClient(importRef); + } + else { + var importRef = imp.disposer.get(); + if (importRef == null) { + // Import still exists, but has no references. Resurrect it. + importRef = new ImportRef(importId); + imp.disposer = new ImportDisposer(importRef); + importClient = new ImportClient(importRef); + } + else { + importClient = new ImportClient(importRef); + } + } + + + imp.setFdIfMissing(fd); + imp.addRemoteRef(); + + if (!isPromise) { + return importClient; + } + + if (imp.appClient != null) { + return imp.appClient; + } + + imp.promise = new CompletableFuture<>(); + var result = new PromiseClient(importClient, imp.promise, importClient.importRef); + imp.appClient = result; + return result; + } + + ClientHook writeTarget(ClientHook cap, RpcProtocol.MessageTarget.Builder target) { + // If calls to the given capability should pass over this connection, fill in `target` + // appropriately for such a call and return nullptr. Otherwise, return a `ClientHook` to which + // the call should be forwarded; the caller should then delegate the call to that `ClientHook`. + // + // The main case where this ends up returning non-null is if `cap` is a promise that has + // recently resolved. The application might have started building a request before the promise + // resolved, and so the request may have been built on the assumption that it would be sent over + // this network connection, but then the promise resolved to point somewhere else before the + // request was sent. Now the request has to be redirected to the new target instead. + + return cap.getBrand() == this + ? ((RpcClient)cap).writeTarget(target) + : cap; + } + + ClientHook getMessageTarget(RpcProtocol.MessageTarget.Reader target) { + switch (target.which()) { + case IMPORTED_CAP: { + var exp = exports.find(target.getImportedCap()); + if (exp != null) { + return exp.clientHook; + } + else { + assert false: "Message target is not a current export ID."; + return null; + } + } + case PROMISED_ANSWER: { + var promisedAnswer = target.getPromisedAnswer(); + var questionId = promisedAnswer.getQuestionId(); + var base = answers.put(questionId); + if (!base.active) { + assert false: "PromisedAnswer.questionId is not a current question."; + return null; + } + var pipeline = base.pipeline; + if (pipeline == null) { + pipeline = Capability.newBrokenPipeline( + RpcException.failed("Pipeline call on a request that returned no capabilities or was already closed.")); + } + var ops = ToPipelineOps(promisedAnswer); + if (ops == null) { + return null; + } + return pipeline.getPipelinedCap(ops); + } + default: { + assert false: "Unknown message target type. " + target.which(); + return null; + } + } + } + + ClientHook getInnermostClient(ClientHook client) { + for (;;) { + var inner = client.getResolved(); + if (inner != null) { + client = inner; + } + else { + break; + } + } + + if (client.getBrand() == this) { + return ((RpcClient)client).getInnermostClient(); + } + + return client; + } + + interface RpcResponse extends ResponseHook { + AnyPointer.Reader getResults(); + } + + interface RpcServerResponse { + AnyPointer.Builder getResultsBuilder(); + } + + class RpcResponseImpl implements RpcResponse { + + private final IncomingRpcMessage message; + private final QuestionRef questionRef; + private final AnyPointer.Reader results; + + RpcResponseImpl(QuestionRef questionRef, + IncomingRpcMessage message, + List capTable, + AnyPointer.Reader results) { + this.questionRef = questionRef; + this.message = message; + this.results = results.imbue(new ReaderCapabilityTable(capTable)); + } + + public AnyPointer.Reader getResults() { + return results; + } + } + + class RpcServerResponseImpl implements RpcServerResponse { + + final OutgoingRpcMessage message; + final RpcProtocol.Payload.Builder payload; + final BuilderCapabilityTable capTable = new BuilderCapabilityTable(); + + RpcServerResponseImpl(OutgoingRpcMessage message, RpcProtocol.Payload.Builder payload) { + this.message = message; + this.payload = payload; + } + + @Override + public AnyPointer.Builder getResultsBuilder() { + return this.payload.getContent().imbue(capTable); + } + + int[] send() { + var capTable = this.capTable.getTable(); + var fds = List.of(); + var exports = writeDescriptors(capTable, payload, fds); + // TODO process FDs + message.setFds(fds); + + for (int ii = 0; ii < capTable.length; ++ii) { + var slot = capTable[ii]; + if (slot != null) { + capTable[ii] = getInnermostClient(slot); + } + } + + message.send(); + return exports; + } + } + + private static final class LocallyRedirectedRpcResponse + implements RpcServerResponse, + RpcResponse { + + private final MessageBuilder message = new MessageBuilder(); + + @Override + public AnyPointer.Builder getResultsBuilder() { + return this.message.getRoot(AnyPointer.factory); + } + + @Override + public AnyPointer.Reader getResults() { + return this.getResultsBuilder().asReader(); + } + } + + private final class RpcCallContext implements CallContextHook { + + private final int answerId; + private final long interfaceId; + private final short methodId; + + // request + private IncomingRpcMessage request; + private final AnyPointer.Reader params; + + // response + private RpcServerResponse response; + private RpcProtocol.Return.Builder returnMessage; + private final boolean redirectResults; + private boolean responseSent = false; + private CompletableFuture tailCallPipeline; + + private boolean cancelRequested = false; + private boolean cancelAllowed = false; + + private final CompletableFuture canceller = new CompletableFuture<>(); + + RpcCallContext(int answerId, IncomingRpcMessage request, List capTable, + AnyPointer.Reader params, boolean redirectResults, + long interfaceId, short methodId) { + this.answerId = answerId; + this.interfaceId = interfaceId; + this.methodId = methodId; + this.request = request; + this.params = params.imbue(new ReaderCapabilityTable(capTable)); + this.redirectResults = redirectResults; + } + + @Override + public AnyPointer.Reader getParams() { + return this.params; + } + + @Override + public void releaseParams() { + this.request = null; + } + + @Override + public AnyPointer.Builder getResults(int sizeHint) { + if (this.response == null) { + + if (this.redirectResults || isDisconnected()) { + this.response = new LocallyRedirectedRpcResponse(); + } + else { + sizeHint += messageSizeHint(RpcProtocol.Return.factory) + + RpcProtocol.Payload.factory.structSize().total(); + var message = connection.newOutgoingMessage(sizeHint); + this.returnMessage = message.getBody().initAs(RpcProtocol.Message.factory).initReturn(); + this.response = new RpcServerResponseImpl(message, returnMessage.getResults()); + } + } + + return this.response.getResultsBuilder(); + } + + @Override + public CompletableFuture tailCall(RequestHook request) { + var result = this.directTailCall(request); + if (this.tailCallPipeline != null) { + this.tailCallPipeline.complete(new AnyPointer.Pipeline(result.pipeline)); + } + return result.promise.copy(); + } + + @Override + public void allowCancellation() { + boolean previouslyRequestedButNotAllowed = (!this.cancelAllowed && this.cancelRequested); + this.cancelAllowed = true; + + if (previouslyRequestedButNotAllowed) { + this.canceller.complete(null); + } + } + + @Override + public CompletableFuture onTailCall() { + assert this.tailCallPipeline == null: "Called onTailCall twice?"; + this.tailCallPipeline = new CompletableFuture<>(); + return this.tailCallPipeline.copy(); + } + + @Override + public ClientHook.VoidPromiseAndPipeline directTailCall(RequestHook request) { + assert this.response == null: "Can't call tailCall() after initializing the results struct."; + + if (request.getBrand() == RpcState.this && !this.redirectResults) { + // The tail call is headed towards the peer that called us in the first place, so we can + // optimize out the return trip. + + var tailInfo = ((RpcRequest)request).tailSend(); + if (tailInfo != null) { + if (isFirstResponder()) { + if (isConnected()) { + var message = connection.newOutgoingMessage( + messageSizeHint() + + RpcProtocol.Return.factory.structSize().total()); + var builder = message.getBody().initAs(RpcProtocol.Message.factory).initReturn(); + builder.setAnswerId(this.answerId); + builder.setReleaseParamCaps(false); + builder.setTakeFromOtherQuestion(tailInfo.questionId); + LOGGER.fine(() -> this.toString() + ": > RETURN answer=" + answerId); + message.send(); + } + + cleanupAnswerTable(null); + } + return new ClientHook.VoidPromiseAndPipeline(tailInfo.promise, tailInfo.pipeline); + } + } + + // Just forward to another local call + var response = request.send(); + var promise = response.thenAccept( + results -> getResults(0).setAs(AnyPointer.factory, results)); + return new ClientHook.VoidPromiseAndPipeline(promise, response.pipeline().hook); + } + + RpcResponse consumeRedirectedResponse() { + assert this.redirectResults; + + if (this.response == null) { + getResults(); // force initialization of response + } + + return ((LocallyRedirectedRpcResponse) this.response); + } + + private void sendReturn() { + assert !redirectResults; + + if (!this.cancelRequested && isDisconnected()) { + assert false : "Cancellation should have been requested on disconnect."; + return; + } + + if (this.response == null) { + getResults(); // force initialization + } + + this.returnMessage.setAnswerId(this.answerId); + this.returnMessage.setReleaseParamCaps(false); + + LOGGER.fine(() -> RpcState.this.toString() + ": > RETURN answer=" + this.answerId); + + int[] exports = null; + try { + exports = ((RpcServerResponseImpl) response).send(); + } catch (Throwable exc) { + this.responseSent = false; + sendErrorReturn(exc); + } + cleanupAnswerTable(exports); + } + + private void sendErrorReturn(Throwable exc) { + assert !redirectResults; + + if (!isFirstResponder()) { + return; + } + + if (isConnected()) { + var message = connection.newOutgoingMessage(); + var builder = message.getBody().initAs(RpcProtocol.Message.factory).initReturn(); + builder.setAnswerId(this.answerId); + builder.setReleaseParamCaps(false); + FromException(exc, builder.initException()); + LOGGER.log(Level.FINE, this.toString() + ": > RETURN", exc); + message.send(); + } + + cleanupAnswerTable(null); + } + + private boolean isFirstResponder() { + if (this.responseSent) { + return false; + } + this.responseSent = true; + return true; + } + + private void cleanupAnswerTable(int[] resultExports) { + if (this.cancelRequested) { + assert resultExports == null || resultExports.length == 0; + // Already received `Finish` so it's our job to erase the table entry. We shouldn't have + // sent results if canceled, so we shouldn't have an export list to deal with. + answers.erase(this.answerId); + } + else { + // We just have to null out callContext and set the exports. + var answer = answers.find(answerId); + answer.callContext = null; + answer.resultExports = resultExports; + } + } + + void requestCancel() { + // Hints that the caller wishes to cancel this call. At the next time when cancellation is + // deemed safe, the RpcCallContext shall send a canceled Return -- or if it never becomes + // safe, the RpcCallContext will send a normal return when the call completes. Either way + // the RpcCallContext is now responsible for cleaning up the entry in the answer table, since + // a Finish message was already received. + + boolean previouslyAllowedButNotRequested = (this.cancelAllowed && !this.cancelRequested); + this.cancelRequested = true; + + if (previouslyAllowedButNotRequested) { + // We just set CANCEL_REQUESTED, and CANCEL_ALLOWED was already set previously. Initiate + // the cancellation. + this.canceller.complete(null); + } + } + + /** Completed by the call context when a cancellation has been + * requested and cancellation is allowed + */ + CompletableFuture whenCancelled() { + return this.canceller; + } + } + + enum PipelineState { + WAITING, RESOLVED, BROKEN + } + + private class RpcPipeline implements PipelineHook { + + private final QuestionRef questionRef; + private PipelineState state = PipelineState.WAITING; + private RpcResponse resolved; + private Throwable broken; + + private final HashMap, ClientHook> clientMap = new HashMap<>(); + private final CompletableFuture redirectLater; + private final CompletableFuture resolveSelf; + + RpcPipeline(QuestionRef questionRef, + CompletableFuture redirectLater) { + this.questionRef = questionRef; + assert redirectLater != null; + this.redirectLater = redirectLater; + this.resolveSelf = this.redirectLater + .thenApply(response -> { + this.state = PipelineState.RESOLVED; + this.resolved = response; + return response; + }) + .exceptionally(exc -> { + this.state = PipelineState.BROKEN; + this.broken = exc; + return null; + }); + } + + /** + * Construct a new RpcPipeline that is never expected to resolve. + */ + RpcPipeline(QuestionRef questionRef) { + this.questionRef = questionRef; + this.redirectLater = null; + this.resolveSelf = null; + } + + @Override + public ClientHook getPipelinedCap(short[] ops) { + var key = new ArrayList(ops.length); + for (short op: ops) { + key.add(op); + } + + return this.clientMap.computeIfAbsent(key, k -> { + switch (state) { + case WAITING: { + var pipelineClient = new PipelineClient(this.questionRef, ops); + if (this.redirectLater == null) { + // This pipeline will never get redirected, so just return the PipelineClient. + return pipelineClient; + } + + assert this.resolveSelf != null; + var resolutionPromise = this.resolveSelf.thenApply( + response -> response.getResults().getPipelinedCap(ops)); + return new PromiseClient(pipelineClient, resolutionPromise, null); + } + case RESOLVED: { + assert this.resolved != null; + return this.resolved.getResults().getPipelinedCap(ops); + } + case BROKEN: { + assert this.broken != null; + return Capability.newBrokenCap(broken); + } + }; + assert false; + return null; + }); + } + + @Override + public void cancel(Throwable exc) { + this.questionRef.fulfill(exc); + } + } + + abstract class RpcClient implements ClientHook { + + public abstract Integer writeDescriptor(RpcProtocol.CapDescriptor.Builder descriptor, List fds); + + public abstract ClientHook writeTarget(RpcProtocol.MessageTarget.Builder target); + + public ClientHook getInnermostClient() { + return this; + } + + @Override + public Request newCall(long interfaceId, short methodId) { + return newCallNoIntercept(interfaceId, methodId); + } + + @Override + public VoidPromiseAndPipeline call(long interfaceId, short methodId, CallContextHook ctx) { + return this.callNoIntercept(interfaceId, methodId, ctx); + } + + public VoidPromiseAndPipeline callNoIntercept(long interfaceId, short methodId, CallContextHook ctx) { + // Implement call() by copying params and results messages. + var params = ctx.getParams(); + var request = newCallNoIntercept(interfaceId, methodId); + request.getParams().setAs(AnyPointer.factory, params); + ctx.releaseParams(); + ctx.allowCancellation(); + return ctx.directTailCall(request.getHook()); + } + + @Override + public final Object getBrand() { + return RpcState.this; + } + + private Request newCallNoIntercept(long interfaceId, short methodId) { + if (isDisconnected()) { + return Capability.newBrokenRequest(disconnected); + } + + var request = new RpcRequest(this); + var callBuilder = request.getCall(); + callBuilder.setInterfaceId(interfaceId); + callBuilder.setMethodId(methodId); + var root = request.getRoot(); + return Capability.newTypelessRequest(root, request); + } + } + + class RpcRequest implements RequestHook { + + private final RpcClient target; + private final OutgoingRpcMessage message; + private final BuilderCapabilityTable capTable = new BuilderCapabilityTable(); + private final RpcProtocol.Call.Builder callBuilder; + private final AnyPointer.Builder paramsBuilder; + + RpcRequest(RpcClient target) { + this(target, 0); + } + + RpcRequest(RpcClient target, int sizeHint) { + this.target = target; + sizeHint += RpcProtocol.Call.factory.structSize().total() + + RpcProtocol.Payload.factory.structSize().total() + + MESSAGE_TARGET_SIZE_HINT; + this.message = connection.newOutgoingMessage(sizeHint); + this.callBuilder = message.getBody().getAs(RpcProtocol.Message.factory).initCall(); + this.paramsBuilder = callBuilder.getParams().getContent().imbue(this.capTable); + } + + private AnyPointer.Builder getRoot() { + return this.paramsBuilder; + } + + private RpcProtocol.Call.Builder getCall() { + return this.callBuilder; + } + + @Override + public RemotePromise send() { + if (isDisconnected()) { + return new RemotePromise<>(CompletableFuture.failedFuture(disconnected), + Capability.newBrokenPipeline(disconnected)); + } + + var redirect = this.target.writeTarget(this.callBuilder.getTarget()); + if (redirect != null) { + var redirected = redirect.newCall( + this.callBuilder.getInterfaceId(), this.callBuilder.getMethodId()); + var replacement = Capability.newTypelessRequest(paramsBuilder, redirected.getHook()); + return replacement.sendInternal(); + } + + var questionRef = sendInternal(false); + + // The pipeline must get notified of resolution before the app does to maintain ordering. + var pipeline = new RpcPipeline(questionRef, questionRef.response); + + var appPromise = questionRef.response.thenApply( + hook -> new Response<>(hook.getResults(), hook)); + + return new RemotePromise<>(appPromise, pipeline); + } + + QuestionRef sendInternal(boolean isTailCall) { + // TODO refactor + var fds = List.of(); + var exports = writeDescriptors(capTable.getTable(), callBuilder.getParams(), fds); + message.setFds(fds); + var question = questions.next(); + question.isAwaitingReturn = true; + question.isTailCall = isTailCall; + question.paramExports = exports; + + var questionRef = question.selfRef; + + callBuilder.setQuestionId(question.id); + if (isTailCall) { + callBuilder.getSendResultsTo().getYourself(); + } + try { + LOGGER.fine(() -> RpcState.this.toString() + ": > CALL question=" + question.id); + message.send(); + } catch (Exception exc) { + question.isAwaitingReturn = false; + question.skipFinish = true; + questionRef.fulfill(exc); + } + return questionRef; + } + + @Override + public final Object getBrand() { + return RpcState.this; + } + + final class TailInfo { + int questionId; + CompletableFuture promise; + PipelineHook pipeline; + } + + TailInfo tailSend() { + if (isDisconnected()) { + // Disconnected; fall back to a regular send() which will fail appropriately. + return null; + } + + // TODO implement tail-calls + return null; + } + } + + private class ImportDisposer extends WeakReference { + + private final int importId; + + ImportDisposer(ImportRef importRef) { + super(importRef, importRefs); + this.importId = importRef.importId; + } + + void dispose() { + var imp = imports.find(this.importId); + if (imp != null) { + imp.dispose(); + } + } + } + + private static class ImportRef { + + final int importId; + + ImportRef(int importId) { + this.importId = importId; + } + } + + private class ImportClient extends RpcClient { + + private final ImportRef importRef; + + ImportClient(ImportRef importRef) { + this.importRef = importRef; + } + + @Override + public Integer writeDescriptor(RpcProtocol.CapDescriptor.Builder descriptor, List fds) { + descriptor.setReceiverHosted(this.importRef.importId); + return null; + } + + @Override + public ClientHook writeTarget(RpcProtocol.MessageTarget.Builder target) { + target.setImportedCap(this.importRef.importId); + return null; + } + + @Override + public CompletableFuture whenMoreResolved() { + return null; + } + + @Override + public FileDescriptor getFd() { + var imp = imports.find(this.importRef.importId); + return imp != null ? imp.fd : null; + } + } + + private void cleanupReferences() { + while (true) { + var disposer = (ImportDisposer)this.importRefs.poll(); + if (disposer == null) { + break; + } + disposer.dispose(); + } + + while (true) { + var disposer = (QuestionDisposer)this.questionRefs.poll(); + if (disposer == null) { + break; + } + disposer.dispose(); + } + } + + enum ResolutionType { + UNRESOLVED, + REMOTE, + REFLECTED, + MERGED, + BROKEN + } + + private class PromiseClient extends RpcClient { + + private ClientHook cap; + private final ImportRef importRef; + private boolean receivedCall = false; + private ResolutionType resolutionType = ResolutionType.UNRESOLVED; + private final CompletableFuture eventual; + + PromiseClient(RpcClient initial, + CompletableFuture eventual, + ImportRef importRef) { + this.cap = initial; + this.importRef = importRef; + this.eventual = eventual.handle((resolution, exc) -> { + this.cap = exc == null + ? this.resolve(resolution) + : this.resolve(Capability.newBrokenCap(exc)); + + if (this.importRef != null) { + var imp = imports.find(this.importRef.importId); + if (imp != null && imp.appClient == this) { + imp.appClient = null; + } + } + + return this.cap; + }); + } + + @Override + public Integer writeDescriptor(RpcProtocol.CapDescriptor.Builder target, List fds) { + this.receivedCall = true; + return RpcState.this.writeDescriptor(this.cap, target, fds); + } + + @Override + public ClientHook writeTarget(RpcProtocol.MessageTarget.Builder target) { + this.receivedCall = true; + return RpcState.this.writeTarget(this.cap, target); + } + + @Override + public ClientHook getInnermostClient() { + this.receivedCall = true; + return RpcState.this.getInnermostClient(this.cap); + } + + @Override + public Request newCall(long interfaceId, short methodId) { + this.receivedCall = true; + return super.newCall(interfaceId, methodId); + } + + @Override + public VoidPromiseAndPipeline call(long interfaceId, short methodId, CallContextHook ctx) { + this.receivedCall = true; + return this.cap.call(interfaceId, methodId, ctx); + } + + @Override + public ClientHook getResolved() { + return this.isResolved() + ? this.cap + : null; + } + + @Override + public CompletableFuture whenMoreResolved() { + return this.eventual.copy(); + } + + @Override + public FileDescriptor getFd() { + if (this.isResolved()) { + return this.cap.getFd(); + } + else { + // In theory, before resolution, the ImportClient for the promise could have an FD + // attached, if the promise itself was presented with an attached FD. However, we can't + // really return that one here because it may be closed when we get the Resolve message + // later. In theory we could have the PromiseClient itself take ownership of an FD that + // arrived attached to a promise cap, but the use case for that is questionable. I'm + // keeping it simple for now. + return null; + } + } + + private boolean isResolved() { + return resolutionType != ResolutionType.UNRESOLVED; + } + + private ClientHook resolve(ClientHook replacement) { + assert !isResolved(); + + var replacementBrand = replacement.getBrand(); + boolean isSameConnection = replacementBrand == RpcState.this; + if (isSameConnection) { + // We resolved to some other RPC capability hosted by the same peer. + var promise = replacement.whenMoreResolved(); + if (promise != null) { + var other = (PromiseClient) replacement; + while (other.resolutionType == ResolutionType.MERGED) { + replacement = other.cap; + other = (PromiseClient) replacement; + assert replacement.getBrand() == replacementBrand; + } + + if (other.isResolved()) { + resolutionType = other.resolutionType; + } + else { + other.receivedCall = other.receivedCall || receivedCall; + resolutionType = ResolutionType.MERGED; + } + } + else { + resolutionType = ResolutionType.REMOTE; + } + } + else if (replacement.isNull() || replacement.isError()) { + resolutionType = ResolutionType.BROKEN; + } + else { + resolutionType = ResolutionType.REFLECTED; + } + + assert isResolved(); + + // TODO Flow control + + if (resolutionType == ResolutionType.REFLECTED && receivedCall && !isDisconnected()) { + LOGGER.fine(() -> RpcState.this.toString() + ": embargoing reflected capability " + this.toString()); + // The new capability is hosted locally, not on the remote machine. And, we had made calls + // to the promise. We need to make sure those calls echo back to us before we allow new + // calls to go directly to the local capability, so we need to set a local embargo and send + // a `Disembargo` to echo through the peer. + int sizeHint = messageSizeHint(RpcProtocol.Disembargo.factory); + var message = connection.newOutgoingMessage(sizeHint); + var disembargo = message.getBody().initAs(RpcProtocol.Message.factory).initDisembargo(); + var redirect = RpcState.this.writeTarget(cap, disembargo.initTarget()); + assert redirect == null: "Original promise target should always be from this RPC connection."; + + var embargo = embargos.next(); + disembargo.getContext().setSenderLoopback(embargo.id); + + ClientHook finalReplacement = replacement; + var embargoPromise = embargo.disembargo.thenApply( + void_ -> finalReplacement); + LOGGER.fine(() -> RpcState.this.toString() + ": > DISEMBARGO"); + message.send(); + return Capability.newLocalPromiseClient(embargoPromise); + } + else { + return replacement; + } + } + } + + private class PipelineClient extends RpcClient { + + private final QuestionRef questionRef; + private final short[] ops; + + PipelineClient(QuestionRef questionRef, short[] ops) { + this.questionRef = questionRef; + this.ops = ops.clone(); + } + + @Override + public CompletableFuture whenMoreResolved() { + return null; + } + + @Override + public Integer writeDescriptor(RpcProtocol.CapDescriptor.Builder descriptor, List fds) { + var promisedAnswer = descriptor.initReceiverAnswer(); + promisedAnswer.setQuestionId(questionRef.questionId); + FromPipelineOps(ops, promisedAnswer); + return null; + } + + @Override + public ClientHook writeTarget(RpcProtocol.MessageTarget.Builder target) { + var builder = target.initPromisedAnswer(); + builder.setQuestionId(questionRef.questionId); + FromPipelineOps(ops, builder); + return null; + } + } + + static void FromPipelineOps(short[] ops, RpcProtocol.PromisedAnswer.Builder builder) { + var transforms = builder.initTransform(ops.length); + for (int ii = 0; ii < ops.length; ++ii) { + var transform = transforms.get(ii); + var pointerIndex = ops[ii]; + if (pointerIndex < 0) { + transform.setNoop(null); + } + else { + transform.setGetPointerField(pointerIndex); + } + } + } + + static short[] ToPipelineOps(RpcProtocol.PromisedAnswer.Reader reader) { + var transforms = reader.getTransform(); + var ops = new short[transforms.size()]; + for (int ii = 0; ii < ops.length; ++ii) { + var transform = transforms.get(ii); + switch (transform.which()) { + case NOOP: + ops[ii] = -1; + break; + case GET_POINTER_FIELD: + ops[ii] = transform.getGetPointerField(); + break; + case _NOT_IN_SCHEMA: + return null; + }; + } + return ops; + } + + static void FromException(Throwable exc, RpcProtocol.Exception.Builder builder) { + var type = RpcProtocol.Exception.Type.FAILED; + if (exc instanceof RpcException) { + var rpcExc = (RpcException) exc; + switch (rpcExc.getType()) { + case FAILED: type = RpcProtocol.Exception.Type.FAILED; break; + case OVERLOADED: type = RpcProtocol.Exception.Type.OVERLOADED; break; + case DISCONNECTED: type = RpcProtocol.Exception.Type.DISCONNECTED; break; + case UNIMPLEMENTED: type = RpcProtocol.Exception.Type.UNIMPLEMENTED; break; + }; + } + builder.setType(type); + + var writer = new StringWriter(); + exc.printStackTrace(new PrintWriter(writer)); + builder.setReason(writer.toString()); + } + + static RpcException ToException(RpcProtocol.Exception.Reader reader) { + var type = RpcException.Type.FAILED; + switch (reader.getType()) { + case OVERLOADED: type = RpcException.Type.OVERLOADED; break; + case DISCONNECTED: type = RpcException.Type.DISCONNECTED; break; + case UNIMPLEMENTED: type = RpcException.Type.UNIMPLEMENTED; break; + default: type = RpcException.Type.FAILED; break; + }; + return new RpcException(type, reader.getReason().toString()); + } + + class TribbleRaceBlocker implements ClientHook { + + final ClientHook inner; + + TribbleRaceBlocker(ClientHook inner) { + this.inner = inner; + } + + @Override + public Request newCall(long interfaceId, short methodId) { + return this.inner.newCall(interfaceId, methodId); + } + + @Override + public VoidPromiseAndPipeline call(long interfaceId, short methodId, CallContextHook ctx) { + return this.inner.call(interfaceId, methodId, ctx); + } + + @Override + public ClientHook getResolved() { + return null; + } + + @Override + public CompletableFuture whenMoreResolved() { + return null; + } + + @Override + public Object getBrand() { + return null; + } + + @Override + public FileDescriptor getFd() { + return this.inner.getFd(); + } + } +} diff --git a/runtime-rpc/src/main/java/org/capnproto/RpcSystem.java b/runtime-rpc/src/main/java/org/capnproto/RpcSystem.java new file mode 100644 index 00000000..7f7c1817 --- /dev/null +++ b/runtime-rpc/src/main/java/org/capnproto/RpcSystem.java @@ -0,0 +1,91 @@ +package org.capnproto; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +public class RpcSystem { + + private final VatNetwork network; + private final BootstrapFactory bootstrapFactory; + private final Map, RpcState> connections = new HashMap<>(); + + public RpcSystem(VatNetwork network) { + this(network, clientId -> new Capability.Client( + Capability.newBrokenCap("No bootstrap interface available"))); + } + + public RpcSystem(VatNetwork network, + Capability.Server bootstrapInterface) { + this(network, new Capability.Client(bootstrapInterface)); + } + + public RpcSystem(VatNetwork network, + Capability.Client bootstrapInterface) { + this(network, clientId -> bootstrapInterface); + } + + public RpcSystem(VatNetwork network, + BootstrapFactory bootstrapFactory) { + this.network = network; + this.bootstrapFactory = bootstrapFactory; + } + + public Capability.Client bootstrap(VatId vatId) { + var connection = this.network.connect(vatId); + if (connection != null) { + var state = getConnectionState(connection); + var hook = state.restore(); + return new Capability.Client(hook); + } + else { + return this.bootstrapFactory.createFor(vatId); + } + } + + public void accept(VatNetwork.Connection connection) { + var state = getConnectionState(connection); + state.runMessageLoop(); + } + + private RpcState getConnectionState(VatNetwork.Connection connection) { + return this.connections.computeIfAbsent(connection, conn -> { + var onDisconnect = new CompletableFuture(); + onDisconnect.thenCompose(info -> { + this.connections.remove(connection); + return info.shutdownPromise.thenRun(connection::close); + }); + return new RpcState<>(this.bootstrapFactory, conn, onDisconnect); + }); + } + + public void runOnce() { + for (var state: this.connections.values()) { + state.pollOnce().join(); + return; + } + } + + public void start() { + this.network.accept() + .thenAccept(this::accept) + .thenRunAsync(this::start); + } + + public static + RpcSystem makeRpcClient(VatNetwork network) { + return new RpcSystem<>(network); + } + + public static + RpcSystem makeRpcServer(VatNetwork network, + BootstrapFactory bootstrapFactory) { + return new RpcSystem<>(network, bootstrapFactory); + } + + public static + RpcSystem makeRpcServer(VatNetwork network, + Capability.Client bootstrapInterface) { + return new RpcSystem<>(network, bootstrapInterface); + } +} diff --git a/runtime-rpc/src/main/java/org/capnproto/TwoPartyClient.java b/runtime-rpc/src/main/java/org/capnproto/TwoPartyClient.java new file mode 100644 index 00000000..f1296cb0 --- /dev/null +++ b/runtime-rpc/src/main/java/org/capnproto/TwoPartyClient.java @@ -0,0 +1,45 @@ +package org.capnproto; + +import java.nio.channels.AsynchronousByteChannel; +import java.util.concurrent.CompletableFuture; + +public class TwoPartyClient { + + private final TwoPartyVatNetwork network; + private final RpcSystem rpcSystem; + + public TwoPartyClient(AsynchronousByteChannel channel) { + this(channel, null); + } + + public TwoPartyClient(AsynchronousByteChannel channel, Capability.Client bootstrapInterface) { + this(channel, bootstrapInterface, RpcTwoPartyProtocol.Side.CLIENT); + } + + public TwoPartyClient(AsynchronousByteChannel channel, + Capability.Client bootstrapInterface, + RpcTwoPartyProtocol.Side side) { + this.network = new TwoPartyVatNetwork(channel, side); + this.rpcSystem = new RpcSystem<>(network, bootstrapInterface); + } + + public Capability.Client bootstrap() { + var message = new MessageBuilder(); + var vatId = message.getRoot(RpcTwoPartyProtocol.VatId.factory); + vatId.setSide(network.getSide() == RpcTwoPartyProtocol.Side.CLIENT + ? RpcTwoPartyProtocol.Side.SERVER + : RpcTwoPartyProtocol.Side.CLIENT); + return rpcSystem.bootstrap(vatId.asReader()); + } + + CompletableFuture onDisconnect() { + return this.network.onDisconnect(); + } + + public CompletableFuture runUntil(CompletableFuture done) { + while (!done.isDone()) { + this.rpcSystem.runOnce(); + } + return done; + } +} diff --git a/runtime-rpc/src/main/java/org/capnproto/TwoPartyServer.java b/runtime-rpc/src/main/java/org/capnproto/TwoPartyServer.java new file mode 100644 index 00000000..b4df3399 --- /dev/null +++ b/runtime-rpc/src/main/java/org/capnproto/TwoPartyServer.java @@ -0,0 +1,66 @@ +package org.capnproto; + +import java.nio.channels.*; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public class TwoPartyServer { + + private class AcceptedConnection { + private final AsynchronousByteChannel connection; + private final TwoPartyVatNetwork network; + private final RpcSystem rpcSystem; + + AcceptedConnection(Capability.Client bootstrapInterface, AsynchronousByteChannel connection) { + this.connection = connection; + this.network = new TwoPartyVatNetwork(this.connection, RpcTwoPartyProtocol.Side.SERVER); + this.rpcSystem = new RpcSystem<>(network, bootstrapInterface); + this.rpcSystem.start(); + } + } + + private final Capability.Client bootstrapInterface; + private final List connections = new ArrayList<>(); + + public TwoPartyServer(Capability.Client bootstrapInterface) { + this.bootstrapInterface = bootstrapInterface; + } + + public TwoPartyServer(Capability.Server bootstrapServer) { + this(new Capability.Client(bootstrapServer)); + } + + public void accept(AsynchronousByteChannel channel) { + var connection = new AcceptedConnection(this.bootstrapInterface, channel); + this.connections.add(connection); + connection.network.onDisconnect().whenComplete((x, exc) -> { + this.connections.remove(connection); + }); + } + + public CompletableFuture listen(AsynchronousServerSocketChannel listener) { + var result = new CompletableFuture(); + listener.accept(null, new CompletionHandler<>() { + @Override + public void completed(AsynchronousSocketChannel channel, Object attachment) { + accept(channel); + result.complete(null); + } + + @Override + public void failed(Throwable exc, Object attachment) { + result.completeExceptionally(exc); + } + }); + return result.thenCompose(void_ -> this.listen(listener)); + } + + CompletableFuture drain() { + CompletableFuture loop = CompletableFuture.completedFuture(null); + for (var conn: this.connections) { + loop = CompletableFuture.allOf(loop, conn.network.onDisconnect()); + } + return loop; + } +} diff --git a/runtime-rpc/src/main/java/org/capnproto/TwoPartyVatNetwork.java b/runtime-rpc/src/main/java/org/capnproto/TwoPartyVatNetwork.java new file mode 100644 index 00000000..615318f0 --- /dev/null +++ b/runtime-rpc/src/main/java/org/capnproto/TwoPartyVatNetwork.java @@ -0,0 +1,172 @@ +package org.capnproto; + +import java.io.FileDescriptor; +import java.nio.channels.AsynchronousByteChannel; +import java.nio.channels.AsynchronousSocketChannel; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public class TwoPartyVatNetwork + implements VatNetwork, + VatNetwork.Connection { + + private CompletableFuture previousWrite = CompletableFuture.completedFuture(null); + private final CompletableFuture disconnectPromise = new CompletableFuture<>(); + private final AsynchronousByteChannel channel; + private final RpcTwoPartyProtocol.Side side; + private final MessageBuilder peerVatId = new MessageBuilder(4); + private boolean accepted; + + public TwoPartyVatNetwork(AsynchronousByteChannel channel, RpcTwoPartyProtocol.Side side) { + this.channel = channel; + this.side = side; + this.peerVatId.initRoot(RpcTwoPartyProtocol.VatId.factory).setSide( + side == RpcTwoPartyProtocol.Side.CLIENT + ? RpcTwoPartyProtocol.Side.SERVER + : RpcTwoPartyProtocol.Side.CLIENT); + } + + @Override + public void close() { + try { + this.channel.close(); + this.disconnectPromise.complete(null); + } + catch (Exception exc) { + this.disconnectPromise.completeExceptionally(exc); + } + } + + @Override + public String toString() { + return this.side.toString(); + } + + @Override + public Connection connect(RpcTwoPartyProtocol.VatId.Reader vatId) { + return vatId.getSide() != side + ? this.asConnection() + : null; + } + + @Override + public RpcTwoPartyProtocol.VatId.Reader getPeerVatId() { + return this.peerVatId.getRoot(RpcTwoPartyProtocol.VatId.factory).asReader(); + } + + @Override + public OutgoingRpcMessage newOutgoingMessage(int firstSegmentWordSize) { + return new OutgoingMessage(firstSegmentWordSize); + } + + @Override + public CompletableFuture receiveIncomingMessage() { + return Serialize.readAsync(channel) + .thenApply(reader -> (IncomingRpcMessage) new IncomingMessage(reader)) + .exceptionally(exc -> null); + } + + @Override + public CompletableFuture shutdown() { + assert this.previousWrite != null: "Already shut down"; + + var result = this.previousWrite.whenComplete((void_, exc) -> { + try { + if (this.channel instanceof AsynchronousSocketChannel) { + ((AsynchronousSocketChannel)this.channel).shutdownOutput(); + } + } + catch (Exception ignored) { + } + }); + + this.previousWrite = null; + return result; + } + + public RpcTwoPartyProtocol.Side getSide() { + return side; + } + + public Connection asConnection() { + return this; + } + + public CompletableFuture onDisconnect() { + return this.disconnectPromise.copy(); + } + + public CompletableFuture> accept() { + if (side == RpcTwoPartyProtocol.Side.SERVER & !accepted) { + accepted = true; + return CompletableFuture.completedFuture(this.asConnection()); + } + else { + // never completes + return new CompletableFuture<>(); + } + } + + private synchronized void write(MessageBuilder message) { + this.previousWrite = this.previousWrite.thenCompose(void_ -> Serialize.writeAsync(channel, message)); + } + + final class OutgoingMessage implements OutgoingRpcMessage { + + private final MessageBuilder message; + private List fds = List.of(); + + OutgoingMessage(int firstSegmentWordSize) { + this.message = new MessageBuilder(firstSegmentWordSize); + } + + @Override + public AnyPointer.Builder getBody() { + return message.getRoot(AnyPointer.factory); + } + + @Override + public void setFds(List fds) { + this.fds = fds; + } + + @Override + public void send() { + write(message); + } + + @Override + public int sizeInWords() { + int size = 0; + for (var segment: message.getSegmentsForOutput()) { + size += segment.position(); + } + return size / 2; + } + } + + static final class IncomingMessage implements IncomingRpcMessage { + + private final MessageReader message; + private final List fds; + + IncomingMessage(MessageReader message) { + this(message, List.of()); + } + + IncomingMessage(MessageReader message, List fds) { + this.message = message; + this.fds = fds; + } + + @Override + public AnyPointer.Reader getBody() { + return this.message.getRoot(AnyPointer.factory); + } + + @Override + public List getAttachedFds() { + return this.fds; + } + } +} diff --git a/runtime-rpc/src/main/java/org/capnproto/VatNetwork.java b/runtime-rpc/src/main/java/org/capnproto/VatNetwork.java new file mode 100644 index 00000000..534e8d0e --- /dev/null +++ b/runtime-rpc/src/main/java/org/capnproto/VatNetwork.java @@ -0,0 +1,20 @@ +package org.capnproto; + +import java.util.concurrent.CompletableFuture; + +public interface VatNetwork +{ + interface Connection extends AutoCloseable { + default OutgoingRpcMessage newOutgoingMessage() { + return newOutgoingMessage(BuilderArena.SUGGESTED_FIRST_SEGMENT_WORDS); + } + OutgoingRpcMessage newOutgoingMessage(int firstSegmentWordSize); + CompletableFuture receiveIncomingMessage(); + CompletableFuture shutdown(); + VatId getPeerVatId(); + void close(); + } + + CompletableFuture> accept(); + Connection connect(VatId hostId); +} diff --git a/runtime-rpc/src/main/schema/rpc-twoparty.capnp b/runtime-rpc/src/main/schema/rpc-twoparty.capnp new file mode 100644 index 00000000..1d54b470 --- /dev/null +++ b/runtime-rpc/src/main/schema/rpc-twoparty.capnp @@ -0,0 +1,173 @@ +# Copyright (c) 2013-2014 Sandstorm Development Group, Inc. and contributors +# Licensed under the MIT License: +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +@0xa184c7885cdaf2a1; +# This file defines the "network-specific parameters" in rpc.capnp to support a network consisting +# of two vats. Each of these vats may in fact be in communication with other vats, but any +# capabilities they forward must be proxied. Thus, to each end of the connection, all capabilities +# received from the other end appear to live in a single vat. +# +# Two notable use cases for this model include: +# - Regular client-server communications, where a remote client machine (perhaps living on an end +# user's personal device) connects to a server. The server may be part of a cluster, and may +# call on other servers in the cluster to help service the user's request. It may even obtain +# capabilities from these other servers which it passes on to the user. To simplify network +# common traversal problems (e.g. if the user is behind a firewall), it is probably desirable to +# multiplex all communications between the server cluster and the client over the original +# connection rather than form new ones. This connection should use the two-party protocol, as +# the client has no interest in knowing about additional servers. +# - Applications running in a sandbox. A supervisor process may execute a confined application +# such that all of the confined app's communications with the outside world must pass through +# the supervisor. In this case, the connection between the confined app and the supervisor might +# as well use the two-party protocol, because the confined app is intentionally prevented from +# talking to any other vat anyway. Any external resources will be proxied through the supervisor, +# and so to the contained app will appear as if they were hosted by the supervisor itself. +# +# Since there are only two vats in this network, there is never a need for three-way introductions, +# so level 3 is free. Moreover, because it is never necessary to form new connections, the +# two-party protocol can be used easily anywhere where a two-way byte stream exists, without regard +# to where that byte stream goes or how it was initiated. This makes the two-party runtime library +# highly reusable. +# +# Joins (level 4) _could_ be needed in cases where one or both vats are participating in other +# networks that use joins. For instance, if Alice and Bob are speaking through the two-party +# protocol, and Bob is also participating on another network, Bob may send Alice two or more +# proxied capabilities which, unbeknownst to Bob at the time, are in fact pointing at the same +# remote object. Alice may then request to join these capabilities, at which point Bob will have +# to forward the join to the other network. Note, however, that if Alice is _not_ participating on +# any other network, then Alice will never need to _receive_ a Join, because Alice would always +# know when two locally-hosted capabilities are the same and would never export a redundant alias +# to Bob. So, Alice can respond to all incoming joins with an error, and only needs to implement +# outgoing joins if she herself desires to use this feature. Also, outgoing joins are relatively +# easy to implement in this scenario. +# +# What all this means is that a level 4 implementation of the confined network is barely more +# complicated than a level 2 implementation. However, such an implementation allows the "client" +# or "confined" app to access the server's/supervisor's network with equal functionality to any +# native participant. In other words, an application which implements only the two-party protocol +# can be paired with a proxy app in order to participate in any network. +# +# So, when implementing Cap'n Proto in a new language, it makes sense to implement only the +# two-party protocol initially, and then pair applications with an appropriate proxy written in +# C++, rather than implement other parameterizations of the RPC protocol directly. + +using Cxx = import "/capnp/c++.capnp"; +$Cxx.namespace("capnp::rpc::twoparty"); + +using Java = import "/capnp/java.capnp"; +$Java.package("org.capnproto"); +$Java.outerClassname("RpcTwoPartyProtocol"); + +# Note: SturdyRef is not specified here. It is up to the application to define semantics of +# SturdyRefs if desired. + +enum Side { + server @0; + # The object lives on the "server" or "supervisor" end of the connection. Only the + # server/supervisor knows how to interpret the ref; to the client, it is opaque. + # + # Note that containers intending to implement strong confinement should rewrite SturdyRefs + # received from the external network before passing them on to the confined app. The confined + # app thus does not ever receive the raw bits of the SturdyRef (which it could perhaps + # maliciously leak), but instead receives only a thing that it can pass back to the container + # later to restore the ref. See: + # http://www.erights.org/elib/capability/dist-confine.html + + client @1; + # The object lives on the "client" or "confined app" end of the connection. Only the client + # knows how to interpret the ref; to the server/supervisor, it is opaque. Most clients do not + # actually know how to persist capabilities at all, so use of this is unusual. +} + +struct VatId { + side @0 :Side; +} + +struct ProvisionId { + # Only used for joins, since three-way introductions never happen on a two-party network. + + joinId @0 :UInt32; + # The ID from `JoinKeyPart`. +} + +struct RecipientId {} +# Never used, because there are only two parties. + +struct ThirdPartyCapId {} +# Never used, because there is no third party. + +struct JoinKeyPart { + # Joins in the two-party case are simplified by a few observations. + # + # First, on a two-party network, a Join only ever makes sense if the receiving end is also + # connected to other networks. A vat which is not connected to any other network can safely + # reject all joins. + # + # Second, since a two-party connection bisects the network -- there can be no other connections + # between the networks at either end of the connection -- if one part of a join crosses the + # connection, then _all_ parts must cross it. Therefore, a vat which is receiving a Join request + # off some other network which needs to be forwarded across the two-party connection can + # collect all the parts on its end and only forward them across the two-party connection when all + # have been received. + # + # For example, imagine that Alice and Bob are vats connected over a two-party connection, and + # each is also connected to other networks. At some point, Alice receives one part of a Join + # request off her network. The request is addressed to a capability that Alice received from + # Bob and is proxying to her other network. Alice goes ahead and responds to the Join part as + # if she hosted the capability locally (this is important so that if not all the Join parts end + # up at Alice, the original sender can detect the failed Join without hanging). As other parts + # trickle in, Alice verifies that each part is addressed to a capability from Bob and continues + # to respond to each one. Once the complete set of join parts is received, Alice checks if they + # were all for the exact same capability. If so, she doesn't need to send anything to Bob at + # all. Otherwise, she collects the set of capabilities (from Bob) to which the join parts were + # addressed and essentially initiates a _new_ Join request on those capabilities to Bob. Alice + # does not forward the Join parts she received herself, but essentially forwards the Join as a + # whole. + # + # On Bob's end, since he knows that Alice will always send all parts of a Join together, he + # simply waits until he's received them all, then performs a join on the respective capabilities + # as if it had been requested locally. + + joinId @0 :UInt32; + # A number identifying this join, chosen by the sender. May be reused once `Finish` messages are + # sent corresponding to all of the `Join` messages. + + partCount @1 :UInt16; + # The number of capabilities to be joined. + + partNum @2 :UInt16; + # Which part this request targets -- a number in the range [0, partCount). +} + +struct JoinResult { + joinId @0 :UInt32; + # Matches `JoinKeyPart`. + + succeeded @1 :Bool; + # All JoinResults in the set will have the same value for `succeeded`. The receiver actually + # implements the join by waiting for all the `JoinKeyParts` and then performing its own join on + # them, then going back and answering all the join requests afterwards. + + cap @2 :AnyPointer; + # One of the JoinResults will have a non-null `cap` which is the joined capability. + # + # TODO(cleanup): Change `AnyPointer` to `Capability` when that is supported. +} diff --git a/runtime-rpc/src/main/schema/rpc.capnp b/runtime-rpc/src/main/schema/rpc.capnp new file mode 100644 index 00000000..9a406dd3 --- /dev/null +++ b/runtime-rpc/src/main/schema/rpc.capnp @@ -0,0 +1,1480 @@ +# Copyright (c) 2013-2014 Sandstorm Development Group, Inc. and contributors +# Licensed under the MIT License: +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +@0xb312981b2552a250; +# Recall that Cap'n Proto RPC allows messages to contain references to remote objects that +# implement interfaces. These references are called "capabilities", because they both designate +# the remote object to use and confer permission to use it. +# +# Recall also that Cap'n Proto RPC has the feature that when a method call itself returns a +# capability, the caller can begin calling methods on that capability _before the first call has +# returned_. The caller essentially sends a message saying "Hey server, as soon as you finish +# that previous call, do this with the result!". Cap'n Proto's RPC protocol makes this possible. +# +# The protocol is significantly more complicated than most RPC protocols. However, this is +# implementation complexity that underlies an easy-to-grasp higher-level model of object oriented +# programming. That is, just like TCP is a surprisingly complicated protocol that implements a +# conceptually-simple byte stream abstraction, Cap'n Proto is a surprisingly complicated protocol +# that implements a conceptually-simple object abstraction. +# +# Cap'n Proto RPC is based heavily on CapTP, the object-capability protocol used by the E +# programming language: +# http://www.erights.org/elib/distrib/captp/index.html +# +# Cap'n Proto RPC takes place between "vats". A vat hosts some set of objects and talks to other +# vats through direct bilateral connections. Typically, there is a 1:1 correspondence between vats +# and processes (in the unix sense of the word), although this is not strictly always true (one +# process could run multiple vats, or a distributed virtual vat might live across many processes). +# +# Cap'n Proto does not distinguish between "clients" and "servers" -- this is up to the application. +# Either end of any connection can potentially hold capabilities pointing to the other end, and +# can call methods on those capabilities. In the doc comments below, we use the words "sender" +# and "receiver". These refer to the sender and receiver of an instance of the struct or field +# being documented. Sometimes we refer to a "third-party" that is neither the sender nor the +# receiver. Documentation is generally written from the point of view of the sender. +# +# It is generally up to the vat network implementation to securely verify that connections are made +# to the intended vat as well as to encrypt transmitted data for privacy and integrity. See the +# `VatNetwork` example interface near the end of this file. +# +# When a new connection is formed, the only interesting things that can be done are to send a +# `Bootstrap` (level 0) or `Accept` (level 3) message. +# +# Unless otherwise specified, messages must be delivered to the receiving application in the same +# order in which they were initiated by the sending application. The goal is to support "E-Order", +# which states that two calls made on the same reference must be delivered in the order which they +# were made: +# http://erights.org/elib/concurrency/partial-order.html +# +# Since the full protocol is complicated, we define multiple levels of support that an +# implementation may target. For many applications, level 1 support will be sufficient. +# Comments in this file indicate which level requires the corresponding feature to be +# implemented. +# +# * **Level 0:** The implementation does not support object references. Only the bootstrap interface +# can be called. At this level, the implementation does not support object-oriented protocols and +# is similar in complexity to JSON-RPC or Protobuf services. This level should be considered only +# a temporary stepping-stone toward level 1 as the lack of object references drastically changes +# how protocols are designed. Applications _should not_ attempt to design their protocols around +# the limitations of level 0 implementations. +# +# * **Level 1:** The implementation supports simple bilateral interaction with object references +# and promise pipelining, but interactions between three or more parties are supported only via +# proxying of objects. E.g. if Alice (in Vat A) wants to send Bob (in Vat B) a capability +# pointing to Carol (in Vat C), Alice must create a proxy of Carol within Vat A and send Bob a +# reference to that; Bob cannot form a direct connection to Carol. Level 1 implementations do +# not support checking if two capabilities received from different vats actually point to the +# same object ("join"), although they should be able to do this check on capabilities received +# from the same vat. +# +# * **Level 2:** The implementation supports saving persistent capabilities -- i.e. capabilities +# that remain valid even after disconnect, and can be restored on a future connection. When a +# capability is saved, the requester receives a `SturdyRef`, which is a token that can be used +# to restore the capability later. +# +# * **Level 3:** The implementation supports three-way interactions. That is, if Alice (in Vat A) +# sends Bob (in Vat B) a capability pointing to Carol (in Vat C), then Vat B will automatically +# form a direct connection to Vat C rather than have requests be proxied through Vat A. +# +# * **Level 4:** The entire protocol is implemented, including joins (checking if two capabilities +# are equivalent). +# +# Note that an implementation must also support specific networks (transports), as described in +# the "Network-specific Parameters" section below. An implementation might have different levels +# depending on the network used. +# +# New implementations of Cap'n Proto should start out targeting the simplistic two-party network +# type as defined in `rpc-twoparty.capnp`. With this network type, level 3 is irrelevant and +# levels 2 and 4 are much easier than usual to implement. When such an implementation is paired +# with a container proxy, the contained app effectively gets to make full use of the proxy's +# network at level 4. And since Cap'n Proto IPC is extremely fast, it may never make sense to +# bother implementing any other vat network protocol -- just use the correct container type and get +# it for free. + +using Cxx = import "/capnp/c++.capnp"; +$Cxx.namespace("capnp::rpc"); + +using Java = import "/capnp/java.capnp"; +$Java.package("org.capnproto"); +$Java.outerClassname("RpcProtocol"); + +# ======================================================================================== +# The Four Tables +# +# Cap'n Proto RPC connections are stateful (although an application built on Cap'n Proto could +# export a stateless interface). As in CapTP, for each open connection, a vat maintains four state +# tables: questions, answers, imports, and exports. See the diagram at: +# http://www.erights.org/elib/distrib/captp/4tables.html +# +# The question table corresponds to the other end's answer table, and the imports table corresponds +# to the other end's exports table. +# +# The entries in each table are identified by ID numbers (defined below as 32-bit integers). These +# numbers are always specific to the connection; a newly-established connection starts with no +# valid IDs. Since low-numbered IDs will pack better, it is suggested that IDs be assigned like +# Unix file descriptors -- prefer the lowest-number ID that is currently available. +# +# IDs in the questions/answers tables are chosen by the questioner and generally represent method +# calls that are in progress. +# +# IDs in the imports/exports tables are chosen by the exporter and generally represent objects on +# which methods may be called. Exports may be "settled", meaning the exported object is an actual +# object living in the exporter's vat, or they may be "promises", meaning the exported object is +# the as-yet-unknown result of an ongoing operation and will eventually be resolved to some other +# object once that operation completes. Calls made to a promise will be forwarded to the eventual +# target once it is known. The eventual replacement object does *not* get the same ID as the +# promise, as it may turn out to be an object that is already exported (so already has an ID) or +# may even live in a completely different vat (and so won't get an ID on the same export table +# at all). +# +# IDs can be reused over time. To make this safe, we carefully define the lifetime of IDs. Since +# messages using the ID could be traveling in both directions simultaneously, we must define the +# end of life of each ID _in each direction_. The ID is only safe to reuse once it has been +# released by both sides. +# +# When a Cap'n Proto connection is lost, everything on the four tables is lost. All questions are +# canceled and throw exceptions. All imports become broken (all future calls to them throw +# exceptions). All exports and answers are implicitly released. The only things not lost are +# persistent capabilities (`SturdyRef`s). The application must plan for this and should respond by +# establishing a new connection and restoring from these persistent capabilities. + +using QuestionId = UInt32; +# **(level 0)** +# +# Identifies a question in the sender's question table (which corresponds to the receiver's answer +# table). The questioner (caller) chooses an ID when making a call. The ID remains valid in +# caller -> callee messages until a Finish message is sent, and remains valid in callee -> caller +# messages until a Return message is sent. + +using AnswerId = QuestionId; +# **(level 0)** +# +# Identifies an answer in the sender's answer table (which corresponds to the receiver's question +# table). +# +# AnswerId is physically equivalent to QuestionId, since the question and answer tables correspond, +# but we define a separate type for documentation purposes: we always use the type representing +# the sender's point of view. + +using ExportId = UInt32; +# **(level 1)** +# +# Identifies an exported capability or promise in the sender's export table (which corresponds +# to the receiver's import table). The exporter chooses an ID before sending a capability over the +# wire. If the capability is already in the table, the exporter should reuse the same ID. If the +# ID is a promise (as opposed to a settled capability), this must be indicated at the time the ID +# is introduced (e.g. by using `senderPromise` instead of `senderHosted` in `CapDescriptor`); in +# this case, the importer shall expect a later `Resolve` message that replaces the promise. +# +# ExportId/ImportIds are subject to reference counting. Whenever an `ExportId` is sent over the +# wire (from the exporter to the importer), the export's reference count is incremented (unless +# otherwise specified). The reference count is later decremented by a `Release` message. Since +# the `Release` message can specify an arbitrary number by which to reduce the reference count, the +# importer should usually batch reference decrements and only send a `Release` when it believes the +# reference count has hit zero. Of course, it is possible that a new reference to the export is +# in-flight at the time that the `Release` message is sent, so it is necessary for the exporter to +# keep track of the reference count on its end as well to avoid race conditions. +# +# When a connection is lost, all exports are implicitly released. It is not possible to restore +# a connection state after disconnect (although a transport layer could implement a concept of +# persistent connections if it is transparent to the RPC layer). + +using ImportId = ExportId; +# **(level 1)** +# +# Identifies an imported capability or promise in the sender's import table (which corresponds to +# the receiver's export table). +# +# ImportId is physically equivalent to ExportId, since the export and import tables correspond, +# but we define a separate type for documentation purposes: we always use the type representing +# the sender's point of view. +# +# An `ImportId` remains valid in importer -> exporter messages until the importer has sent +# `Release` messages that (it believes) have reduced the reference count to zero. + +# ======================================================================================== +# Messages + +struct Message { + # An RPC connection is a bi-directional stream of Messages. + + union { + unimplemented @0 :Message; + # The sender previously received this message from the peer but didn't understand it or doesn't + # yet implement the functionality that was requested. So, the sender is echoing the message + # back. In some cases, the receiver may be able to recover from this by pretending the sender + # had taken some appropriate "null" action. + # + # For example, say `resolve` is received by a level 0 implementation (because a previous call + # or return happened to contain a promise). The level 0 implementation will echo it back as + # `unimplemented`. The original sender can then simply release the cap to which the promise + # had resolved, thus avoiding a leak. + # + # For any message type that introduces a question, if the message comes back unimplemented, + # the original sender may simply treat it as if the question failed with an exception. + # + # In cases where there is no sensible way to react to an `unimplemented` message (without + # resource leaks or other serious problems), the connection may need to be aborted. This is + # a gray area; different implementations may take different approaches. + + abort @1 :Exception; + # Sent when a connection is being aborted due to an unrecoverable error. This could be e.g. + # because the sender received an invalid or nonsensical message or because the sender had an + # internal error. The sender will shut down the outgoing half of the connection after `abort` + # and will completely close the connection shortly thereafter (it's up to the sender how much + # of a time buffer they want to offer for the client to receive the `abort` before the + # connection is reset). + + # Level 0 features ----------------------------------------------- + + bootstrap @8 :Bootstrap; # Request the peer's bootstrap interface. + call @2 :Call; # Begin a method call. + return @3 :Return; # Complete a method call. + finish @4 :Finish; # Release a returned answer / cancel a call. + + # Level 1 features ----------------------------------------------- + + resolve @5 :Resolve; # Resolve a previously-sent promise. + release @6 :Release; # Release a capability so that the remote object can be deallocated. + disembargo @13 :Disembargo; # Lift an embargo used to enforce E-order over promise resolution. + + # Level 2 features ----------------------------------------------- + + obsoleteSave @7 :AnyPointer; + # Obsolete request to save a capability, resulting in a SturdyRef. This has been replaced + # by the `Persistent` interface defined in `persistent.capnp`. This operation was never + # implemented. + + obsoleteDelete @9 :AnyPointer; + # Obsolete way to delete a SturdyRef. This operation was never implemented. + + # Level 3 features ----------------------------------------------- + + provide @10 :Provide; # Provide a capability to a third party. + accept @11 :Accept; # Accept a capability provided by a third party. + + # Level 4 features ----------------------------------------------- + + join @12 :Join; # Directly connect to the common root of two or more proxied caps. + } +} + +# Level 0 message types ---------------------------------------------- + +struct Bootstrap { + # **(level 0)** + # + # Get the "bootstrap" interface exported by the remote vat. + # + # For level 0, 1, and 2 implementations, the "bootstrap" interface is simply the main interface + # exported by a vat. If the vat acts as a server fielding connections from clients, then the + # bootstrap interface defines the basic functionality available to a client when it connects. + # The exact interface definition obviously depends on the application. + # + # We call this a "bootstrap" because in an ideal Cap'n Proto world, bootstrap interfaces would + # never be used. In such a world, any time you connect to a new vat, you do so because you + # received an introduction from some other vat (see `ThirdPartyCapId`). Thus, the first message + # you send is `Accept`, and further communications derive from there. `Bootstrap` is not used. + # + # In such an ideal world, DNS itself would support Cap'n Proto -- performing a DNS lookup would + # actually return a new Cap'n Proto capability, thus introducing you to the target system via + # level 3 RPC. Applications would receive the capability to talk to DNS in the first place as + # an initial endowment or part of a Powerbox interaction. Therefore, an app can form arbitrary + # connections without ever using `Bootstrap`. + # + # Of course, in the real world, DNS is not Cap'n-Proto-based, and we don't want Cap'n Proto to + # require a whole new internet infrastructure to be useful. Therefore, we offer bootstrap + # interfaces as a way to get up and running without a level 3 introduction. Thus, bootstrap + # interfaces are used to "bootstrap" from other, non-Cap'n-Proto-based means of service discovery, + # such as legacy DNS. + # + # Note that a vat need not provide a bootstrap interface, and in fact many vats (especially those + # acting as clients) do not. In this case, the vat should either reply to `Bootstrap` with a + # `Return` indicating an exception, or should return a dummy capability with no methods. + + questionId @0 :QuestionId; + # A new question ID identifying this request, which will eventually receive a Return message + # containing the restored capability. + + deprecatedObjectId @1 :AnyPointer; + # ** DEPRECATED ** + # + # A Vat may export multiple bootstrap interfaces. In this case, `deprecatedObjectId` specifies + # which one to return. If this pointer is null, then the default bootstrap interface is returned. + # + # As of verison 0.5, use of this field is deprecated. If a service wants to export multiple + # bootstrap interfaces, it should instead define a single bootstrap interface that has methods + # that return each of the other interfaces. + # + # **History** + # + # In the first version of Cap'n Proto RPC (0.4.x) the `Bootstrap` message was called `Restore`. + # At the time, it was thought that this would eventually serve as the way to restore SturdyRefs + # (level 2). Meanwhile, an application could offer its "main" interface on a well-known + # (non-secret) SturdyRef. + # + # Since level 2 RPC was not implemented at the time, the `Restore` message was in practice only + # used to obtain the main interface. Since most applications had only one main interface that + # they wanted to restore, they tended to designate this with a null `objectId`. + # + # Unfortunately, the earliest version of the EZ RPC interfaces set a precedent of exporting + # multiple main interfaces by allowing them to be exported under string names. In this case, + # `objectId` was a Text value specifying the name. + # + # All of this proved problematic for several reasons: + # + # - The arrangement assumed that a client wishing to restore a SturdyRef would know exactly what + # machine to connect to and would be able to immediately restore a SturdyRef on connection. + # However, in practice, the ability to restore SturdyRefs is itself a capability that may + # require going through an authentication process to obtain. Thus, it makes more sense to + # define a "restorer service" as a full Cap'n Proto interface. If this restorer interface is + # offered as the vat's bootstrap interface, then this is equivalent to the old arrangement. + # + # - Overloading "Restore" for the purpose of obtaining well-known capabilities encouraged the + # practice of exporting singleton services with string names. If singleton services are desired, + # it is better to have one main interface that has methods that can be used to obtain each + # service, in order to get all the usual benefits of schemas and type checking. + # + # - Overloading "Restore" also had a security problem: Often, "main" or "well-known" + # capabilities exported by a vat are in fact not public: they are intended to be accessed only + # by clients who are capable of forming a connection to the vat. This can lead to trouble if + # the client itself has other clients and wishes to foward some `Restore` requests from those + # external clients -- it has to be very careful not to allow through `Restore` requests + # addressing the default capability. + # + # For example, consider the case of a sandboxed Sandstorm application and its supervisor. The + # application exports a default capability to its supervisor that provides access to + # functionality that only the supervisor is supposed to access. Meanwhile, though, applications + # may publish other capabilities that may be persistent, in which case the application needs + # to field `Restore` requests that could come from anywhere. These requests of course have to + # pass through the supervisor, as all communications with the outside world must. But, the + # supervisor has to be careful not to honor an external request addressing the application's + # default capability, since this capability is privileged. Unfortunately, the default + # capability cannot be given an unguessable name, because then the supervisor itself would not + # be able to address it! + # + # As of Cap'n Proto 0.5, `Restore` has been renamed to `Bootstrap` and is no longer planned for + # use in restoring SturdyRefs. + # + # Note that 0.4 also defined a message type called `Delete` that, like `Restore`, addressed a + # SturdyRef, but indicated that the client would not restore the ref again in the future. This + # operation was never implemented, so it was removed entirely. If a "delete" operation is desired, + # it should exist as a method on the same interface that handles restoring SturdyRefs. However, + # the utility of such an operation is questionable. You wouldn't be able to rely on it for + # garbage collection since a client could always disappear permanently without remembering to + # delete all its SturdyRefs, thus leaving them dangling forever. Therefore, it is advisable to + # design systems such that SturdyRefs never represent "owned" pointers. + # + # For example, say a SturdyRef points to an image file hosted on some server. That image file + # should also live inside a collection (a gallery, perhaps) hosted on the same server, owned by + # a user who can delete the image at any time. If the user deletes the image, the SturdyRef + # stops working. On the other hand, if the SturdyRef is discarded, this has no effect on the + # existence of the image in its collection. +} + +struct Call { + # **(level 0)** + # + # Message type initiating a method call on a capability. + + questionId @0 :QuestionId; + # A number, chosen by the caller, that identifies this call in future messages. This number + # must be different from all other calls originating from the same end of the connection (but + # may overlap with question IDs originating from the opposite end). A fine strategy is to use + # sequential question IDs, but the recipient should not assume this. + # + # A question ID can be reused once both: + # - A matching Return has been received from the callee. + # - A matching Finish has been sent from the caller. + + target @1 :MessageTarget; + # The object that should receive this call. + + interfaceId @2 :UInt64; + # The type ID of the interface being called. Each capability may implement multiple interfaces. + + methodId @3 :UInt16; + # The ordinal number of the method to call within the requested interface. + + allowThirdPartyTailCall @8 :Bool = false; + # Indicates whether or not the receiver is allowed to send a `Return` containing + # `acceptFromThirdParty`. Level 3 implementations should set this true. Otherwise, the callee + # will have to proxy the return in the case of a tail call to a third-party vat. + + params @4 :Payload; + # The call parameters. `params.content` is a struct whose fields correspond to the parameters of + # the method. + + sendResultsTo :union { + # Where should the return message be sent? + + caller @5 :Void; + # Send the return message back to the caller (the usual). + + yourself @6 :Void; + # **(level 1)** + # + # Don't actually return the results to the sender. Instead, hold on to them and await + # instructions from the sender regarding what to do with them. In particular, the sender + # may subsequently send a `Return` for some other call (which the receiver had previously made + # to the sender) with `takeFromOtherQuestion` set. The results from this call are then used + # as the results of the other call. + # + # When `yourself` is used, the receiver must still send a `Return` for the call, but sets the + # field `resultsSentElsewhere` in that `Return` rather than including the results. + # + # This feature can be used to implement tail calls in which a call from Vat A to Vat B ends up + # returning the result of a call from Vat B back to Vat A. + # + # In particular, the most common use case for this feature is when Vat A makes a call to a + # promise in Vat B, and then that promise ends up resolving to a capability back in Vat A. + # Vat B must forward all the queued calls on that promise back to Vat A, but can set `yourself` + # in the calls so that the results need not pass back through Vat B. + # + # For example: + # - Alice, in Vat A, calls foo() on Bob in Vat B. + # - Alice makes a pipelined call bar() on the promise returned by foo(). + # - Later on, Bob resolves the promise from foo() to point at Carol, who lives in Vat A (next + # to Alice). + # - Vat B dutifully forwards the bar() call to Carol. Let us call this forwarded call bar'(). + # Notice that bar() and bar'() are travelling in opposite directions on the same network + # link. + # - The `Call` for bar'() has `sendResultsTo` set to `yourself`. + # - Vat B sends a `Return` for bar() with `takeFromOtherQuestion` set in place of the results, + # with the value set to the question ID of bar'(). Vat B does not wait for bar'() to return, + # as doing so would introduce unnecessary round trip latency. + # - Vat A receives bar'() and delivers it to Carol. + # - When bar'() returns, Vat A sends a `Return` for bar'() to Vat B, with `resultsSentElsewhere` + # set in place of results. + # - Vat A sends a `Finish` for the bar() call to Vat B. + # - Vat B receives the `Finish` for bar() and sends a `Finish` for bar'(). + + thirdParty @7 :RecipientId; + # **(level 3)** + # + # The call's result should be returned to a different vat. The receiver (the callee) expects + # to receive an `Accept` message from the indicated vat, and should return the call's result + # to it, rather than to the sender of the `Call`. + # + # This operates much like `yourself`, above, except that Carol is in a separate Vat C. `Call` + # messages are sent from Vat A -> Vat B and Vat B -> Vat C. A `Return` message is sent from + # Vat B -> Vat A that contains `acceptFromThirdParty` in place of results. When Vat A sends + # an `Accept` to Vat C, it receives back a `Return` containing the call's actual result. Vat C + # also sends a `Return` to Vat B with `resultsSentElsewhere`. + } +} + +struct Return { + # **(level 0)** + # + # Message type sent from callee to caller indicating that the call has completed. + + answerId @0 :AnswerId; + # Equal to the QuestionId of the corresponding `Call` message. + + releaseParamCaps @1 :Bool = true; + # If true, all capabilities that were in the params should be considered released. The sender + # must not send separate `Release` messages for them. Level 0 implementations in particular + # should always set this true. This defaults true because if level 0 implementations forget to + # set it they'll never notice (just silently leak caps), but if level >=1 implementations forget + # to set it to false they'll quickly get errors. + # + # The receiver should act as if the sender had sent a release message with count=1 for each + # CapDescriptor in the original Call message. + + union { + results @2 :Payload; + # The result. + # + # For regular method calls, `results.content` points to the result struct. + # + # For a `Return` in response to an `Accept` or `Bootstrap`, `results` contains a single + # capability (rather than a struct), and `results.content` is just a capability pointer with + # index 0. A `Finish` is still required in this case. + + exception @3 :Exception; + # Indicates that the call failed and explains why. + + canceled @4 :Void; + # Indicates that the call was canceled due to the caller sending a Finish message + # before the call had completed. + + resultsSentElsewhere @5 :Void; + # This is set when returning from a `Call` that had `sendResultsTo` set to something other + # than `caller`. + # + # It doesn't matter too much when this is sent, as the receiver doesn't need to do anything + # with it, but the C++ implementation appears to wait for the call to finish before sending + # this. + + takeFromOtherQuestion @6 :QuestionId; + # The sender has also sent (before this message) a `Call` with the given question ID and with + # `sendResultsTo.yourself` set, and the results of that other call should be used as the + # results here. `takeFromOtherQuestion` can only used once per question. + + acceptFromThirdParty @7 :ThirdPartyCapId; + # **(level 3)** + # + # The caller should contact a third-party vat to pick up the results. An `Accept` message + # sent to the vat will return the result. This pairs with `Call.sendResultsTo.thirdParty`. + # It should only be used if the corresponding `Call` had `allowThirdPartyTailCall` set. + } +} + +struct Finish { + # **(level 0)** + # + # Message type sent from the caller to the callee to indicate: + # 1) The questionId will no longer be used in any messages sent by the callee (no further + # pipelined requests). + # 2) If the call has not returned yet, the caller no longer cares about the result. If nothing + # else cares about the result either (e.g. there are no other outstanding calls pipelined on + # the result of this one) then the callee may wish to immediately cancel the operation and + # send back a Return message with "canceled" set. However, implementations are not required + # to support premature cancellation -- instead, the implementation may wait until the call + # actually completes and send a normal `Return` message. + # + # TODO(someday): Should we separate (1) and implicitly releasing result capabilities? It would be + # possible and useful to notify the server that it doesn't need to keep around the response to + # service pipeline requests even though the caller still wants to receive it / hasn't yet + # finished processing it. It could also be useful to notify the server that it need not marshal + # the results because the caller doesn't want them anyway, even if the caller is still sending + # pipelined calls, although this seems less useful (just saving some bytes on the wire). + + questionId @0 :QuestionId; + # ID of the call whose result is to be released. + + releaseResultCaps @1 :Bool = true; + # If true, all capabilities that were in the results should be considered released. The sender + # must not send separate `Release` messages for them. Level 0 implementations in particular + # should always set this true. This defaults true because if level 0 implementations forget to + # set it they'll never notice (just silently leak caps), but if level >=1 implementations forget + # set it false they'll quickly get errors. +} + +# Level 1 message types ---------------------------------------------- + +struct Resolve { + # **(level 1)** + # + # Message type sent to indicate that a previously-sent promise has now been resolved to some other + # object (possibly another promise) -- or broken, or canceled. + # + # Keep in mind that it's possible for a `Resolve` to be sent to a level 0 implementation that + # doesn't implement it. For example, a method call or return might contain a capability in the + # payload. Normally this is fine even if the receiver is level 0, because they will implicitly + # release all such capabilities on return / finish. But if the cap happens to be a promise, then + # a follow-up `Resolve` may be sent regardless of this release. The level 0 receiver will reply + # with an `unimplemented` message, and the sender (of the `Resolve`) can respond to this as if the + # receiver had immediately released any capability to which the promise resolved. + # + # When implementing promise resolution, it's important to understand how embargos work and the + # tricky case of the Tribble 4-way race condition. See the comments for the Disembargo message, + # below. + + promiseId @0 :ExportId; + # The ID of the promise to be resolved. + # + # Unlike all other instances of `ExportId` sent from the exporter, the `Resolve` message does + # _not_ increase the reference count of `promiseId`. In fact, it is expected that the receiver + # will release the export soon after receiving `Resolve`, and the sender will not send this + # `ExportId` again until it has been released and recycled. + # + # When an export ID sent over the wire (e.g. in a `CapDescriptor`) is indicated to be a promise, + # this indicates that the sender will follow up at some point with a `Resolve` message. If the + # same `promiseId` is sent again before `Resolve`, still only one `Resolve` is sent. If the + # same ID is sent again later _after_ a `Resolve`, it can only be because the export's + # reference count hit zero in the meantime and the ID was re-assigned to a new export, therefore + # this later promise does _not_ correspond to the earlier `Resolve`. + # + # If a promise ID's reference count reaches zero before a `Resolve` is sent, the `Resolve` + # message may or may not still be sent (the `Resolve` may have already been in-flight when + # `Release` was sent, but if the `Release` is received before `Resolve` then there is no longer + # any reason to send a `Resolve`). Thus a `Resolve` may be received for a promise of which + # the receiver has no knowledge, because it already released it earlier. In this case, the + # receiver should simply release the capability to which the promise resolved. + + union { + cap @1 :CapDescriptor; + # The object to which the promise resolved. + # + # The sender promises that from this point forth, until `promiseId` is released, it shall + # simply forward all messages to the capability designated by `cap`. This is true even if + # `cap` itself happens to designate another promise, and that other promise later resolves -- + # messages sent to `promiseId` shall still go to that other promise, not to its resolution. + # This is important in the case that the receiver of the `Resolve` ends up sending a + # `Disembargo` message towards `promiseId` in order to control message ordering -- that + # `Disembargo` really needs to reflect back to exactly the object designated by `cap` even + # if that object is itself a promise. + + exception @2 :Exception; + # Indicates that the promise was broken. + } +} + +struct Release { + # **(level 1)** + # + # Message type sent to indicate that the sender is done with the given capability and the receiver + # can free resources allocated to it. + + id @0 :ImportId; + # What to release. + + referenceCount @1 :UInt32; + # The amount by which to decrement the reference count. The export is only actually released + # when the reference count reaches zero. +} + +struct Disembargo { + # **(level 1)** + # + # Message sent to indicate that an embargo on a recently-resolved promise may now be lifted. + # + # Embargos are used to enforce E-order in the presence of promise resolution. That is, if an + # application makes two calls foo() and bar() on the same capability reference, in that order, + # the calls should be delivered in the order in which they were made. But if foo() is called + # on a promise, and that promise happens to resolve before bar() is called, then the two calls + # may travel different paths over the network, and thus could arrive in the wrong order. In + # this case, the call to `bar()` must be embargoed, and a `Disembargo` message must be sent along + # the same path as `foo()` to ensure that the `Disembargo` arrives after `foo()`. Once the + # `Disembargo` arrives, `bar()` can then be delivered. + # + # There are two particular cases where embargos are important. Consider object Alice, in Vat A, + # who holds a promise P, pointing towards Vat B, that eventually resolves to Carol. The two + # cases are: + # - Carol lives in Vat A, i.e. next to Alice. In this case, Vat A needs to send a `Disembargo` + # message that echos through Vat B and back, to ensure that all pipelined calls on the promise + # have been delivered. + # - Carol lives in a different Vat C. When the promise resolves, a three-party handoff occurs + # (see `Provide` and `Accept`, which constitute level 3 of the protocol). In this case, we + # piggyback on the state that has already been set up to handle the handoff: the `Accept` + # message (from Vat A to Vat C) is embargoed, as are all pipelined messages sent to it, while + # a `Disembargo` message is sent from Vat A through Vat B to Vat C. See `Accept.embargo` for + # an example. + # + # Note that in the case where Carol actually lives in Vat B (i.e., the same vat that the promise + # already pointed at), no embargo is needed, because the pipelined calls are delivered over the + # same path as the later direct calls. + # + # Keep in mind that promise resolution happens both in the form of Resolve messages as well as + # Return messages (which resolve PromisedAnswers). Embargos apply in both cases. + # + # An alternative strategy for enforcing E-order over promise resolution could be for Vat A to + # implement the embargo internally. When Vat A is notified of promise resolution, it could + # send a dummy no-op call to promise P and wait for it to complete. Until that call completes, + # all calls to the capability are queued locally. This strategy works, but is pessimistic: + # in the three-party case, it requires an A -> B -> C -> B -> A round trip before calls can start + # being delivered directly to from Vat A to Vat C. The `Disembargo` message allows latency to be + # reduced. (In the two-party loopback case, the `Disembargo` message is just a more explicit way + # of accomplishing the same thing as a no-op call, but isn't any faster.) + # + # *The Tribble 4-way Race Condition* + # + # Any implementation of promise resolution and embargos must be aware of what we call the + # "Tribble 4-way race condition", after Dean Tribble, who explained the problem in a lively + # Friam meeting. + # + # Embargos are designed to work in the case where a two-hop path is being shortened to one hop. + # But sometimes there are more hops. Imagine that Alice has a reference to a remote promise P1 + # that eventually resolves to _another_ remote promise P2 (in a third vat), which _at the same + # time_ happens to resolve to Bob (in a fourth vat). In this case, we're shortening from a 3-hop + # path (with four parties) to a 1-hop path (Alice -> Bob). + # + # Extending the embargo/disembargo protocol to be able to shorted multiple hops at once seems + # difficult. Instead, we make a rule that prevents this case from coming up: + # + # One a promise P has been resolved to a remote object reference R, then all further messages + # received addressed to P will be forwarded strictly to R. Even if it turns out later that R is + # itself a promise, and has resolved to some other object Q, messages sent to P will still be + # forwarded to R, not directly to Q (R will of course further forward the messages to Q). + # + # This rule does not cause a significant performance burden because once P has resolved to R, it + # is expected that people sending messages to P will shortly start sending them to R instead and + # drop P. P is at end-of-life anyway, so it doesn't matter if it ignores chances to further + # optimize its path. + + target @0 :MessageTarget; + # What is to be disembargoed. + + using EmbargoId = UInt32; + # Used in `senderLoopback` and `receiverLoopback`, below. + + context :union { + senderLoopback @1 :EmbargoId; + # The sender is requesting a disembargo on a promise that is known to resolve back to a + # capability hosted by the sender. As soon as the receiver has echoed back all pipelined calls + # on this promise, it will deliver the Disembargo back to the sender with `receiverLoopback` + # set to the same value as `senderLoopback`. This value is chosen by the sender, and since + # it is also consumed be the sender, the sender can use whatever strategy it wants to make sure + # the value is unambiguous. + # + # The receiver must verify that the target capability actually resolves back to the sender's + # vat. Otherwise, the sender has committed a protocol error and should be disconnected. + + receiverLoopback @2 :EmbargoId; + # The receiver previously sent a `senderLoopback` Disembargo towards a promise resolving to + # this capability, and that Disembargo is now being echoed back. + + accept @3 :Void; + # **(level 3)** + # + # The sender is requesting a disembargo on a promise that is known to resolve to a third-party + # capability that the sender is currently in the process of accepting (using `Accept`). + # The receiver of this `Disembargo` has an outstanding `Provide` on said capability. The + # receiver should now send a `Disembargo` with `provide` set to the question ID of that + # `Provide` message. + # + # See `Accept.embargo` for an example. + + provide @4 :QuestionId; + # **(level 3)** + # + # The sender is requesting a disembargo on a capability currently being provided to a third + # party. The question ID identifies the `Provide` message previously sent by the sender to + # this capability. On receipt, the receiver (the capability host) shall release the embargo + # on the `Accept` message that it has received from the third party. See `Accept.embargo` for + # an example. + } +} + +# Level 2 message types ---------------------------------------------- + +# See persistent.capnp. + +# Level 3 message types ---------------------------------------------- + +struct Provide { + # **(level 3)** + # + # Message type sent to indicate that the sender wishes to make a particular capability implemented + # by the receiver available to a third party for direct access (without the need for the third + # party to proxy through the sender). + # + # (In CapTP, `Provide` and `Accept` are methods of the global `NonceLocator` object exported by + # every vat. In Cap'n Proto, we bake this into the core protocol.) + + questionId @0 :QuestionId; + # Question ID to be held open until the recipient has received the capability. A result will be + # returned once the third party has successfully received the capability. The sender must at some + # point send a `Finish` message as with any other call, and that message can be used to cancel the + # whole operation. + + target @1 :MessageTarget; + # What is to be provided to the third party. + + recipient @2 :RecipientId; + # Identity of the third party that is expected to pick up the capability. +} + +struct Accept { + # **(level 3)** + # + # Message type sent to pick up a capability hosted by the receiving vat and provided by a third + # party. The third party previously designated the capability using `Provide`. + # + # This message is also used to pick up a redirected return -- see `Return.acceptFromThirdParty`. + + questionId @0 :QuestionId; + # A new question ID identifying this accept message, which will eventually receive a Return + # message containing the provided capability (or the call result in the case of a redirected + # return). + + provision @1 :ProvisionId; + # Identifies the provided object to be picked up. + + embargo @2 :Bool; + # If true, this accept shall be temporarily embargoed. The resulting `Return` will not be sent, + # and any pipelined calls will not be delivered, until the embargo is released. The receiver + # (the capability host) will expect the provider (the vat that sent the `Provide` message) to + # eventually send a `Disembargo` message with the field `context.provide` set to the question ID + # of the original `Provide` message. At that point, the embargo is released and the queued + # messages are delivered. + # + # For example: + # - Alice, in Vat A, holds a promise P, which currently points toward Vat B. + # - Alice calls foo() on P. The `Call` message is sent to Vat B. + # - The promise P in Vat B ends up resolving to Carol, in Vat C. + # - Vat B sends a `Provide` message to Vat C, identifying Vat A as the recipient. + # - Vat B sends a `Resolve` message to Vat A, indicating that the promise has resolved to a + # `ThirdPartyCapId` identifying Carol in Vat C. + # - Vat A sends an `Accept` message to Vat C to pick up the capability. Since Vat A knows that + # it has an outstanding call to the promise, it sets `embargo` to `true` in the `Accept` + # message. + # - Vat A sends a `Disembargo` message to Vat B on promise P, with `context.accept` set. + # - Alice makes a call bar() to promise P, which is now pointing towards Vat C. Alice doesn't + # know anything about the mechanics of promise resolution happening under the hood, but she + # expects that bar() will be delivered after foo() because that is the order in which she + # initiated the calls. + # - Vat A sends the bar() call to Vat C, as a pipelined call on the result of the `Accept` (which + # hasn't returned yet, due to the embargo). Since calls to the newly-accepted capability + # are embargoed, Vat C does not deliver the call yet. + # - At some point, Vat B forwards the foo() call from the beginning of this example on to Vat C. + # - Vat B forwards the `Disembargo` from Vat A on to vat C. It sets `context.provide` to the + # question ID of the `Provide` message it had sent previously. + # - Vat C receives foo() before `Disembargo`, thus allowing it to correctly deliver foo() + # before delivering bar(). + # - Vat C receives `Disembargo` from Vat B. It can now send a `Return` for the `Accept` from + # Vat A, as well as deliver bar(). +} + +# Level 4 message types ---------------------------------------------- + +struct Join { + # **(level 4)** + # + # Message type sent to implement E.join(), which, given a number of capabilities that are + # expected to be equivalent, finds the underlying object upon which they all agree and forms a + # direct connection to it, skipping any proxies that may have been constructed by other vats + # while transmitting the capability. See: + # http://erights.org/elib/equality/index.html + # + # Note that this should only serve to bypass fully-transparent proxies -- proxies that were + # created merely for convenience, without any intention of hiding the underlying object. + # + # For example, say Bob holds two capabilities hosted by Alice and Carol, but he expects that both + # are simply proxies for a capability hosted elsewhere. He then issues a join request, which + # operates as follows: + # - Bob issues Join requests on both Alice and Carol. Each request contains a different piece + # of the JoinKey. + # - Alice is proxying a capability hosted by Dana, so forwards the request to Dana's cap. + # - Dana receives the first request and sees that the JoinKeyPart is one of two. She notes that + # she doesn't have the other part yet, so she records the request and responds with a + # JoinResult. + # - Alice relays the JoinAnswer back to Bob. + # - Carol is also proxying a capability from Dana, and so forwards her Join request to Dana as + # well. + # - Dana receives Carol's request and notes that she now has both parts of a JoinKey. She + # combines them in order to form information needed to form a secure connection to Bob. She + # also responds with another JoinResult. + # - Bob receives the responses from Alice and Carol. He uses the returned JoinResults to + # determine how to connect to Dana and attempts to form the connection. Since Bob and Dana now + # agree on a secret key that neither Alice nor Carol ever saw, this connection can be made + # securely even if Alice or Carol is conspiring against the other. (If Alice and Carol are + # conspiring _together_, they can obviously reproduce the key, but this doesn't matter because + # the whole point of the join is to verify that Alice and Carol agree on what capability they + # are proxying.) + # + # If the two capabilities aren't actually proxies of the same object, then the join requests + # will come back with conflicting `hostId`s and the join will fail before attempting to form any + # connection. + + questionId @0 :QuestionId; + # Question ID used to respond to this Join. (Note that this ID only identifies one part of the + # request for one hop; each part has a different ID and relayed copies of the request have + # (probably) different IDs still.) + # + # The receiver will reply with a `Return` whose `results` is a JoinResult. This `JoinResult` + # is relayed from the joined object's host, possibly with transformation applied as needed + # by the network. + # + # Like any return, the result must be released using a `Finish`. However, this release + # should not occur until the joiner has either successfully connected to the joined object. + # Vats relaying a `Join` message similarly must not release the result they receive until the + # return they relayed back towards the joiner has itself been released. This allows the + # joined object's host to detect when the Join operation is canceled before completing -- if + # it receives a `Finish` for one of the join results before the joiner successfully + # connects. It can then free any resources it had allocated as part of the join. + + target @1 :MessageTarget; + # The capability to join. + + keyPart @2 :JoinKeyPart; + # A part of the join key. These combine to form the complete join key, which is used to establish + # a direct connection. + + # TODO(before implementing): Change this so that multiple parts can be sent in a single Join + # message, so that if multiple join parts are going to cross the same connection they can be sent + # together, so that the receive can potentially optimize its handling of them. In the case where + # all parts are bundled together, should the recipient be expected to simply return a cap, so + # that the caller can immediately start pipelining to it? +} + +# ======================================================================================== +# Common structures used in messages + +struct MessageTarget { + # The target of a `Call` or other messages that target a capability. + + union { + importedCap @0 :ImportId; + # This message is to a capability or promise previously imported by the caller (exported by + # the receiver). + + promisedAnswer @1 :PromisedAnswer; + # This message is to a capability that is expected to be returned by another call that has not + # yet been completed. + # + # At level 0, this is supported only for addressing the result of a previous `Bootstrap`, so + # that initial startup doesn't require a round trip. + } +} + +struct Payload { + # Represents some data structure that might contain capabilities. + + content @0 :AnyPointer; + # Some Cap'n Proto data structure. Capability pointers embedded in this structure index into + # `capTable`. + + capTable @1 :List(CapDescriptor); + # Descriptors corresponding to the cap pointers in `content`. +} + +struct CapDescriptor { + # **(level 1)** + # + # When an application-defined type contains an interface pointer, that pointer contains an index + # into the message's capability table -- i.e. the `capTable` part of the `Payload`. Each + # capability in the table is represented as a `CapDescriptor`. The runtime API should not reveal + # the CapDescriptor directly to the application, but should instead wrap it in some kind of + # callable object with methods corresponding to the interface that the capability implements. + # + # Keep in mind that `ExportIds` in a `CapDescriptor` are subject to reference counting. See the + # description of `ExportId`. + # + # Note that it is currently not possible to include a broken capability in the CapDescriptor + # table. Instead, create a new export (`senderPromise`) for each broken capability and then + # immediately follow the payload-bearing Call or Return message with one Resolve message for each + # broken capability, resolving it to an exception. + + union { + none @0 :Void; + # There is no capability here. This `CapDescriptor` should not appear in the payload content. + # A `none` CapDescriptor can be generated when an application inserts a capability into a + # message and then later changes its mind and removes it -- rewriting all of the other + # capability pointers may be hard, so instead a tombstone is left, similar to the way a removed + # struct or list instance is zeroed out of the message but the space is not reclaimed. + # Hopefully this is unusual. + + senderHosted @1 :ExportId; + # The ID of a capability in the sender's export table (receiver's import table). It may be a + # newly allocated table entry, or an existing entry (increments the reference count). + + senderPromise @2 :ExportId; + # A promise that the sender will resolve later. The sender will send exactly one Resolve + # message at a future point in time to replace this promise. Note that even if the same + # `senderPromise` is received multiple times, only one `Resolve` is sent to cover all of + # them. If `senderPromise` is released before the `Resolve` is sent, the sender (of this + # `CapDescriptor`) may choose not to send the `Resolve` at all. + + receiverHosted @3 :ImportId; + # A capability (or promise) previously exported by the receiver (imported by the sender). + + receiverAnswer @4 :PromisedAnswer; + # A capability expected to be returned in the results of a currently-outstanding call posed + # by the sender. + + thirdPartyHosted @5 :ThirdPartyCapDescriptor; + # **(level 3)** + # + # A capability that lives in neither the sender's nor the receiver's vat. The sender needs + # to form a direct connection to a third party to pick up the capability. + # + # Level 1 and 2 implementations that receive a `thirdPartyHosted` may simply send calls to its + # `vine` instead. + } + + attachedFd @6 :UInt8 = 0xff; + # If the RPC message in which this CapDescriptor was delivered also had file descriptors + # attached, and `fd` is a valid index into the list of attached file descriptors, then + # that file descriptor should be attached to this capability. If `attachedFd` is out-of-bounds + # for said list, then no FD is attached. + # + # For example, if the RPC message arrived over a Unix socket, then file descriptors may be + # attached by sending an SCM_RIGHTS ancillary message attached to the data bytes making up the + # raw message. Receivers who wish to opt into FD passing should arrange to receive SCM_RIGHTS + # whenever receiving an RPC message. Senders who wish to send FDs need not verify whether the + # receiver knows how to receive them, because the operating system will automatically discard + # ancillary messages like SCM_RIGHTS if the receiver doesn't ask to receive them, including + # automatically closing any FDs. + # + # It is up to the application protocol to define what capabilities are expected to have file + # descriptors attached, and what those FDs mean. But, for example, an application could use this + # to open a file on disk and then transmit the open file descriptor to a sandboxed process that + # does not otherwise have permission to access the filesystem directly. This is usually an + # optimization: the sending process could instead provide an RPC interface supporting all the + # operations needed (such as reading and writing a file), but by passing the file descriptor + # directly, the recipient can often perform operations much more efficiently. Application + # designers are encouraged to provide such RPC interfaces and automatically fall back to them + # when FD passing is not available, so that the application can still work when the parties are + # remote over a network. + # + # An attached FD is most often associated with a `senderHosted` descriptor. It could also make + # sense in the case of `thirdPartyHosted`: in this case, the sender is forwarding the FD that + # they received from the third party, so that the receiver can start using it without first + # interacting with the third party. This is an optional optimization -- the middleman may choose + # not to forward capabilities, in which case the receiver will need to complete the handshake + # with the third party directly before receiving the FD. If an implementation receives a second + # attached FD after having already received one previously (e.g. both in a `thirdPartyHosted` + # CapDescriptor and then later again when receiving the final capability directly from the + # third party), the implementation should discard the later FD and stick with the original. At + # present, there is no known reason why other capability types (e.g. `receiverHosted`) would want + # to carry an attached FD, but we reserve the right to define a meaning for this in the future. + # + # Each file descriptor attached to the message must be used in no more than one CapDescriptor, + # so that the receiver does not need to use dup() or refcounting to handle the possibility of + # multiple capabilities using the same descriptor. If multiple CapDescriptors do point to the + # same FD index, then the receiver can arbitrarily choose which capability ends up having the + # FD attached. + # + # To mitigate DoS attacks, RPC implementations should limit the number of FDs they are willing to + # receive in a single message to a small value. If a message happens to contain more than that, + # the list is truncated. Moreover, in some cases, FD passing needs to be blocked entirely for + # security or implementation reasons, in which case the list may be truncated to zero. Hence, + # `attachedFd` might point past the end of the list, which the implementation should treat as if + # no FD was attached at all. + # + # The type of this field was chosen to be UInt8 because Linux supports sending only a maximum + # of 253 file descriptors in an SCM_RIGHTS message anyway, and CapDescriptor had two bytes of + # padding left -- so after adding this, there is still one byte for a future feature. + # Conveniently, this also means we're able to use 0xff as the default value, which will always + # be out-of-range (of course, the implementation should explicitly enforce that 255 descriptors + # cannot be sent at once, rather than relying on Linux to do so). +} + +struct PromisedAnswer { + # **(mostly level 1)** + # + # Specifies how to derive a promise from an unanswered question, by specifying the path of fields + # to follow from the root of the eventual result struct to get to the desired capability. Used + # to address method calls to a not-yet-returned capability or to pass such a capability as an + # input to some other method call. + # + # Level 0 implementations must support `PromisedAnswer` only for the case where the answer is + # to a `Bootstrap` message. In this case, `path` is always empty since `Bootstrap` always returns + # a raw capability. + + questionId @0 :QuestionId; + # ID of the question (in the sender's question table / receiver's answer table) whose answer is + # expected to contain the capability. + + transform @1 :List(Op); + # Operations / transformations to apply to the result in order to get the capability actually + # being addressed. E.g. if the result is a struct and you want to call a method on a capability + # pointed to by a field of the struct, you need a `getPointerField` op. + + struct Op { + union { + noop @0 :Void; + # Does nothing. This member is mostly defined so that we can make `Op` a union even + # though (as of this writing) only one real operation is defined. + + getPointerField @1 :UInt16; + # Get a pointer field within a struct. The number is an index into the pointer section, NOT + # a field ordinal, so that the receiver does not need to understand the schema. + + # TODO(someday): We could add: + # - For lists, the ability to address every member of the list, or a slice of the list, the + # result of which would be another list. This is useful for implementing the equivalent of + # a SQL table join (not to be confused with the `Join` message type). + # - Maybe some ability to test a union. + # - Probably not a good idea: the ability to specify an arbitrary script to run on the + # result. We could define a little stack-based language where `Op` specifies one + # "instruction" or transformation to apply. Although this is not a good idea + # (over-engineered), any narrower additions to `Op` should be designed as if this + # were the eventual goal. + } + } +} + +struct ThirdPartyCapDescriptor { + # **(level 3)** + # + # Identifies a capability in a third-party vat that the sender wants the receiver to pick up. + + id @0 :ThirdPartyCapId; + # Identifies the third-party host and the specific capability to accept from it. + + vineId @1 :ExportId; + # A proxy for the third-party object exported by the sender. In CapTP terminology this is called + # a "vine", because it is an indirect reference to the third-party object that snakes through the + # sender vat. This serves two purposes: + # + # * Level 1 and 2 implementations that don't understand how to connect to a third party may + # simply send calls to the vine. Such calls will be forwarded to the third-party by the + # sender. + # + # * Level 3 implementations must release the vine only once they have successfully picked up the + # object from the third party. This ensures that the capability is not released by the sender + # prematurely. + # + # The sender will close the `Provide` request that it has sent to the third party as soon as + # it receives either a `Call` or a `Release` message directed at the vine. +} + +struct Exception { + # **(level 0)** + # + # Describes an arbitrary error that prevented an operation (e.g. a call) from completing. + # + # Cap'n Proto exceptions always indicate that something went wrong. In other words, in a fantasy + # world where everything always works as expected, no exceptions would ever be thrown. Clients + # should only ever catch exceptions as a means to implement fault-tolerance, where "fault" can + # mean: + # - Bugs. + # - Invalid input. + # - Configuration errors. + # - Network problems. + # - Insufficient resources. + # - Version skew (unimplemented functionality). + # - Other logistical problems. + # + # Exceptions should NOT be used to flag application-specific conditions that a client is expected + # to handle in an application-specific way. Put another way, in the Cap'n Proto world, + # "checked exceptions" (where an interface explicitly defines the exceptions it throws and + # clients are forced by the type system to handle those exceptions) do NOT make sense. + + reason @0 :Text; + # Human-readable failure description. + + type @3 :Type; + # The type of the error. The purpose of this enum is not to describe the error itself, but + # rather to describe how the client might want to respond to the error. + + enum Type { + failed @0; + # A generic problem occurred, and it is believed that if the operation were repeated without + # any change in the state of the world, the problem would occur again. + # + # A client might respond to this error by logging it for investigation by the developer and/or + # displaying it to the user. + + overloaded @1; + # The request was rejected due to a temporary lack of resources. + # + # Examples include: + # - There's not enough CPU time to keep up with incoming requests, so some are rejected. + # - The server ran out of RAM or disk space during the request. + # - The operation timed out (took significantly longer than it should have). + # + # A client might respond to this error by scheduling to retry the operation much later. The + # client should NOT retry again immediately since this would likely exacerbate the problem. + + disconnected @2; + # The method failed because a connection to some necessary capability was lost. + # + # Examples include: + # - The client introduced the server to a third-party capability, the connection to that third + # party was subsequently lost, and then the client requested that the server use the dead + # capability for something. + # - The client previously requested that the server obtain a capability from some third party. + # The server returned a capability to an object wrapping the third-party capability. Later, + # the server's connection to the third party was lost. + # - The capability has been revoked. Revocation does not necessarily mean that the client is + # no longer authorized to use the capability; it is often used simply as a way to force the + # client to repeat the setup process, perhaps to efficiently move them to a new back-end or + # get them to recognize some other change that has occurred. + # + # A client should normally respond to this error by releasing all capabilities it is currently + # holding related to the one it called and then re-creating them by restoring SturdyRefs and/or + # repeating the method calls used to create them originally. In other words, disconnect and + # start over. This should in turn cause the server to obtain a new copy of the capability that + # it lost, thus making everything work. + # + # If the client receives another `disconnected` error in the process of rebuilding the + # capability and retrying the call, it should treat this as an `overloaded` error: the network + # is currently unreliable, possibly due to load or other temporary issues. + + unimplemented @3; + # The server doesn't implement the requested method. If there is some other method that the + # client could call (perhaps an older and/or slower interface), it should try that instead. + # Otherwise, this should be treated like `failed`. + } + + obsoleteIsCallersFault @1 :Bool; + # OBSOLETE. Ignore. + + obsoleteDurability @2 :UInt16; + # OBSOLETE. See `type` instead. +} + +# ======================================================================================== +# Network-specific Parameters +# +# Some parts of the Cap'n Proto RPC protocol are not specified here because different vat networks +# may wish to use different approaches to solving them. For example, on the public internet, you +# may want to authenticate vats using public-key cryptography, but on a local intranet with trusted +# infrastructure, you may be happy to authenticate based on network address only, or some other +# lightweight mechanism. +# +# To accommodate this, we specify several "parameter" types. Each type is defined here as an +# alias for `AnyPointer`, but a specific network will want to define a specific set of types to use. +# All vats in a vat network must agree on these parameters in order to be able to communicate. +# Inter-network communication can be accomplished through "gateways" that perform translation +# between the primitives used on each network; these gateways may need to be deeply stateful, +# depending on the translations they perform. +# +# For interaction over the global internet between parties with no other prior arrangement, a +# particular set of bindings for these types is defined elsewhere. (TODO(someday): Specify where +# these common definitions live.) +# +# Another common network type is the two-party network, in which one of the parties typically +# interacts with the outside world entirely through the other party. In such a connection between +# Alice and Bob, all objects that exist on Bob's other networks appear to Alice as if they were +# hosted by Bob himself, and similarly all objects on Alice's network (if she even has one) appear +# to Bob as if they were hosted by Alice. This network type is interesting because from the point +# of view of a simple application that communicates with only one other party via the two-party +# protocol, there are no three-party interactions at all, and joins are unusually simple to +# implement, so implementing at level 4 is barely more complicated than implementing at level 1. +# Moreover, if you pair an app implementing the two-party network with a container that implements +# some other network, the app can then participate on the container's network just as if it +# implemented that network directly. The types used by the two-party network are defined in +# `rpc-twoparty.capnp`. +# +# The things that we need to parameterize are: +# - How to store capabilities long-term without holding a connection open (mostly level 2). +# - How to authenticate vats in three-party introductions (level 3). +# - How to implement `Join` (level 4). +# +# Persistent references +# --------------------- +# +# **(mostly level 2)** +# +# We want to allow some capabilities to be stored long-term, even if a connection is lost and later +# recreated. ExportId is a short-term identifier that is specific to a connection, so it doesn't +# help here. We need a way to specify long-term identifiers, as well as a strategy for +# reconnecting to a referenced capability later. +# +# Three-party interactions +# ------------------------ +# +# **(level 3)** +# +# In cases where more than two vats are interacting, we have situations where VatA holds a +# capability hosted by VatB and wants to send that capability to VatC. This can be accomplished +# by VatA proxying requests on the new capability, but doing so has two big problems: +# - It's inefficient, requiring an extra network hop. +# - If VatC receives another capability to the same object from VatD, it is difficult for VatC to +# detect that the two capabilities are really the same and to implement the E "join" operation, +# which is necessary for certain four-or-more-party interactions, such as the escrow pattern. +# See: http://www.erights.org/elib/equality/grant-matcher/index.html +# +# Instead, we want a way for VatC to form a direct, authenticated connection to VatB. +# +# Join +# ---- +# +# **(level 4)** +# +# The `Join` message type and corresponding operation arranges for a direct connection to be formed +# between the joiner and the host of the joined object, and this connection must be authenticated. +# Thus, the details are network-dependent. + +using SturdyRef = AnyPointer; +# **(level 2)** +# +# Identifies a persisted capability that can be restored in the future. How exactly a SturdyRef +# is restored to a live object is specified along with the SturdyRef definition (i.e. not by +# rpc.capnp). +# +# Generally a SturdyRef needs to specify three things: +# - How to reach the vat that can restore the ref (e.g. a hostname or IP address). +# - How to authenticate the vat after connecting (e.g. a public key fingerprint). +# - The identity of a specific object hosted by the vat. Generally, this is an opaque pointer whose +# format is defined by the specific vat -- the client has no need to inspect the object ID. +# It is important that the object ID be unguessable if the object is not public (and objects +# should almost never be public). +# +# The above are only suggestions. Some networks might work differently. For example, a private +# network might employ a special restorer service whose sole purpose is to restore SturdyRefs. +# In this case, the entire contents of SturdyRef might be opaque, because they are intended only +# to be forwarded to the restorer service. + +using ProvisionId = AnyPointer; +# **(level 3)** +# +# The information that must be sent in an `Accept` message to identify the object being accepted. +# +# In a network where each vat has a public/private key pair, this could simply be the public key +# fingerprint of the provider vat along with a nonce matching the one in the `RecipientId` used +# in the `Provide` message sent from that provider. + +using RecipientId = AnyPointer; +# **(level 3)** +# +# The information that must be sent in a `Provide` message to identify the recipient of the +# capability. +# +# In a network where each vat has a public/private key pair, this could simply be the public key +# fingerprint of the recipient along with a nonce matching the one in the `ProvisionId`. +# +# As another example, when communicating between processes on the same machine over Unix sockets, +# RecipientId could simply refer to a file descriptor attached to the message via SCM_RIGHTS. +# This file descriptor would be one end of a newly-created socketpair, with the other end having +# been sent to the capability's recipient in ThirdPartyCapId. + +using ThirdPartyCapId = AnyPointer; +# **(level 3)** +# +# The information needed to connect to a third party and accept a capability from it. +# +# In a network where each vat has a public/private key pair, this could be a combination of the +# third party's public key fingerprint, hints on how to connect to the third party (e.g. an IP +# address), and the nonce used in the corresponding `Provide` message's `RecipientId` as sent +# to that third party (used to identify which capability to pick up). +# +# As another example, when communicating between processes on the same machine over Unix sockets, +# ThirdPartyCapId could simply refer to a file descriptor attached to the message via SCM_RIGHTS. +# This file descriptor would be one end of a newly-created socketpair, with the other end having +# been sent to the process hosting the capability in RecipientId. + +using JoinKeyPart = AnyPointer; +# **(level 4)** +# +# A piece of a secret key. One piece is sent along each path that is expected to lead to the same +# place. Once the pieces are combined, a direct connection may be formed between the sender and +# the receiver, bypassing any men-in-the-middle along the paths. See the `Join` message type. +# +# The motivation for Joins is discussed under "Supporting Equality" in the "Unibus" protocol +# sketch: http://www.erights.org/elib/distrib/captp/unibus.html +# +# In a network where each vat has a public/private key pair and each vat forms no more than one +# connection to each other vat, Joins will rarely -- perhaps never -- be needed, as objects never +# need to be transparently proxied and references to the same object sent over the same connection +# have the same export ID. Thus, a successful join requires only checking that the two objects +# come from the same connection and have the same ID, and then completes immediately. +# +# However, in networks where two vats may form more than one connection between each other, or +# where proxying of objects occurs, joins are necessary. +# +# Typically, each JoinKeyPart would include a fixed-length data value such that all value parts +# XOR'd together forms a shared secret that can be used to form an encrypted connection between +# the joiner and the joined object's host. Each JoinKeyPart should also include an indication of +# how many parts to expect and a hash of the shared secret (used to match up parts). + +using JoinResult = AnyPointer; +# **(level 4)** +# +# Information returned as the result to a `Join` message, needed by the joiner in order to form a +# direct connection to a joined object. This might simply be the address of the joined object's +# host vat, since the `JoinKey` has already been communicated so the two vats already have a shared +# secret to use to authenticate each other. +# +# The `JoinResult` should also contain information that can be used to detect when the Join +# requests ended up reaching different objects, so that this situation can be detected easily. +# This could be a simple matter of including a sequence number -- if the joiner receives two +# `JoinResult`s with sequence number 0, then they must have come from different objects and the +# whole join is a failure. + +# ======================================================================================== +# Network interface sketch +# +# The interfaces below are meant to be pseudo-code to illustrate how the details of a particular +# vat network might be abstracted away. They are written like Cap'n Proto interfaces, but in +# practice you'd probably define these interfaces manually in the target programming language. A +# Cap'n Proto RPC implementation should be able to use these interfaces without knowing the +# definitions of the various network-specific parameters defined above. + +# interface VatNetwork { +# # Represents a vat network, with the ability to connect to particular vats and receive +# # connections from vats. +# # +# # Note that methods returning a `Connection` may return a pre-existing `Connection`, and the +# # caller is expected to find and share state with existing users of the connection. +# +# # Level 0 features ----------------------------------------------- +# +# connect(vatId :VatId) :Connection; +# # Connect to the given vat. The transport should return a promise that does not +# # resolve until authentication has completed, but allows messages to be pipelined in before +# # that; the transport either queues these messages until authenticated, or sends them encrypted +# # such that only the authentic vat would be able to decrypt them. The latter approach avoids a +# # round trip for authentication. +# +# accept() :Connection; +# # Wait for the next incoming connection and return it. Only connections formed by +# # connect() are returned by this method. +# +# # Level 4 features ----------------------------------------------- +# +# newJoiner(count :UInt32) :NewJoinerResponse; +# # Prepare a new Join operation, which will eventually lead to forming a new direct connection +# # to the host of the joined capability. `count` is the number of capabilities to join. +# +# struct NewJoinerResponse { +# joinKeyParts :List(JoinKeyPart); +# # Key parts to send in Join messages to each capability. +# +# joiner :Joiner; +# # Used to establish the final connection. +# } +# +# interface Joiner { +# addJoinResult(result :JoinResult) :Void; +# # Add a JoinResult received in response to one of the `Join` messages. All `JoinResult`s +# # returned from all paths must be added before trying to connect. +# +# connect() :ConnectionAndProvisionId; +# # Try to form a connection to the joined capability's host, verifying that it has received +# # all of the JoinKeyParts. Once the connection is formed, the caller should send an `Accept` +# # message on it with the specified `ProvisionId` in order to receive the final capability. +# } +# +# acceptConnectionFromJoiner(parts :List(JoinKeyPart), paths :List(VatPath)) +# :ConnectionAndProvisionId; +# # Called on a joined capability's host to receive the connection from the joiner, once all +# # key parts have arrived. The caller should expect to receive an `Accept` message over the +# # connection with the given ProvisionId. +# } +# +# interface Connection { +# # Level 0 features ----------------------------------------------- +# +# send(message :Message) :Void; +# # Send the message. Returns successfully when the message (and all preceding messages) has +# # been acknowledged by the recipient. +# +# receive() :Message; +# # Receive the next message, and acknowledges receipt to the sender. Messages are received in +# # the order in which they are sent. +# +# # Level 3 features ----------------------------------------------- +# +# introduceTo(recipient :Connection) :IntroductionInfo; +# # Call before starting a three-way introduction, assuming a `Provide` message is to be sent on +# # this connection and a `ThirdPartyCapId` is to be sent to `recipient`. +# +# struct IntroductionInfo { +# sendToRecipient :ThirdPartyCapId; +# sendToTarget :RecipientId; +# } +# +# connectToIntroduced(capId :ThirdPartyCapId) :ConnectionAndProvisionId; +# # Given a ThirdPartyCapId received over this connection, connect to the third party. The +# # caller should then send an `Accept` message over the new connection. +# +# acceptIntroducedConnection(recipientId :RecipientId) :Connection; +# # Given a RecipientId received in a `Provide` message on this `Connection`, wait for the +# # recipient to connect, and return the connection formed. Usually, the first message received +# # on the new connection will be an `Accept` message. +# } +# +# struct ConnectionAndProvisionId { +# # **(level 3)** +# +# connection :Connection; +# # Connection on which to issue `Accept` message. +# +# provision :ProvisionId; +# # `ProvisionId` to send in the `Accept` message. +# } diff --git a/runtime-rpc/src/test/java/org/capnproto/CapabilityTest.java b/runtime-rpc/src/test/java/org/capnproto/CapabilityTest.java new file mode 100644 index 00000000..2296746a --- /dev/null +++ b/runtime-rpc/src/test/java/org/capnproto/CapabilityTest.java @@ -0,0 +1,357 @@ +package org.capnproto; + +// Copyright (c) 2018 Sandstorm Development Group, Inc. and contributors +// Licensed under the MIT License: +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import org.capnproto.rpctest.Test; + +import org.junit.Assert; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; + +class Counter { + private int count = 0; + int inc() { return count++; } + int value() { return count; } +} + +class TestExtendsImpl extends Test.TestExtends2.Server { + + final Counter counter; + + TestExtendsImpl(Counter counter) { + this.counter = counter; + } + + @Override + protected CompletableFuture foo(CallContext context) { + counter.inc(); + var params = context.getParams(); + var result = context.getResults(); + Assert.assertEquals(321, params.getI()); + Assert.assertFalse(params.getJ()); + result.setX("bar"); + return CompletableFuture.completedFuture(null); + } + + @Override + protected CompletableFuture grault(CallContext context) { + counter.inc(); + context.releaseParams(); + RpcTestUtil.initTestMessage(context.getResults()); + return CompletableFuture.completedFuture(null); + } +} + +public final class CapabilityTest { + + @org.junit.Test + public void testBasic() { + var callCount = new Counter(); + var client = new Test.TestInterface.Client( + new RpcTestUtil.TestInterfaceImpl(callCount)); + + var request1 = client.fooRequest(); + request1.getParams().setI(123); + request1.getParams().setJ(true); + var promise1 = request1.send(); + + var request2 = client.bazRequest(); + RpcTestUtil.initTestMessage(request2.getParams().initS()); + var promise2 = request2.send(); + + boolean barFailed = false; + var request3 = client.barRequest(); + var promise3 = request3.send().whenComplete((value, exc) -> { + Assert.assertNotNull(exc); + Assert.assertTrue(exc instanceof RpcException); + var rpcExc = (RpcException)exc; + Assert.assertEquals(RpcException.Type.UNIMPLEMENTED, rpcExc.getType()); + }); + } + + @org.junit.Test + public void testInheritance() throws ExecutionException, InterruptedException { + var callCount = new Counter(); + + var client1 = new Test.TestExtends.Client( + new TestExtendsImpl(callCount)); + + Test.TestInterface.Client client2 = client1; + var client = (Test.TestExtends.Client)client2; + + var request1 = client.fooRequest(); + request1.getParams().setI(321); + var promise1 = request1.send(); + + var request2 = client.graultRequest(); + var promise2 = request2.send(); + + // Hmm, we have no means to defer the evaluation of callInternal. + //Assert.assertEquals(0, callCount.value()); + + var response2 = promise2.get(); + RpcTestUtil.checkTestMessage(response2); + + var response1 = promise1.get(); + Assert.assertEquals("bar", response1.getX().toString()); + Assert.assertEquals(2, callCount.value()); + } + + @org.junit.Test + public void testPipelining() throws ExecutionException, InterruptedException { + var callCount = new Counter(); + var chainedCallCount = new Counter(); + + var client = new Test.TestPipeline.Client( + new RpcTestUtil.TestPipelineImpl(callCount)); + + var request = client.getCapRequest(); + var params = request.getParams(); + params.setN(234); + params.setInCap(new Test.TestInterface.Client( + new RpcTestUtil.TestInterfaceImpl(chainedCallCount))); + + var promise = request.send(); + var outbox = promise.getOutBox(); + var pipelineRequest = outbox.getCap().fooRequest(); + pipelineRequest.getParams().setI(321); + var pipelinePromise = pipelineRequest.send(); + var pipelineRequest2 = new Test.TestExtends.Client(promise.getOutBox().getCap()).graultRequest(); + var pipelinePromise2 = pipelineRequest2.send(); + + // Hmm, we have no means to defer the evaluation of callInternal. The best we can do is + // wait for the client to have resolved. + + //Assert.assertEquals(0, callCount.value()); + //Assert.assertEquals(0, chainedCallCount.value()); + + var response = pipelinePromise.get(); + Assert.assertEquals("bar", response.getX().toString()); + var response2 = pipelinePromise2.get(); + RpcTestUtil.checkTestMessage(response2); + Assert.assertEquals(3, callCount.value()); + Assert.assertEquals(1, chainedCallCount.value()); + } + + @org.junit.Test + public void testTailCall() { + var calleeCallCount = new Counter(); + var callerCallCount = new Counter(); + var callee = new Test.TestTailCallee.Client( + new RpcTestUtil.TestTailCalleeImpl(calleeCallCount)); + + var caller = new Test.TestTailCaller.Client( + new RpcTestUtil.TestTailCallerImpl(callerCallCount)); + + var request = caller.fooRequest(); + request.getParams().setI(456); + request.getParams().setCallee(callee); + + var promise = request.send(); + + var dependentCall0 = promise.getC().getCallSequenceRequest().send(); + + var response = promise.join(); + Assert.assertEquals(456, response.getI()); + Assert.assertEquals(456, response.getI()); + + var dependentCall1 = promise.getC().getCallSequenceRequest().send(); + + var dependentCall2 = response.getC().getCallSequenceRequest().send(); + + Assert.assertEquals(0, dependentCall0.join().getN()); + Assert.assertEquals(1, dependentCall1.join().getN()); + Assert.assertEquals(2, dependentCall2.join().getN()); + + Assert.assertEquals(1, calleeCallCount.value()); + Assert.assertEquals(1, callerCallCount.value()); + } + + class TestThisCap extends Test.TestInterface.Server { + + Counter counter; + + TestThisCap(Counter counter) { + this.counter = counter; + } + + Test.TestInterface.Client getSelf() { + return this.thisCap(); + } + + @Override + protected CompletableFuture bar(CallContext context) { + this.counter.inc(); + return READY_NOW; + } + } + + @org.junit.Test + public void testGenerics() { + var factory = Test.TestGenerics.newFactory(Test.TestAllTypes.factory, AnyPointer.factory); + } + + @org.junit.Test + public void thisCap() { + var callCount = new Counter(); + var server = new TestThisCap(callCount); + var client = new Test.TestInterface.Client(server); + client.barRequest().send().join(); + Assert.assertEquals(1, callCount.value()); + + var client2 = server.getSelf(); + Assert.assertEquals(1, callCount.value()); + client2.barRequest().send().join(); + Assert.assertEquals(2, callCount.value()); + client = null; + Assert.assertEquals(2, callCount.value()); + client2.barRequest().send().join(); + Assert.assertEquals(3, callCount.value()); + } + + @org.junit.Test + public void testStreamingCallsBlockSubsequentCalls() { + var server = new RpcTestUtil.TestStreamingImpl(); + var cap = new Test.TestStreaming.Client(server); + + CompletableFuture promise1 = null; + CompletableFuture promise2 = null; + CompletableFuture promise3 = null; + + { + var req = cap.doStreamIRequest(); + req.getParams().setI(123); + promise1 = req.send(); + } + + { + var req = cap.doStreamJRequest(); + req.getParams().setJ(321); + promise2 = req.send(); + } + + { + var req = cap.doStreamIRequest(); + req.getParams().setI(456); + promise3 = req.send(); + } + + var promise4 = cap.finishStreamRequest().send(); + + // Only the first streaming call has executed + Assert.assertEquals(123, server.iSum); + Assert.assertEquals(0, server.jSum); + + // Complete first streaming call + Assert.assertNotNull(server.fulfiller); + server.fulfiller.complete(null); + + // second streaming call unblocked + Assert.assertEquals(123, server.iSum); + Assert.assertEquals(321, server.jSum); + + // complete second streaming call + Assert.assertNotNull(server.fulfiller); + server.fulfiller.complete(null); + + // third streaming call unblocked + Assert.assertEquals(579, server.iSum); + Assert.assertEquals(321, server.jSum); + + // complete third streaming call + Assert.assertNotNull(server.fulfiller); + server.fulfiller.complete(null); + + // last call is unblocked + var result = promise4.join(); + Assert.assertEquals(579, result.getTotalI()); + Assert.assertEquals(321, result.getTotalJ()); + } + + @org.junit.Test + public void testCapabilityServerSet() { + var set1 = new Capability.CapabilityServerSet(); + var set2 = new Capability.CapabilityServerSet(); + + var callCount = new Counter(); + var clientStandalone = new Test.TestInterface.Client(new RpcTestUtil.TestInterfaceImpl(callCount)); + var clientNull = new Test.TestInterface.Client(); + + var ownServer1 = new RpcTestUtil.TestInterfaceImpl(callCount); + var server1 = ownServer1; + var client1 = set1.add(Test.TestInterface.factory, ownServer1); + + var ownServer2 = new RpcTestUtil.TestInterfaceImpl(callCount); + var server2 = ownServer2; + var client2 = set2.add(Test.TestInterface.factory, ownServer2); + + // Getting the local server using the correct set works. + Assert.assertEquals(server1, set1.getLocalServer(client1).join()); + Assert.assertEquals(server2, set2.getLocalServer(client2).join()); + + // Getting the local server using the wrong set doesn't work. + Assert.assertNull(set1.getLocalServer(client2).join()); + Assert.assertNull(set2.getLocalServer(client1).join()); + Assert.assertNull(set1.getLocalServer(clientStandalone).join()); + Assert.assertNull(set1.getLocalServer(clientNull).join()); + + var promise = new CompletableFuture(); + var clientPromise = new Test.TestInterface.Client(promise); + + var errorPromise = new CompletableFuture(); + var clientErrorPromise = new Test.TestInterface.Client(errorPromise); + + var resolved1 = new AtomicBoolean(false); + var resolved2 = new AtomicBoolean(false); + var resolved3 = new AtomicBoolean(false); + + var promise1 = set1.getLocalServer(clientPromise).thenAccept(server -> { + resolved1.set(true); + Assert.assertEquals(server1, server); + }); + + var promise2 = set2.getLocalServer(clientPromise).thenAccept(server -> { + resolved2.set(true); + Assert.assertNull(server); + }); + + var promise3 = set1.getLocalServer(clientErrorPromise).whenComplete((server, exc) -> { + resolved3.set(true); + Assert.assertNull(server); + Assert.assertNotNull(exc); + Assert.assertTrue(exc.getCause() instanceof RpcException); + }); + + Assert.assertFalse(resolved1.get()); + Assert.assertFalse(resolved2.get()); + Assert.assertFalse(resolved3.get()); + + promise.complete(client1); + errorPromise.completeExceptionally(RpcException.failed("foo")); + + Assert.assertTrue(resolved1.get()); + Assert.assertTrue(resolved2.get()); + Assert.assertTrue(resolved3.get()); + } +} diff --git a/runtime-rpc/src/test/java/org/capnproto/EzRpcTest.java b/runtime-rpc/src/test/java/org/capnproto/EzRpcTest.java new file mode 100644 index 00000000..31de620f --- /dev/null +++ b/runtime-rpc/src/test/java/org/capnproto/EzRpcTest.java @@ -0,0 +1,29 @@ +package org.capnproto; + +import org.capnproto.rpctest.Test; +import org.junit.Assert; +import java.net.InetSocketAddress; +import java.nio.channels.AsynchronousSocketChannel; + +public class EzRpcTest { + + @org.junit.Test + public void testBasic() throws Exception { + var callCount = new Counter(); + var address = new InetSocketAddress("localhost", 0); + var server = new EzRpcServer(new RpcTestUtil.TestInterfaceImpl(callCount), address); + server.start(); + + var clientSocket = AsynchronousSocketChannel.open(); + clientSocket.connect(new InetSocketAddress("localhost", server.getPort())).get(); + var client = new EzRpcClient(clientSocket); + var cap = new Test.TestInterface.Client(client.getMain()); + var request = cap.fooRequest(); + request.getParams().setI(123); + request.getParams().setJ(true); + + var response = client.runUntil(request.send()).join(); + Assert.assertEquals("foo", response.getX().toString()); + Assert.assertEquals(1, callCount.value()); + } +} diff --git a/runtime-rpc/src/test/java/org/capnproto/RpcTest.java b/runtime-rpc/src/test/java/org/capnproto/RpcTest.java new file mode 100644 index 00000000..e9102f94 --- /dev/null +++ b/runtime-rpc/src/test/java/org/capnproto/RpcTest.java @@ -0,0 +1,644 @@ +// Copyright (c) 2018 Sandstorm Development Group, Inc. and contributors +// Licensed under the MIT License: +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package org.capnproto; + +import org.capnproto.rpctest.Test; + +import org.junit.Assert; + +import java.lang.ref.WeakReference; +import java.util.ArrayDeque; +import java.util.HashMap; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.atomic.AtomicBoolean; + +public class RpcTest { + + static final class TestNetwork { + + final Map map = new HashMap<>(); + int received = 0; + + TestNetworkAdapter add(String name) { + return this.map.computeIfAbsent( + name, key -> new TestNetworkAdapter(this, name)); + } + + TestNetworkAdapter find(String name) { + return this.map.get(name); + } + } + + static final class TestNetworkAdapter + implements VatNetwork, + AutoCloseable { + + class Connection implements VatNetwork.Connection { + + Throwable networkException; + Connection partner; + final Queue messages = new ArrayDeque<>(); + final Queue> fulfillers = new ArrayDeque<>(); + CompletableFuture fulfillOnEnd; + final boolean isClient; + final Test.TestSturdyRef.Reader peerId; + + Connection(boolean isClient, Test.TestSturdyRef.Reader peerId) { + this.isClient = isClient; + this.peerId = peerId; + } + + @Override + public String toString() { + return this.isClient ? "CLIENT" : "SERVER"; + } + + void attach(Connection other) { + Assert.assertNull(this.partner); + Assert.assertNull(other.partner); + this.partner = other; + other.partner = this; + } + + void disconnect(Exception exc) { + while (!fulfillers.isEmpty()) { + fulfillers.remove().completeExceptionally(exc); + } + + this.networkException = exc; + } + + TestNetwork getNetwork() { + return network; + } + + @Override + public OutgoingRpcMessage newOutgoingMessage(int firstSegmentWordSize) { + var message = new MessageBuilder(firstSegmentWordSize); + + return new OutgoingRpcMessage() { + @Override + public AnyPointer.Builder getBody() { + return message.getRoot(AnyPointer.factory); + } + + @Override + public void send() { + if (networkException != null) { + return; + } + + var incomingMessage = new IncomingRpcMessage() { + @Override + public AnyPointer.Reader getBody() { + return message.getRoot(AnyPointer.factory).asReader(); + } + }; + + if (partner == null) { + return; + } + + if (partner.fulfillers.isEmpty()) { + partner.messages.add(incomingMessage); + } + else { + partner.getNetwork().received++; + var front = partner.fulfillers.remove(); + front.complete(incomingMessage); + } + } + + @Override + public int sizeInWords() { + return 0; + } + }; + } + + @Override + public CompletableFuture receiveIncomingMessage() { + if (this.networkException != null) { + return CompletableFuture.failedFuture(this.networkException); + } + + if (this.messages.isEmpty()) { + if (this.fulfillOnEnd != null) { + this.fulfillOnEnd.complete(null); + return CompletableFuture.completedFuture(null); + } + else { + var promise = new CompletableFuture(); + this.fulfillers.add(promise); + return promise.copy(); + } + } + else { + this.getNetwork().received++; + var result = this.messages.remove(); + return CompletableFuture.completedFuture(result); + } + } + + @Override + public CompletableFuture shutdown() { + if (this.partner == null) { + return CompletableFuture.completedFuture(null); + } + var promise = new CompletableFuture(); + this.partner.fulfillOnEnd = promise; + return promise.copy(); + } + + public Test.TestSturdyRef.Reader getPeerVatId() { + return this.peerId; + } + + @Override + public void close() { + } + } + + final TestNetwork network; + private final String self; + int sent = 0; + int received = 0; + Map connections = new HashMap<>(); + Queue>> fulfillerQueue = new ArrayDeque<>(); + Queue connectionQueue = new ArrayDeque<>(); + + TestNetworkAdapter(TestNetwork network, String self) { + this.network = network; + this.self = self; + } + + Connection newConnection(boolean isClient, Test.TestSturdyRef.Reader peerId) { + return new Connection(isClient, peerId); + } + + @Override + public void close() { + var exc = RpcException.failed("Network was destroyed"); + for (var conn: this.connections.values()) { + conn.disconnect(exc); + } + } + + @Override + public VatNetwork.Connection connect(Test.TestSturdyRef.Reader refId) { + var hostId = refId.getHostId().getHost().toString(); + if (hostId.equals(self)) { + return null; + } + + var dst = this.network.find(hostId); + Assert.assertNotNull(dst); + + var connnection = this.connections.get(dst); + if (connnection != null) { + return connnection; + } + + var local = this.newConnection(true, refId); + var remote = dst.newConnection(false, refId); + local.attach(remote); + + this.connections.put(dst, local); + dst.connections.put(this, remote); + + if (dst.fulfillerQueue.isEmpty()) { + dst.fulfillerQueue.add(CompletableFuture.completedFuture(remote)); + } else { + dst.fulfillerQueue.remove().complete(remote); + } + return local; + } + + public CompletableFuture> accept() { + if (this.connections.isEmpty()) { + var promise = new CompletableFuture>(); + this.fulfillerQueue.add(promise); + return promise; + } + else { + return CompletableFuture.completedFuture(this.connectionQueue.remove()); + } + } + } + + static final class TestContext { + final TestNetwork network = new TestNetwork(); + final TestNetworkAdapter clientNetwork; + final TestNetworkAdapter serverNetwork; + + final RpcSystem rpcClient; + final RpcSystem rpcServer; + + TestContext(Capability.Client bootstrapInterface) { + this.clientNetwork = this.network.add("client"); + this.serverNetwork = this.network.add("server"); + this.rpcClient = RpcSystem.makeRpcClient(this.clientNetwork); + this.rpcServer = RpcSystem.makeRpcServer(this.serverNetwork, bootstrapInterface); + } + + TestContext(BootstrapFactory bootstrapFactory) { + this.clientNetwork = this.network.add("client"); + this.serverNetwork = this.network.add("server"); + this.rpcClient = RpcSystem.makeRpcClient(this.clientNetwork); + this.rpcServer = RpcSystem.makeRpcServer(this.serverNetwork, bootstrapFactory); + this.rpcServer.start(); + } + + Capability.Client connect(Test.TestSturdyRefObjectId.Tag tag) { + var message = new MessageBuilder(); + var ref = message.initRoot(Test.TestSturdyRef.factory); + var hostId = ref.initHostId(); + hostId.setHost("server"); + ref.getObjectId().initAs(Test.TestSturdyRefObjectId.factory).setTag(tag); + return rpcClient.bootstrap(ref.asReader()); + } + + public CompletableFuture runUntil(CompletableFuture done) { + while (!done.isDone()) { + this.rpcClient.runOnce(); + } + return done; + } + } + + static BootstrapFactory bootstrapFactory = new BootstrapFactory<>() { + @Override + public Capability.Client createFor(Test.TestSturdyRef.Reader refId) { + var callCount = new Counter(); + var handleCount = new Counter(); + + var objectId = refId.getObjectId().getAs(Test.TestSturdyRefObjectId.factory); + var tag = objectId.getTag(); + switch (tag) { + case TEST_INTERFACE: + return new Capability.Client(new RpcTestUtil.TestInterfaceImpl(callCount)); + case TEST_EXTENDS: + return new Capability.Client(Capability.newBrokenCap("No TestExtends implemented.")); + case TEST_PIPELINE: + return new Capability.Client(new RpcTestUtil.TestPipelineImpl(callCount)); + case TEST_TAIL_CALLEE: + return new Capability.Client(new RpcTestUtil.TestTailCalleeImpl(callCount)); + case TEST_TAIL_CALLER: + return new Capability.Client(new RpcTestUtil.TestTailCallerImpl(callCount)); + case TEST_MORE_STUFF: + return new Capability.Client(new RpcTestUtil.TestMoreStuffImpl(callCount, handleCount)); + default: + return new Capability.Client(); + } + } + }; + + TestContext context; + + @org.junit.Before + public void setUp() { + this.context = new TestContext(bootstrapFactory); + } + + @org.junit.After + public void tearDown() { + this.context.clientNetwork.close(); + this.context.serverNetwork.close(); + this.context = null; + } + + @org.junit.Test + public void testBasic() { + var client = new Test.TestInterface.Client(context.connect(Test.TestSturdyRefObjectId.Tag.TEST_INTERFACE)); + var request1 = client.fooRequest(); + request1.getParams().setI(123); + request1.getParams().setJ(true); + var promise1 = request1.send(); + + final var ref = new Object() { + boolean barFailed = false; + }; + var request3 = client.barRequest(); + var promise3 = request3.send().exceptionally(exc -> { + ref.barFailed = true; + return null; + }); + + var request2 = client.bazRequest(); + RpcTestUtil.initTestMessage(request2.getParams().initS()); + var promise2 = request2.send(); + + var response1 = this.context.runUntil(promise1).join(); + Assert.assertEquals("foo", response1.getX().toString()); + + while (!promise2.isDone()) { + this.context.rpcClient.runOnce(); + } + var response2 = this.context.runUntil(promise2).join(); + + this.context.runUntil(promise3).join(); + Assert.assertTrue(ref.barFailed); + } + + @org.junit.Test + public void testPipelining() { + var client = new Test.TestPipeline.Client(context.connect(Test.TestSturdyRefObjectId.Tag.TEST_PIPELINE)); + + var chainedCallCount = new Counter(); + + var request = client.getCapRequest(); + request.getParams().setN(234); + request.getParams().setInCap(new RpcTestUtil.TestInterfaceImpl(chainedCallCount)); + + var promise = request.send(); + + var pipelineRequest = promise.getOutBox().getCap().fooRequest(); + pipelineRequest.getParams().setI(321); + + var pipelinePromise = pipelineRequest.send(); + + var pipelineRequest2 = new Test.TestExtends.Client(promise.getOutBox().getCap()).graultRequest(); + var pipelinePromise2 = pipelineRequest2.send(); + + promise = null; + + //Assert.assertEquals(0, chainedCallCount.value()); + + var response = this.context.runUntil(pipelinePromise).join(); + Assert.assertEquals("bar", response.getX().toString()); + + var response2 = this.context.runUntil(pipelinePromise2).join(); + RpcTestUtil.checkTestMessage(response2); + + Assert.assertEquals(1, chainedCallCount.value()); + } + + @org.junit.Test + public void testRelease() { + var client = new Test.TestMoreStuff.Client(context.connect(Test.TestSturdyRefObjectId.Tag.TEST_MORE_STUFF)); + + var handle1 = this.context.runUntil(client.getHandleRequest().send()).join().getHandle(); + var promise = client.getHandleRequest().send(); + var handle2 = this.context.runUntil(promise).join().getHandle(); + + var handleRef1 = new WeakReference<>(handle1); + var handleRef2 = new WeakReference<>(handle2); + + promise = null; + handle1 = null; + handle2 = null; + + // TODO monitor the imported caps for release? close? + while (handleRef1.get() != null && handleRef2.get() != null) { + System.gc(); + this.context.runUntil(client.echoRequest().send()).join(); + } + } + + @org.junit.Test + public void testPromiseResolve() { + var client = new Test.TestMoreStuff.Client(context.connect(Test.TestSturdyRefObjectId.Tag.TEST_MORE_STUFF)); + + var chainedCallCount = new Counter(); + + var request = client.callFooRequest(); + var request2 = client.callFooWhenResolvedRequest(); + + var paf = new CompletableFuture(); + + { + request.getParams().setCap(new Test.TestInterface.Client(paf.copy())); + request2.getParams().setCap(new Test.TestInterface.Client(paf.copy())); + } + + var promise = request.send(); + var promise2 = request2.send(); + + // Make sure getCap() has been called on the server side by sending another call and waiting + // for it. + Assert.assertEquals(2, this.context.runUntil(client.getCallSequenceRequest().send()).join().getN()); + //Assert.assertEquals(3, context.restorer.callCount); + + // OK, now fulfill the local promise. + paf.complete(new Test.TestInterface.Client(new RpcTestUtil.TestInterfaceImpl(chainedCallCount))); + + // We should now be able to wait for getCap() to finish. + Assert.assertEquals("bar", this.context.runUntil(promise).join().getS().toString()); + Assert.assertEquals("bar", this.context.runUntil(promise2).join().getS().toString()); + + //Assert.assertEquals(3, context.restorer.callCount); + Assert.assertEquals(2, chainedCallCount.value()); + } + + @org.junit.Test + public void testTailCall() { + var releaseMe = new CompletableFuture(); + var caller = new Test.TestTailCaller.Client(context.connect(Test.TestSturdyRefObjectId.Tag.TEST_TAIL_CALLER)); + + var calleeCallCount = new Counter(); + var callee = new Test.TestTailCallee.Client(new RpcTestUtil.TestTailCalleeImpl(calleeCallCount, releaseMe)); + var request = caller.fooRequest(); + request.getParams().setI(456); + request.getParams().setCallee(callee); + + var promise = request.send(); + var dependentCall0 = promise.getC().getCallSequenceRequest().send(); + releaseMe.complete(null); + var response = this.context.runUntil(promise).join(); + Assert.assertEquals(456, response.getI()); + + var dependentCall1 = promise.getC().getCallSequenceRequest().send(); + + Assert.assertEquals(0, this.context.runUntil(dependentCall0).join().getN()); + Assert.assertEquals(1, this.context.runUntil(dependentCall1).join().getN()); + + var dependentCall2 = response.getC().getCallSequenceRequest().send(); + Assert.assertEquals(2, this.context.runUntil(dependentCall2).join().getN()); + Assert.assertEquals(1, calleeCallCount.value()); + + // The imported cap has resolved locally, and can be called directly + Assert.assertEquals(3, promise.getC().getCallSequenceRequest().send().join().getN()); + Assert.assertEquals(4, promise.getC().getCallSequenceRequest().send().join().getN()); + } + + static CompletableFuture getCallSequence( + Test.TestCallOrder.Client client, int expected) { + var req = client.getCallSequenceRequest(); + req.getParams().setExpected(expected); + return req.send(); + } + + @org.junit.Test + public void testEmbargo() { + var client = new Test.TestMoreStuff.Client(context.connect(Test.TestSturdyRefObjectId.Tag.TEST_MORE_STUFF)); + + var cap = new Test.TestCallOrder.Client(new RpcTestUtil.TestCallOrderImpl()); + var earlyCall = client.getCallSequenceRequest().send(); + + var echoRequest = client.echoRequest(); + echoRequest.getParams().setCap(cap); + var echo = echoRequest.send(); + + var pipeline = echo.getCap(); + var call0 = getCallSequence(pipeline, 0); + var call1 = getCallSequence(pipeline, 1); + + this.context.runUntil(earlyCall).join(); + + var call2 = getCallSequence(pipeline, 2); + + var resolved = this.context.runUntil(echo).join().getCap(); + + var call3 = getCallSequence(pipeline, 3); + var call4 = getCallSequence(pipeline, 4); + var call5 = getCallSequence(pipeline, 5); + + Assert.assertEquals(0, this.context.runUntil(call0).join().getN()); + Assert.assertEquals(1, this.context.runUntil(call1).join().getN()); + Assert.assertEquals(2, this.context.runUntil(call2).join().getN()); + Assert.assertEquals(3, this.context.runUntil(call3).join().getN()); + Assert.assertEquals(4, this.context.runUntil(call4).join().getN()); + Assert.assertEquals(5, this.context.runUntil(call5).join().getN()); + } + + @org.junit.Test + public void testCallBrokenPromise() { + var client = new Test.TestMoreStuff.Client(context.connect(Test.TestSturdyRefObjectId.Tag.TEST_MORE_STUFF)); + + var paf = new CompletableFuture(); + + { + var req = client.holdRequest(); + req.getParams().setCap(paf); + this.context.runUntil(req.send()).join(); + } + + AtomicBoolean returned = new AtomicBoolean(false); + + var req = client.callHeldRequest().send().exceptionally(exc -> { + returned.set(true); + return null; + }).thenAccept(results -> { + returned.set(true); + }); + + Assert.assertFalse(returned.get()); + + paf.completeExceptionally(new Exception("foo")); + this.context.runUntil(req); + Assert.assertTrue(returned.get()); + + // Verify that we are still connected + this.context.runUntil(getCallSequence(client, 1)).join(); + } + + @org.junit.Test + public void testCallCancel() { + var client = new Test.TestMoreStuff.Client(context.connect(Test.TestSturdyRefObjectId.Tag.TEST_MORE_STUFF)); + + var request = client.expectCancelRequest(); + var cap = new RpcTestUtil.TestCapDestructor(); + request.getParams().setCap(cap); + + // auto-close the request without waiting for a response, triggering a cancellation request. + try (var response = request.send()) { + response.thenRun(() -> Assert.fail("Never completing call returned?")); + } + catch (CompletionException exc) { + Assert.assertNotNull(exc.getCause()); + Assert.assertTrue(exc.getCause() instanceof RpcException); + Assert.assertSame(((RpcException) exc.getCause()).getType(), RpcException.Type.FAILED); + } + catch (Exception exc) { + Assert.fail(exc.toString()); + } + + // check that the connection is still open + this.context.runUntil(getCallSequence(client, 1)).join(); + } + + @org.junit.Test + public void testEmbargoUnwrap() { + var capSet = new Capability.CapabilityServerSet(); + var client = new Test.TestMoreStuff.Client(context.connect(Test.TestSturdyRefObjectId.Tag.TEST_MORE_STUFF)); + + var cap = capSet.add(Test.TestCallOrder.factory, new RpcTestUtil.TestCallOrderImpl()); + + var earlyCall = client.getCallSequenceRequest().send(); + + var echoRequest = client.echoRequest(); + echoRequest.getParams().setCap(cap); + var echo = echoRequest.send(); + + var pipeline = echo.getCap(); + + var unwrap = capSet.getLocalServer(pipeline).thenApply(unwrapped -> { + Assert.assertNotNull(unwrapped); + return unwrapped != null + ? ((RpcTestUtil.TestCallOrderImpl)unwrapped).getCount() + : -1; + }); + + var call0 = getCallSequence(pipeline, 0); + var call1 = getCallSequence(pipeline, 1); + + this.context.runUntil(earlyCall).join(); + + var call2 = getCallSequence(pipeline, 2); + + var resolved = this.context.runUntil(echo).join().getCap(); + + var call3 = getCallSequence(pipeline, 3); + var call4 = getCallSequence(pipeline, 4); + var call5 = getCallSequence(pipeline, 5); + + Assert.assertEquals(0, this.context.runUntil(call0).join().getN()); + Assert.assertEquals(1, this.context.runUntil(call1).join().getN()); + Assert.assertEquals(2, this.context.runUntil(call2).join().getN()); + Assert.assertEquals(3, this.context.runUntil(call3).join().getN()); + Assert.assertEquals(4, this.context.runUntil(call4).join().getN()); + Assert.assertEquals(5, this.context.runUntil(call5).join().getN()); + + int unwrappedAt = this.context.runUntil(unwrap).join(); + Assert.assertTrue(unwrappedAt >= 0); + } + + @org.junit.Test + public void testEmbargoNull() { + var client = new Test.TestMoreStuff.Client(context.connect(Test.TestSturdyRefObjectId.Tag.TEST_MORE_STUFF)); + var promise = client.getNullRequest().send(); + var cap = promise.getNullCap(); + var call0 = cap.getCallSequenceRequest().send(); + this.context.runUntil(promise); + var call1 = cap.getCallSequenceRequest().send(); + + Assert.assertThrows(CompletionException.class, () -> this.context.runUntil(call0).join()); + Assert.assertThrows(CompletionException.class, () -> this.context.runUntil(call1).join()); + + // check that the connection is still open + this.context.runUntil(getCallSequence(client, 0)).join(); + } +} + diff --git a/runtime-rpc/src/test/java/org/capnproto/RpcTestUtil.java b/runtime-rpc/src/test/java/org/capnproto/RpcTestUtil.java new file mode 100644 index 00000000..370940b3 --- /dev/null +++ b/runtime-rpc/src/test/java/org/capnproto/RpcTestUtil.java @@ -0,0 +1,373 @@ +package org.capnproto; + +import org.capnproto.rpctest.Test; +import org.junit.Assert; + +import java.awt.desktop.SystemEventListener; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +class RpcTestUtil { + + static void initTestMessage(Test.TestAllTypes.Builder builder) { + builder.setVoidField(Void.VOID); + builder.setBoolField(true); + builder.setInt8Field((byte) -123); + builder.setInt16Field((short) -12345); + builder.setInt32Field(-12345678); + builder.setInt64Field(-123456789012345L); + builder.setUInt8Field((byte) 234); + builder.setUInt16Field((short) 45678); + builder.setUInt32Field((int) 3456789012L); + builder.setUInt64Field(1234567890123456789L); + builder.setFloat32Field(1234.5f); + builder.setFloat64Field(-123e45); + builder.setTextField("foo"); + } + + static void checkTestMessage(Test.TestAllTypes.Reader reader) { + Assert.assertEquals(Void.VOID, reader.getVoidField()); + Assert.assertTrue(reader.getBoolField()); + Assert.assertEquals((byte)-123, reader.getInt8Field()); + Assert.assertEquals((short)-12345, reader.getInt16Field()); + Assert.assertEquals(-12345678, reader.getInt32Field()); + Assert.assertEquals(-123456789012345L, reader.getInt64Field()); + Assert.assertEquals((byte)234, reader.getUInt8Field()); + Assert.assertEquals((short)45678, reader.getUInt16Field()); + Assert.assertEquals((int) 3456789012L, reader.getUInt32Field()); + Assert.assertEquals(1234567890123456789L, reader.getUInt64Field()); + Assert.assertEquals(null, 1234.5f, reader.getFloat32Field(), 0.1f); + Assert.assertEquals(null, -123e45, reader.getFloat64Field(), 0.1f); + Assert.assertEquals("foo", reader.getTextField().toString()); + } + + static class TestInterfaceImpl extends Test.TestInterface.Server { + + final Counter counter; + + TestInterfaceImpl(Counter counter) { + this.counter = counter; + } + + @Override + protected CompletableFuture foo(CallContext ctx) { + this.counter.inc(); + var params = ctx.getParams(); + var result = ctx.getResults(); + Assert.assertEquals(123, params.getI()); + Assert.assertTrue(params.getJ()); + result.setX("foo"); + return Capability.Server.READY_NOW; + } + + @Override + protected CompletableFuture baz(CallContext context) { + this.counter.inc(); + var params = context.getParams(); + checkTestMessage(params.getS()); + context.releaseParams(); + return Capability.Server.READY_NOW; + } + } + + static class TestTailCallerImpl extends Test.TestTailCaller.Server { + + private final Counter count; + + public TestTailCallerImpl(Counter count) { + this.count = count; + } + + @Override + protected CompletableFuture foo(CallContext context) { + this.count.inc(); + var params = context.getParams(); + var tailRequest = params.getCallee().fooRequest(); + tailRequest.getParams().setI(params.getI()); + tailRequest.getParams().setT("from TestTailCaller"); + return context.tailCall(tailRequest); + } + + public int getCount() { + return this.count.value(); + } + } + + static class HandleImpl extends Test.TestHandle.Server { + final Counter count; + + HandleImpl(Counter count) { + this.count = count; + count.inc(); + } + } + + static class TestMoreStuffImpl extends Test.TestMoreStuff.Server { + + private final Counter callCount; + private final Counter handleCount; + private Test.TestInterface.Client clientToHold; + + public TestMoreStuffImpl(Counter callCount, Counter handleCount) { + this.callCount = callCount; + this.handleCount = handleCount; + } + + @Override + protected CompletableFuture echo(CallContext context) { + this.callCount.inc(); + var params = context.getParams(); + var result = context.getResults(); + result.setCap(params.getCap()); + return READY_NOW; + } + + @Override + protected CompletableFuture expectCancel(CallContext context) { + var cap = context.getParams().getCap(); + context.allowCancellation(); + return new CompletableFuture().whenComplete((void_, exc) -> { + if (exc != null) { + System.out.println("expectCancel completed exceptionally: " + exc.getMessage()); + } + }); // never completes, just await doom... + } + + @Override + protected CompletableFuture getHandle(CallContext context) { + context.getResults().setHandle(new HandleImpl(this.handleCount)); + return READY_NOW; + } + + @Override + protected CompletableFuture getCallSequence(CallContext context) { + var result = context.getResults(); + result.setN(this.callCount.inc()); + return READY_NOW; + } + + @Override + protected CompletableFuture callFoo(CallContext context) { + this.callCount.inc(); + var params = context.getParams(); + var cap = params.getCap(); + var request = cap.fooRequest(); + request.getParams().setI(123); + request.getParams().setJ(true); + + return request.send().thenAccept(response -> { + Assert.assertEquals("foo", response.getX().toString()); + context.getResults().setS("bar"); + }); + } + + @Override + protected CompletableFuture callFooWhenResolved(CallContext context) { + this.callCount.inc(); + var params = context.getParams(); + var cap = params.getCap(); + + return cap.whenResolved().thenCompose(void_ -> { + var request = cap.fooRequest(); + request.getParams().setI(123); + request.getParams().setJ(true); + + return request.send().thenAccept(response -> { + Assert.assertEquals("foo", response.getX().toString()); + context.getResults().setS("bar"); + }); + }); + } + + @Override + protected CompletableFuture hold(CallContext context) { + this.callCount.inc(); + var params = context.getParams(); + this.clientToHold = params.getCap(); + return READY_NOW; + } + + @Override + protected CompletableFuture callHeld(CallContext context) { + this.callCount.inc(); + var request = this.clientToHold.fooRequest(); + request.getParams().setI(123); + request.getParams().setJ(true); + return request.send().thenAccept(response -> { + Assert.assertEquals("foo", response.getX().toString()); + context.getResults().setS("bar"); + }); + } + + @Override + protected CompletableFuture getHeld(CallContext context) { + this.callCount.inc(); + var result = context.getResults(); + result.setCap(this.clientToHold); + return READY_NOW; + } + + @Override + protected CompletableFuture neverReturn(CallContext context) { + this.callCount.inc(); + var cap = context.getParams().getCap(); + context.getResults().setCapCopy(cap); + context.allowCancellation(); + return new CompletableFuture<>().thenAccept(void_ -> { + // Ensure that the cap is used inside the lambda. + System.out.println(cap); + }); + } + + @Override + protected CompletableFuture getNull(CallContext context) { + return READY_NOW; + } + } + + static class TestTailCalleeImpl extends Test.TestTailCallee.Server { + + private final Counter count; + private final CompletableFuture releaseMe; + + public TestTailCalleeImpl(Counter count) { + this(count, READY_NOW); + } + + public TestTailCalleeImpl(Counter count, CompletableFuture releaseMe) { + this.count = count; + this.releaseMe = releaseMe; + } + + @Override + protected CompletableFuture foo(CallContext context) { + this.count.inc(); + + var params = context.getParams(); + var results = context.getResults(); + + results.setI(params.getI()); + results.setT(params.getT()); + results.setC(new TestCallOrderImpl()); + return releaseMe; + } + } + + static class TestCallOrderImpl extends Test.TestCallOrder.Server { + + private int count = 0; + + @Override + protected CompletableFuture getCallSequence(CallContext context) { + var result = context.getResults(); + result.setN(this.count++); + return READY_NOW; + } + + public int getCount() { + return this.count; + } + } + + static class TestPipelineImpl extends Test.TestPipeline.Server { + + final Counter callCount; + + TestPipelineImpl(Counter callCount) { + this.callCount = callCount; + } + + @Override + protected CompletableFuture getCap(CallContext ctx) { + this.callCount.inc(); + var params = ctx.getParams(); + Assert.assertEquals(234, params.getN()); + var cap = params.getInCap(); + ctx.releaseParams(); + + var request = cap.fooRequest(); + var fooParams = request.getParams(); + fooParams.setI(123); + fooParams.setJ(true); + + return request.send().thenAccept(response -> { + Assert.assertEquals("foo", response.getX().toString()); + var result = ctx.getResults(); + result.setS("bar"); + + Test.TestExtends.Server server = new TestExtendsImpl(this.callCount); + result.initOutBox().setCap(server); + }); + } + + @Override + protected CompletableFuture getAnyCap(CallContext context) { + this.callCount.inc(); + var params = context.getParams(); + Assert.assertEquals(234, params.getN()); + + var cap = params.getInCap(); + context.releaseParams(); + + var request = new Test.TestInterface.Client(cap).fooRequest(); + request.getParams().setI(123); + request.getParams().setJ(true); + + return request.send().thenAccept(response -> { + Assert.assertEquals("foo", response.getX().toString()); + + var result = context.getResults(); + result.setS("bar"); + result.initOutBox().setCap(new TestExtendsImpl(callCount)); + }); + } + } + + static class TestCapDestructor extends Test.TestInterface.Server { + private final Counter dummy = new Counter(); + private final TestInterfaceImpl impl = new TestInterfaceImpl(dummy); + + @Override + protected CompletableFuture foo(CallContext context) { + return this.impl.foo(context); + } + } + + static class TestStreamingImpl + extends Test.TestStreaming.Server { + + public int iSum = 0; + public int jSum = 0; + CompletableFuture fulfiller; + boolean jShouldThrow = false; + + @Override + protected CompletableFuture doStreamI(StreamingCallContext context) { + iSum += context.getParams().getI(); + fulfiller = new CompletableFuture<>(); + return fulfiller; + } + + @Override + protected CompletableFuture doStreamJ(StreamingCallContext context) { + context.allowCancellation(); + jSum += context.getParams().getJ(); + if (jShouldThrow) { + return CompletableFuture.failedFuture(RpcException.failed("throw requested")); + } + fulfiller = new CompletableFuture<>(); + return fulfiller; + } + + @Override + protected CompletableFuture finishStream(CallContext context) { + var results = context.getResults(); + results.setTotalI(iSum); + results.setTotalJ(jSum); + return READY_NOW; + } + } +} + diff --git a/runtime-rpc/src/test/java/org/capnproto/TwoPartyTest.java b/runtime-rpc/src/test/java/org/capnproto/TwoPartyTest.java new file mode 100644 index 00000000..39cb1098 --- /dev/null +++ b/runtime-rpc/src/test/java/org/capnproto/TwoPartyTest.java @@ -0,0 +1,264 @@ +package org.capnproto; + +import org.capnproto.rpctest.*; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; + +import java.io.IOException; +import java.nio.channels.AsynchronousByteChannel; +import java.nio.channels.AsynchronousChannelGroup; +import java.nio.channels.AsynchronousServerSocketChannel; +import java.nio.channels.AsynchronousSocketChannel; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Consumer; + +public class TwoPartyTest { + + static final class PipeThread { + Thread thread; + AsynchronousSocketChannel channel; + + } + + private AsynchronousChannelGroup group; + + PipeThread newPipeThread(Consumer startFunc) throws Exception { + var pipeThread = new PipeThread(); + var serverAcceptSocket = AsynchronousServerSocketChannel.open(this.group); + serverAcceptSocket.bind(null); + var clientSocket = AsynchronousSocketChannel.open(); + + pipeThread.thread = new Thread(() -> { + try { + var serverSocket = serverAcceptSocket.accept().get(); + startFunc.accept(serverSocket); + } catch (InterruptedException | ExecutionException exc) { + exc.printStackTrace(); + } + }); + pipeThread.thread.start(); + pipeThread.thread.setName("TwoPartyTest server"); + + clientSocket.connect(serverAcceptSocket.getLocalAddress()).get(); + pipeThread.channel = clientSocket; + return pipeThread; + } + + PipeThread runServer(Capability.Server bootstrapInterface) throws Exception { + return runServer(new Capability.Client(bootstrapInterface)); + } + + PipeThread runServer(Capability.Client bootstrapInterface) throws Exception { + return newPipeThread(channel -> { + var network = new TwoPartyVatNetwork(channel, RpcTwoPartyProtocol.Side.SERVER); + var system = new RpcSystem<>(network, bootstrapInterface); + system.start(); + network.onDisconnect().join(); + }); + } + + @Before + public void setUp() throws IOException { + this.group = AsynchronousChannelGroup.withThreadPool(Executors.newFixedThreadPool(5)); + } + + @After + public void tearDown() { + this.group.shutdown(); + } + + @org.junit.Test + public void testNullCap() throws Exception { + var pipe = runServer(new Capability.Client()); + var rpcClient = new TwoPartyClient(pipe.channel); + var client = rpcClient.bootstrap(); + var resolved = client.whenResolved(); + rpcClient.runUntil(resolved).join(); + } + + @org.junit.Test + public void testBasic() throws Exception { + var callCount = new Counter(); + var pipe = runServer(new RpcTestUtil.TestInterfaceImpl(callCount)); + var rpcClient = new TwoPartyClient(pipe.channel); + var client = new Test.TestInterface.Client(rpcClient.bootstrap()); + var request1 = client.fooRequest(); + request1.getParams().setI(123); + request1.getParams().setJ(true); + + var promise1 = request1.send(); + + var request2 = client.bazRequest(); + RpcTestUtil.initTestMessage(request2.getParams().initS()); + var promise2 = request2.send(); + + boolean barFailed = false; + var request3 = client.barRequest(); + var promise3 = request3.send() + .thenAccept(results -> Assert.fail("Expected bar() to fail")) + .exceptionally(exc -> null); + + var response1 = rpcClient.runUntil(promise1).join(); + Assert.assertEquals("foo", response1.getX().toString()); + + rpcClient.runUntil(promise2).join(); + rpcClient.runUntil(promise3).join(); + + Assert.assertEquals(2, callCount.value()); + } + + @org.junit.Test + public void testDisconnect() { + //this.serverSocket.shutdownOutput(); + //this.serverNetwork.close(); + //this.serverNetwork.onDisconnect().join(); + } + + @org.junit.Test + public void testPipelining() throws Exception { + var callCount = new Counter(); + var chainedCallCount = new Counter(); + var pipe = runServer(new RpcTestUtil.TestPipelineImpl(callCount)); + var rpcClient = new TwoPartyClient(pipe.channel); + var client = new Test.TestPipeline.Client(rpcClient.bootstrap()); + + { + var request = client.getCapRequest(); + request.getParams().setN(234); + request.getParams().setInCap(new RpcTestUtil.TestInterfaceImpl(chainedCallCount)); + + var promise = request.send(); + + var pipelineRequest = promise.getOutBox().getCap().fooRequest(); + pipelineRequest.getParams().setI(321); + + var pipelinePromise = pipelineRequest.send(); + + var pipelineRequest2 = new Test.TestExtends.Client(promise.getOutBox().getCap()).graultRequest(); + var pipelinePromise2 = pipelineRequest2.send(); + + promise = null; + + //Assert.assertEquals(0, chainedCallCount.value()); + + var response = rpcClient.runUntil(pipelinePromise).join(); + Assert.assertEquals("bar", response.getX().toString()); + + var response2 = rpcClient.runUntil(pipelinePromise2).join(); + RpcTestUtil.checkTestMessage(response2); + + Assert.assertEquals(1, chainedCallCount.value()); + } + + // disconnect the client + ((AsynchronousSocketChannel)pipe.channel).shutdownOutput(); + rpcClient.runUntil(rpcClient.onDisconnect()).join(); + + { + // Use the now-broken capability. + var request = client.getCapRequest(); + request.getParams().setN(234); + request.getParams().setInCap(new RpcTestUtil.TestInterfaceImpl(chainedCallCount)); + + var promise = request.send(); + + var pipelineRequest = promise.getOutBox().getCap().fooRequest(); + pipelineRequest.getParams().setI(321); + var pipelinePromise = pipelineRequest.send(); + + var pipelineRequest2 = new Test.TestExtends.Client(promise.getOutBox().getCap()).graultRequest(); + var pipelinePromise2 = pipelineRequest2.send(); + + Assert.assertThrows(Exception.class, pipelinePromise::join); + Assert.assertThrows(Exception.class, pipelinePromise2::join); + + Assert.assertEquals(3, callCount.value()); + Assert.assertEquals(1, chainedCallCount.value()); + } + } + + @org.junit.Test + public void testAbort() { + + } +/* + @org.junit.Test + public void testBasicCleanup() throws ExecutionException, InterruptedException, TimeoutException { + var server = new RpcSystem<>(this.serverNetwork, new TestCap0Impl()); + var demo = new Demo.TestCap0.Client(this.client.bootstrap()); + var request = demo.testMethod0Request(); + var params = request.getParams(); + params.setParam0(4321); + var response = request.send(); + response.get(); + Assert.assertTrue(response.isDone()); + var results = response.get(); + Assert.assertEquals(params.getParam0(), results.getResult0()); + + demo = null; + } + + @org.junit.Test + public void testShutdown() throws InterruptedException, IOException { + var server = new RpcSystem<>(this.serverNetwork, new TestCap0Impl()); + var demo = new Demo.TestCap0.Client(this.client.bootstrap()); + this.clientSocket.shutdownOutput(); + serverThread.join(); + } + + @org.junit.Test + public void testCallThrows() throws ExecutionException, InterruptedException { + var impl = new Demo.TestCap0.Server() { + public CompletableFuture testMethod0(CallContext ctx) { + return CompletableFuture.failedFuture(new RuntimeException("Call to testMethod0 failed")); + } + public CompletableFuture testMethod1(CallContext ctx) { + return CompletableFuture.completedFuture(null); + } + }; + + var rpcSystem = new RpcSystem<>(this.serverNetwork, impl); + + var demoClient = new Demo.TestCap0.Client(this.client.bootstrap()); + { + var request = demoClient.testMethod0Request(); + var response = request.send(); + while (!response.isDone()) { + CompletableFuture.anyOf(response).exceptionally(exc -> { return null; }); + } + Assert.assertTrue(response.isCompletedExceptionally()); + } + + // test that the system is still valid + { + var request = demoClient.testMethod1Request(); + var response = request.send(); + response.get(); + Assert.assertFalse(response.isCompletedExceptionally()); + } + } + + @org.junit.Test + public void testReturnCap() throws ExecutionException, InterruptedException { + // send a capability back from the server to the client + var capServer = new TestCap0Impl(); + var rpcSystem = new RpcSystem<>(this.serverNetwork, capServer); + var demoClient = new Demo.TestCap0.Client(this.client.bootstrap()); + var request = demoClient.testMethod1Request(); + var response = request.send(); + response.get(); + Assert.assertTrue(response.isDone()); + + var results = response.get(); + var cap0 = results.getResult0(); + Assert.assertFalse(cap0.isNull()); + var cap1 = results.getResult1(); + Assert.assertFalse(cap1.isNull()); + var cap2 = results.getResult2(); + Assert.assertFalse(cap2.isNull()); + } + */ +} diff --git a/runtime-rpc/src/test/logging.properties b/runtime-rpc/src/test/logging.properties new file mode 100644 index 00000000..73307ed0 --- /dev/null +++ b/runtime-rpc/src/test/logging.properties @@ -0,0 +1,2 @@ +handlers = java.util.logging.FileHandler +java.util.logging.ConsoleHandler.level = ALL \ No newline at end of file diff --git a/runtime-rpc/src/test/schema/test.capnp b/runtime-rpc/src/test/schema/test.capnp new file mode 100644 index 00000000..06e7a18f --- /dev/null +++ b/runtime-rpc/src/test/schema/test.capnp @@ -0,0 +1,262 @@ +@0xb365fb00cc89383b; + +using Java = import "/capnp/java.capnp"; +$Java.package("org.capnproto.rpctest"); +$Java.outerClassname("Test"); + +enum TestEnum { + foo @0; + bar @1; + baz @2; + qux @3; + quux @4; + corge @5; + grault @6; + garply @7; +} + +struct TestAllTypes { + voidField @0 : Void; + boolField @1 : Bool; + int8Field @2 : Int8; + int16Field @3 : Int16; + int32Field @4 : Int32; + int64Field @5 : Int64; + uInt8Field @6 : UInt8; + uInt16Field @7 : UInt16; + uInt32Field @8 : UInt32; + uInt64Field @9 : UInt64; + float32Field @10 : Float32; + float64Field @11 : Float64; + textField @12 : Text; + dataField @13 : Data; + structField @14 : TestAllTypes; + enumField @15 : TestEnum; + interfaceField @16 : Void; # TODO + + voidList @17 : List(Void); + boolList @18 : List(Bool); + int8List @19 : List(Int8); + int16List @20 : List(Int16); + int32List @21 : List(Int32); + int64List @22 : List(Int64); + uInt8List @23 : List(UInt8); + uInt16List @24 : List(UInt16); + uInt32List @25 : List(UInt32); + uInt64List @26 : List(UInt64); + float32List @27 : List(Float32); + float64List @28 : List(Float64); + textList @29 : List(Text); + dataList @30 : List(Data); + structList @31 : List(TestAllTypes); + enumList @32 : List(TestEnum); + interfaceList @33 : List(Void); # TODO +} + +struct TestAnyPointer { + anyPointerField @0 :AnyPointer; + + # Do not add any other fields here! Some tests rely on anyPointerField being the last pointer + # in the struct. +} + +struct TestAnyOthers { + anyStructField @0 :AnyStruct; + anyListField @1 :AnyList; + capabilityField @2 :Capability; +} + +struct TestOutOfOrder { + foo @3 :Text; + bar @2 :Text; + baz @8 :Text; + qux @0 :Text; + quux @6 :Text; + corge @4 :Text; + grault @1 :Text; + garply @7 :Text; + waldo @5 :Text; +} + +struct TestUnnamedUnion { + before @0 :Text; + + union { + foo @1 :UInt16; + bar @3 :UInt32; + } + + middle @2 :UInt16; + + after @4 :Text; +} + +struct TestUnionInUnion { + # There is no reason to ever do this. + outer :union { + inner :union { + foo @0 :Int32; + bar @1 :Int32; + } + baz @2 :Int32; + } +} + +struct TestGroups { + groups :union { + foo :group { + corge @0 :Int32; + grault @2 :Int64; + garply @8 :Text; + } + bar :group { + corge @3 :Int32; + grault @4 :Text; + garply @5 :Int64; + } + baz :group { + corge @1 :Int32; + grault @6 :Text; + garply @7 :Text; + } + } +} + + +struct TestSturdyRef { + hostId @0 :TestSturdyRefHostId; + objectId @1 :AnyPointer; +} + +struct TestSturdyRefHostId { + host @0 :Text; +} + +struct TestSturdyRefObjectId { + tag @0 :Tag; + enum Tag { + testInterface @0; + testExtends @1; + testPipeline @2; + testTailCallee @3; + testTailCaller @4; + testMoreStuff @5; + } +} + +struct TestProvisionId {} +struct TestRecipientId {} +struct TestThirdPartyCapId {} +struct TestJoinResult {} + +interface TestInterface { + foo @0 (i :UInt32, j :Bool) -> (x :Text); + bar @1 () -> (); + baz @2 (s: TestAllTypes); +} + +interface TestExtends extends(TestInterface) { + qux @0 (); + corge @1 TestAllTypes -> (); + grault @2 () -> TestAllTypes; +} + +interface TestExtends2 extends(TestExtends) {} + +interface TestPipeline { + getCap @0 (n: UInt32, inCap :TestInterface) -> (s: Text, outBox :Box); + testPointers @1 (cap :TestInterface, obj :AnyPointer, list :List(TestInterface)) -> (); + getAnyCap @2 (n: UInt32, inCap :Capability) -> (s: Text, outBox :AnyBox); + + struct Box { + cap @0 :TestInterface; + } + struct AnyBox { + cap @0 :Capability; + } +} + +interface TestCallOrder { + getCallSequence @0 (expected: UInt32) -> (n: UInt32); + # First call returns 0, next returns 1, ... + # + # The input `expected` is ignored but useful for disambiguating debug logs. +} + +interface TestTailCallee { + struct TailResult { + i @0 :UInt32; + t @1 :Text; + c @2 :TestCallOrder; + } + + foo @0 (i :Int32, t :Text) -> TailResult; +} + +interface TestTailCaller { + foo @0 (i :Int32, callee :TestTailCallee) -> TestTailCallee.TailResult; +} + +interface TestStreaming { + doStreamI @0 (i: UInt32) -> stream; + doStreamJ @1 (j: UInt32) -> stream; + finishStream @2 () -> (totalI :UInt32, totalJ: UInt32); + # Test streaming. finishStream() returns the totals of the values streamed to the other calls. +} + +interface TestHandle {} + +interface TestMoreStuff extends(TestCallOrder) { + # Catch-all type that contains lots of testing methods. + + callFoo @0 (cap :TestInterface) -> (s: Text); + # Call `cap.foo()`, check the result, and return "bar". + + callFooWhenResolved @1 (cap :TestInterface) -> (s: Text); + # Like callFoo but waits for `cap` to resolve first. + + neverReturn @2 (cap :TestInterface) -> (capCopy :TestInterface); + # Doesn't return. You should cancel it. + + hold @3 (cap :TestInterface) -> (); + # Returns immediately but holds on to the capability. + + callHeld @4 () -> (s: Text); + # Calls the capability previously held using `hold` (and keeps holding it). + + getHeld @5 () -> (cap :TestInterface); + # Returns the capability previously held using `hold` (and keeps holding it). + + echo @6 (cap :TestCallOrder) -> (cap :TestCallOrder); + # Just returns the input cap. + + expectCancel @7 (cap :TestInterface) -> (); + # evalLater()-loops forever, holding `cap`. Must be canceled. + + methodWithDefaults @8 (a :Text, b :UInt32 = 123, c :Text = "foo") -> (d :Text, e :Text = "bar"); + + methodWithNullDefault @12 (a :Text, b :TestInterface = null); + + getHandle @9 () -> (handle :TestHandle); + # Get a new handle. Tests have an out-of-band way to check the current number of live handles, so + # this can be used to test garbage collection. + + getNull @10 () -> (nullCap :TestMoreStuff); + # Always returns a null capability. + + getEnormousString @11 () -> (str :Text); + # Attempts to return an 100MB string. Should always fail. + + writeToFd @13 (fdCap1 :TestInterface, fdCap2 :TestInterface) + -> (fdCap3 :TestInterface, secondFdPresent :Bool); + # Expects fdCap1 and fdCap2 wrap socket file descriptors. Writes "foo" to the first and "bar" to + # the second. Also creates a socketpair, writes "baz" to one end, and returns the other end. +} + +struct TestGenerics(Foo, Bar) { + foo @0 :Foo; + rev @1 :TestGenerics(Bar, Foo); + + interface Interface(Qux) { + } +} diff --git a/runtime/pom.xml b/runtime/pom.xml index d49cd2f9..ed8dad47 100644 --- a/runtime/pom.xml +++ b/runtime/pom.xml @@ -28,11 +28,16 @@ David Renshaw https://github.com/dwrensha + + vaci + Vaci Koblizek + https://github.com/vaci + UTF-8 - 1.8 - 1.8 + 11 + 11 @@ -62,6 +67,8 @@ 3.6.2 -Xlint:unchecked + 11 + 11 @@ -69,12 +76,12 @@ - jdk9FF + jdk11FF - (1.8,) + (11,) - 8 + 11 diff --git a/runtime/src/main/java/org/capnproto/AnyList.java b/runtime/src/main/java/org/capnproto/AnyList.java new file mode 100644 index 00000000..5524594d --- /dev/null +++ b/runtime/src/main/java/org/capnproto/AnyList.java @@ -0,0 +1,58 @@ +package org.capnproto; + +public class AnyList { + + public static final class Factory extends ListFactory { + + Factory() { + super(ElementSize.VOID); + } + + public final Reader asReader(Builder builder) { + return builder.asReader(); + } + + @Override + public Builder constructBuilder(SegmentBuilder segment, int ptr, int elementCount, int step, int structDataSize, short structPointerCount) { + return new Builder(segment, ptr, elementCount, step, structDataSize, structPointerCount); + } + + @Override + public Reader constructReader(SegmentReader segment, int ptr, int elementCount, int step, int structDataSize, short structPointerCount, int nestingLimit) { + return new Reader(segment, ptr, elementCount, step, structDataSize, structPointerCount, nestingLimit); + } + } + + public static final Factory factory = new Factory(); + + public static final ListList.Factory listFactory = + new ListList.Factory<>(factory); + + public static final class Builder extends ListBuilder { + Builder(SegmentBuilder segment, int ptr, int elementCount, int step, int structDataSize, short structPointerCount){ + super(segment, ptr, elementCount, step, structDataSize, structPointerCount); + } + + public final Reader asReader() { + return new Reader(this.segment, this.ptr, this.elementCount, this.step, this.structDataSize, this.structPointerCount, 0x7fffffff); + } + + public final T initAs(Factory factory) { + return factory.constructBuilder(this.segment, this.ptr, this.elementCount, this.step, this.structDataSize, this.structPointerCount); + } + + public final T setAs(Factory factory) { + return factory.constructBuilder(this.segment, this.ptr, this.elementCount, this.step, this.structDataSize, this.structPointerCount); + } + } + + public static final class Reader extends ListReader { + Reader(SegmentReader segment, int ptr, int elementCount, int step, int structDataSize, short structPointerCount, int nestingLimit){ + super(segment, ptr, elementCount, step, structDataSize, structPointerCount, nestingLimit); + } + + public final T getAs(Factory factory) { + return factory.constructReader(this.segment, this.ptr, this.elementCount, this.step, this.structDataSize, this.structPointerCount, this.nestingLimit); + } + } +} diff --git a/runtime/src/main/java/org/capnproto/AnyPointer.java b/runtime/src/main/java/org/capnproto/AnyPointer.java index 12c0e6ce..1094d3ac 100644 --- a/runtime/src/main/java/org/capnproto/AnyPointer.java +++ b/runtime/src/main/java/org/capnproto/AnyPointer.java @@ -24,32 +24,31 @@ public final class AnyPointer { public static final class Factory implements PointerFactory, - SetPointerBuilder - { - public final Reader fromPointerReader(SegmentReader segment, int pointer, int nestingLimit) { - return new Reader(segment, pointer, nestingLimit); + SetPointerBuilder { + public final Reader fromPointerReader(SegmentReader segment, CapTableReader capTable, int pointer, int nestingLimit) { + return new Reader(segment, capTable, pointer, nestingLimit); } - public final Builder fromPointerBuilder(SegmentBuilder segment, int pointer) { - return new Builder(segment, pointer); + public final Builder fromPointerBuilder(SegmentBuilder segment, CapTableBuilder capTable, int pointer) { + return new Builder(segment, capTable, pointer); } - public final Builder initFromPointerBuilder(SegmentBuilder segment, int pointer, int elementCount) { - Builder result = new Builder(segment, pointer); + public final Builder initFromPointerBuilder(SegmentBuilder segment, CapTableBuilder capTable, int pointer, int elementCount) { + Builder result = new Builder(segment, capTable, pointer); result.clear(); return result; } - public void setPointerBuilder(SegmentBuilder segment, int pointer, Reader value) { + public void setPointerBuilder(SegmentBuilder segment, CapTableBuilder capTable, int pointer, Reader value) { if (value.isNull()) { - WireHelpers.zeroObject(segment, pointer); + WireHelpers.zeroObject(segment, capTable, pointer); WireHelpers.zeroPointerAndFars(segment, pointer); } else { - WireHelpers.copyPointer(segment, pointer, value.segment, value.pointer, value.nestingLimit); + WireHelpers.copyPointer(segment, capTable, pointer, value.segment, value.capTable, value.pointer, value.nestingLimit); } } } public static final Factory factory = new Factory(); - public final static class Reader { + public final static class Reader extends CapTableReader.ReaderContext { final SegmentReader segment; final int pointer; // offset in words final int nestingLimit; @@ -60,16 +59,41 @@ public Reader(SegmentReader segment, int pointer, int nestingLimit) { this.nestingLimit = nestingLimit; } + public Reader(SegmentReader segment, CapTableReader capTable, int pointer, int nestingLimit) { + this.segment = segment; + this.pointer = pointer; + this.nestingLimit = nestingLimit; + this.capTable = capTable; + } + + final Reader imbue(CapTableReader capTable) { + Reader result = new Reader(segment, pointer, nestingLimit); + result.capTable = capTable; + return result; + } + public final boolean isNull() { return WirePointer.isNull(this.segment.buffer.getLong(this.pointer * Constants.BYTES_PER_WORD)); } public final T getAs(FromPointerReader factory) { - return factory.fromPointerReader(this.segment, this.pointer, this.nestingLimit); + return factory.fromPointerReader(this.segment, this.capTable, this.pointer, this.nestingLimit); + } + + public final ClientHook getPipelinedCap(short[] ops) { + AnyPointer.Reader any = this; + + for (short pointerIndex: ops) { + if (pointerIndex >= 0) { + StructReader reader = WireHelpers.readStructPointer(any.segment, any.capTable, any.pointer, null, 0, any.nestingLimit); + any = reader._getPointerField(AnyPointer.factory, pointerIndex); + } + } + return WireHelpers.readCapabilityPointer(any.segment, any.capTable, any.pointer, 0); } } - public static final class Builder { + public static final class Builder extends CapTableBuilder.BuilderContext { final SegmentBuilder segment; final int pointer; @@ -78,34 +102,84 @@ public Builder(SegmentBuilder segment, int pointer) { this.pointer = pointer; } + Builder(SegmentBuilder segment, CapTableBuilder capTable, int pointer) { + this.segment = segment; + this.pointer = pointer; + this.capTable = capTable; + } + + final Builder imbue(CapTableBuilder capTable) { + return new Builder(segment, capTable, pointer); + } + public final boolean isNull() { return WirePointer.isNull(this.segment.buffer.getLong(this.pointer * Constants.BYTES_PER_WORD)); } public final T getAs(FromPointerBuilder factory) { - return factory.fromPointerBuilder(this.segment, this.pointer); + return factory.fromPointerBuilder(this.segment, this.capTable, this.pointer); } public final T initAs(FromPointerBuilder factory) { - return factory.initFromPointerBuilder(this.segment, this.pointer, 0); + return factory.initFromPointerBuilder(this.segment, this.capTable, this.pointer, 0); } public final T initAs(FromPointerBuilder factory, int elementCount) { - return factory.initFromPointerBuilder(this.segment, this.pointer, elementCount); + return factory.initFromPointerBuilder(this.segment, this.capTable, this.pointer, elementCount); } public final void setAs(SetPointerBuilder factory, U reader) { - factory.setPointerBuilder(this.segment, this.pointer, reader); + factory.setPointerBuilder(this.segment, this.capTable, this.pointer, reader); } public final Reader asReader() { - return new Reader(segment, pointer, java.lang.Integer.MAX_VALUE); + return new Reader(segment, this.capTable, pointer, java.lang.Integer.MAX_VALUE); } public final void clear() { - WireHelpers.zeroObject(this.segment, this.pointer); + WireHelpers.zeroObject(this.segment, this.capTable, this.pointer); this.segment.buffer.putLong(this.pointer * 8, 0L); } } + public static final class Pipeline + implements org.capnproto.Pipeline { + + final PipelineHook hook; + private final short[] ops; + + public Pipeline(PipelineHook hook) { + this(hook, new short[0]); + } + + Pipeline(PipelineHook hook, short[] ops) { + this.hook = hook; + this.ops = ops; + } + + @Override + public Pipeline typelessPipeline() { + return this; + } + + @Override + public void cancel(Throwable exc) { + this.hook.cancel(exc); + } + + public Pipeline noop() { + return new Pipeline(this.hook, this.ops.clone()); + } + + public ClientHook asCap() { + return this.hook.getPipelinedCap(ops); + } + + public Pipeline getPointerField(short pointerIndex) { + short[] newOps = new short[this.ops.length + 1]; + System.arraycopy(this.ops, 0, newOps, 0, this.ops.length); + newOps[this.ops.length] = pointerIndex; + return new Pipeline(this.hook, newOps); + } + } } diff --git a/runtime/src/main/java/org/capnproto/AnyStruct.java b/runtime/src/main/java/org/capnproto/AnyStruct.java new file mode 100644 index 00000000..0e7a4a31 --- /dev/null +++ b/runtime/src/main/java/org/capnproto/AnyStruct.java @@ -0,0 +1,56 @@ +package org.capnproto; + +public class AnyStruct { + + public static final org.capnproto.StructSize STRUCT_SIZE = new org.capnproto.StructSize((short)0,(short)0); + + public static final class Factory extends org.capnproto.StructFactory { + public Factory() { + } + public final Reader constructReader(org.capnproto.SegmentReader segment, int data,int pointers, int dataSize, short pointerCount, int nestingLimit) { + return new Reader(segment,data,pointers,dataSize,pointerCount,nestingLimit); + } + public final Builder constructBuilder(org.capnproto.SegmentBuilder segment, int data,int pointers, int dataSize, short pointerCount) { + return new Builder(segment, data, pointers, dataSize, pointerCount); + } + public final org.capnproto.StructSize structSize() { + return AnyStruct.STRUCT_SIZE; + } + public final Reader asReader(Builder builder) { + return builder.asReader(); + } + } + + public static final Factory factory = new Factory(); + + public static final org.capnproto.StructList.Factory listFactory = + new org.capnproto.StructList.Factory<>(factory); + + public static final class Builder extends org.capnproto.StructBuilder { + Builder(org.capnproto.SegmentBuilder segment, int data, int pointers,int dataSize, short pointerCount){ + super(segment, data, pointers, dataSize, pointerCount); + } + public final Reader asReader() { + return new Reader(segment, data, pointers, dataSize, pointerCount, 0x7fffffff); + } + + public final T initAs(StructBuilder.Factory factory) { + return factory.constructBuilder(this.segment, this.capTable, this.data, this.pointers, this.dataSize, this.pointerCount); + } + + public final T setAs(StructBuilder.Factory factory) { + return factory.constructBuilder(this.segment, this.capTable, this.data, this.pointers, this.dataSize, this.pointerCount); + } + + } + + public static final class Reader extends org.capnproto.StructReader { + Reader(org.capnproto.SegmentReader segment, int data, int pointers,int dataSize, short pointerCount, int nestingLimit){ + super(segment, data, pointers, dataSize, pointerCount, nestingLimit); + } + + public final T getAs(StructReader.Factory factory) { + return factory.constructReader(this.segment, this.capTable, this.data, this.pointers, this.dataSize, this.pointerCount, this.nestingLimit); + } + } +} diff --git a/runtime/src/main/java/org/capnproto/BootstrapFactory.java b/runtime/src/main/java/org/capnproto/BootstrapFactory.java new file mode 100644 index 00000000..a3195d45 --- /dev/null +++ b/runtime/src/main/java/org/capnproto/BootstrapFactory.java @@ -0,0 +1,5 @@ +package org.capnproto; + +public interface BootstrapFactory { + Capability.Client createFor(VatId clientId); +} \ No newline at end of file diff --git a/runtime/src/main/java/org/capnproto/BuilderArena.java b/runtime/src/main/java/org/capnproto/BuilderArena.java index 4f063cd7..290a6aac 100644 --- a/runtime/src/main/java/org/capnproto/BuilderArena.java +++ b/runtime/src/main/java/org/capnproto/BuilderArena.java @@ -24,6 +24,7 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; +import java.util.List; public final class BuilderArena implements Arena { public enum AllocationStrategy { @@ -38,6 +39,34 @@ public enum AllocationStrategy { public final ArrayList segments; private final Allocator allocator; + private final CapTableBuilder localCapTable = new CapTableBuilder() { + + private final List capTable = new ArrayList<>(); + + @Override + public int injectCap(ClientHook cap) { + int result = this.capTable.size(); + this.capTable.add(cap); + return result; + } + + @Override + public void dropCap(int index) { + if (index < this.capTable.size()) { + assert false : "Invalid capability descriptor in message."; + return; + } + this.capTable.set(index, null); + } + + @Override + public ClientHook extractCap(int index) { + return index < this.capTable.size() + ? this.capTable.get(index) + : null; + } + }; + public BuilderArena(int firstSegmentSizeWords, AllocationStrategy allocationStrategy) { this.segments = new ArrayList(); { @@ -64,7 +93,17 @@ public BuilderArena(Allocator allocator, ByteBuffer firstSegment) { this.allocator = allocator; } - /** + /** + * Return a CapTableBuilder that merely implements local loopback. That is, you can set + * capabilities, then read the same capabilities back, but there is no intent ever to transmit + * these capabilities. A MessageBuilder that isn't imbued with some other CapTable uses this + * by default. + */ + public CapTableBuilder getLocalCapTable() { + return this.localCapTable; + } + + /** * Constructs a BuilderArena from a ReaderArena and uses the size of the largest segment * as the next allocation size. */ diff --git a/runtime/src/main/java/org/capnproto/BuilderCapabilityTable.java b/runtime/src/main/java/org/capnproto/BuilderCapabilityTable.java new file mode 100644 index 00000000..5996c472 --- /dev/null +++ b/runtime/src/main/java/org/capnproto/BuilderCapabilityTable.java @@ -0,0 +1,34 @@ +package org.capnproto; + +import java.util.ArrayList; +import java.util.List; + +class BuilderCapabilityTable implements CapTableBuilder { + + private final List table = new ArrayList<>(); + + BuilderCapabilityTable() { + } + + @Override + public ClientHook extractCap(int index) { + return table.get(index); + } + + @Override + public int injectCap(ClientHook cap) { + int index = table.size(); + table.add(cap); + return index; + } + + @Override + public void dropCap(int index) { + table.set(index, null); + } + + @Override + public ClientHook[] getTable() { + return table.toArray(new ClientHook[0]); + } +} diff --git a/runtime/src/main/java/org/capnproto/CallContext.java b/runtime/src/main/java/org/capnproto/CallContext.java new file mode 100644 index 00000000..e27925fa --- /dev/null +++ b/runtime/src/main/java/org/capnproto/CallContext.java @@ -0,0 +1,46 @@ +package org.capnproto; + +import java.util.concurrent.CompletableFuture; + +public final class CallContext { + + private final FromPointerReader paramsFactory; + private final FromPointerBuilder resultsFactory; + private final CallContextHook hook; + + public CallContext(FromPointerReader paramsFactory, + FromPointerBuilder resultsFactory, + CallContextHook hook) { + this.paramsFactory = paramsFactory; + this.resultsFactory = resultsFactory; + this.hook = hook; + } + + public final Params getParams() { + return this.hook.getParams().getAs(paramsFactory); + } + + public final void releaseParams() { + this.hook.releaseParams(); + } + + public final Results getResults() { + return this.hook.getResults().getAs(resultsFactory); + } + + public final Results initResults() { + return this.hook.getResults().initAs(resultsFactory); + } + + public final CompletableFuture tailCall(Request tailRequest) { + return this.hook.tailCall(tailRequest.getHook()); + } + + public final void allowCancellation() { + this.hook.allowCancellation(); + } + + public final CallContextHook getHook() { + return this.hook; + } +} diff --git a/runtime/src/main/java/org/capnproto/CallContextHook.java b/runtime/src/main/java/org/capnproto/CallContextHook.java new file mode 100644 index 00000000..38ffe49c --- /dev/null +++ b/runtime/src/main/java/org/capnproto/CallContextHook.java @@ -0,0 +1,28 @@ +package org.capnproto; + +import java.util.concurrent.CompletableFuture; + +public interface CallContextHook { + + AnyPointer.Reader getParams(); + + void releaseParams(); + + default AnyPointer.Builder getResults() { + return getResults(0); + } + + AnyPointer.Builder getResults(int sizeHint); + + CompletableFuture tailCall(RequestHook request); + + void allowCancellation(); + + CompletableFuture onTailCall(); + + ClientHook.VoidPromiseAndPipeline directTailCall(RequestHook request); + + default CompletableFuture releaseCall() { + return CompletableFuture.completedFuture(null); + } +} diff --git a/runtime/src/main/java/org/capnproto/CapTableBuilder.java b/runtime/src/main/java/org/capnproto/CapTableBuilder.java new file mode 100644 index 00000000..8f137a70 --- /dev/null +++ b/runtime/src/main/java/org/capnproto/CapTableBuilder.java @@ -0,0 +1,16 @@ +package org.capnproto; + +public interface CapTableBuilder extends CapTableReader { + + class BuilderContext { + public CapTableBuilder capTable; + } + + int injectCap(ClientHook cap); + + void dropCap(int index); + + default ClientHook[] getTable() { + return new ClientHook[0]; + } +} diff --git a/runtime/src/main/java/org/capnproto/CapTableReader.java b/runtime/src/main/java/org/capnproto/CapTableReader.java new file mode 100644 index 00000000..b6d6f8de --- /dev/null +++ b/runtime/src/main/java/org/capnproto/CapTableReader.java @@ -0,0 +1,10 @@ +package org.capnproto; + +public interface CapTableReader { + + class ReaderContext { + public CapTableReader capTable; + } + + ClientHook extractCap(int index); +} diff --git a/runtime/src/main/java/org/capnproto/Capability.java b/runtime/src/main/java/org/capnproto/Capability.java new file mode 100644 index 00000000..30eb6cf6 --- /dev/null +++ b/runtime/src/main/java/org/capnproto/Capability.java @@ -0,0 +1,851 @@ +package org.capnproto; + +import java.io.FileDescriptor; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import static org.capnproto.ClientHook.BROKEN_CAPABILITY_BRAND; +import static org.capnproto.ClientHook.NULL_CAPABILITY_BRAND; + +public final class Capability { + + public static abstract class Factory + implements PointerFactory, + SetPointerBuilder { + + public abstract T newClient(ClientHook hook); + + @Override + public T fromPointerReader(SegmentReader segment, CapTableReader capTable, int pointer, int nestingLimit) { + return newClient( + WireHelpers.readCapabilityPointer(segment, capTable, pointer, 0)); + } + + @Override + public T fromPointerBuilder(SegmentBuilder segment, CapTableBuilder capTable, int pointer) { + return newClient( + WireHelpers.readCapabilityPointer(segment, capTable, pointer, 0)); + } + + @Override + public T initFromPointerBuilder(SegmentBuilder segment, CapTableBuilder capTable, int pointer, int elementCount) { + return newClient( + WireHelpers.readCapabilityPointer(segment, capTable, pointer, 0)); + } + + @Override + public void setPointerBuilder(SegmentBuilder segment, CapTableBuilder capTable, int pointer, T value) { + WireHelpers.setCapabilityPointer(segment, capTable, pointer, value.getHook()); + } + } + + public static class CapabilityFactory extends Factory { + @Override + public Client newClient(ClientHook hook) { + return new Client(hook); + } + } + + public static final CapabilityFactory factory = new CapabilityFactory(); + + public interface ClientBase { + + ClientHook getHook(); + + default CompletableFuture whenResolved() { + return this.getHook().whenResolved(); + } + + /** + * If the capability's server implemented {@link Server.getFd} returning non-null, and all + * RPC links between the client and server support FD passing, returns a file descriptor pointing + * to the same underlying file description as the server did. Returns null if the server provided + * no FD or if FD passing was unavailable at some intervening link. + *

+ * This returns a Promise to handle the case of an unresolved promise capability, e.g. a + * pipelined capability. The promise resolves no later than when the capability settles, i.e. + * the same time `whenResolved()` would complete. + *

+ * The file descriptor will remain open at least as long as the {@link Client} remains alive. + * If you need it to last longer, you will need to `dup()` it. + */ + default CompletableFuture getFd() { + var fd = this.getHook().getFd(); + if (fd != null) { + return CompletableFuture.completedFuture(fd); + } + var promise = this.getHook().whenMoreResolved(); + if (promise != null) { + return promise.thenCompose(newHook -> new Client(newHook).getFd()); + } + return CompletableFuture.completedFuture(null); + } + + default Request newCall(FromPointerBuilder paramsFactory, long interfaceId, short methodId) { + var request = this.getHook().newCall(interfaceId, methodId); + return new Request<>() { + @Override + public FromPointerBuilder getParamsFactory() { + return paramsFactory; + } + + @Override + public Request getTypelessRequest() { + return request; + } + + @Override + public Request getBaseRequest() { + return this; + } + }; + } + + default StreamingRequest newStreamingCall(FromPointerBuilder paramsFactory, long interfaceId, short methodId) { + var request = this.getHook().newCall(interfaceId, methodId); + var streamingRequest = newTypelessStreamingRequest(request.getParams(), request.getHook()); + return new StreamingRequest<>() { + @Override + public FromPointerBuilder getParamsFactory() { + return paramsFactory; + } + + @Override + public StreamingRequest getTypelessRequest() { + return streamingRequest; + } + }; + } + } + + public static class Client implements ClientBase { + + private final ClientHook hook; + + public Client() { + this(newNullCap()); + } + + public Client(Client other) { + this(other.hook); + } + + public Client(T server) { + this(makeLocalClient(server)); + } + + public Client(ClientHook hook) { + this.hook = hook; + } + + public Client(CompletionStage promise) { + this(Capability.newLocalPromiseClient( + promise.thenApply(Client::getHook))); + } + + public Client(Throwable exc) { + this(newBrokenCap(exc)); + } + + public ClientHook getHook() { + return this.hook; + } + + private static ClientHook makeLocalClient(T server) { + return server.makeLocalClient(); + } + } + + public abstract static class Server { + + public static final CompletableFuture READY_NOW = CompletableFuture.completedFuture(null); + private static final Object BRAND = new Object(); + private ClientHook hook; + + ClientHook makeLocalClient() { + return new LocalClient<>(); + } + + ClientHook makeLocalClient(CapabilityServerSet capServerSet) { + return new LocalClient<>(capServerSet); + } + + private final class LocalClient implements ClientHook { + + private CompletableFuture resolveTask; + private ClientHook resolved; + private boolean blocked = false; + private Throwable brokenException; + private final Queue blockedCalls = new ArrayDeque<>(); + private final CapabilityServerSet capServerSet; + + LocalClient() { + this(null); + } + + LocalClient(CapabilityServerSet capServerSet) { + Server.this.hook = this; + this.capServerSet = capServerSet; + var resolveTask = shortenPath(); + if (resolveTask != null) { + this.resolveTask = resolveTask.thenAccept(cap -> { + this.resolved = cap.getHook(); + }); + } + } + + @Override + public Request newCall(long interfaceId, short methodId) { + var hook = new LocalRequest(interfaceId, methodId, this); + var root = hook.message.getRoot(AnyPointer.factory); + return newTypelessRequest(root, hook); + } + + @Override + public VoidPromiseAndPipeline call(long interfaceId, short methodId, CallContextHook ctx) { + // Note this comment from the C++ source: + // + // "We don't want to actually dispatch the call synchronously, because we don't want the callee + // to have any side effects before the promise is returned to the caller. This helps avoid + // race conditions. + // + // So, we do an evalLater() here. + // + // Note also that QueuedClient depends on this evalLater() to ensure that pipelined calls don't + // complete before 'whenMoreResolved()' promises resolve." + // + // As the Java implementation doesn't (currently) have an evalLater() call, we obtain a promise + // from the CallContextHook that will be completed by QueuedClient when appropriate. + var promise = ctx.releaseCall().thenCompose(void_ -> { + if (blocked) { + var blockedCall = new CompletableFuture(); + this.blockedCalls.add(() -> callInternal(interfaceId, methodId, ctx).whenComplete((result, exc) -> { + if (exc == null) { + blockedCall.complete(result); + } + else { + blockedCall.completeExceptionally(exc); + } + })); + return blockedCall; + } + else { + return this.callInternal(interfaceId, methodId, ctx); + } + }); + + var pipelinePromise = promise.thenApply(x -> { + ctx.releaseParams(); + return (PipelineHook) new LocalPipeline(ctx); + }); + + var tailCall = ctx.onTailCall().thenApply(pipeline -> pipeline.hook); + pipelinePromise = tailCall.applyToEither(pipelinePromise, pipeline -> pipeline); + + return new VoidPromiseAndPipeline( + promise, + new QueuedPipeline(pipelinePromise)); + } + + @Override + public ClientHook getResolved() { + return this.resolved; + } + + @Override + public CompletableFuture whenMoreResolved() { + if (this.resolved != null) { + return null; + } + else if (this.resolveTask != null) { + return this.resolveTask.thenApply(void_ -> this.resolved); + } + else { + return null; + } + } + + @Override + public Object getBrand() { + return BRAND; + } + + void unblock() { + this.blocked = false; + while (!this.blocked) { + if (this.blockedCalls.isEmpty()) { + break; + } + var call = this.blockedCalls.remove(); + call.run(); + } + } + + CompletableFuture callInternal(long interfaceId, short methodId, CallContextHook ctx) { + assert !this.blocked; + + if (this.brokenException != null) { + return CompletableFuture.failedFuture(this.brokenException); + } + + var result = dispatchCall( + interfaceId, methodId, + new CallContext<>(AnyPointer.factory, AnyPointer.factory, ctx)); + if (!result.isStreaming()) { + return result.promise; + } + else { + this.blocked = true; + return result.promise.whenComplete((void_, exc) -> { + if (exc != null) { + this.brokenException = exc; + } + this.unblock(); + }); + } + } + + public CompletableFuture getLocalServer(CapabilityServerSet capServerSet) { + if (this.capServerSet == capServerSet) { + if (this.blocked) { + var promise = new CompletableFuture(); + var server = (T)Server.this; + this.blockedCalls.add(() -> promise.complete(server)); + return promise; + } + + var server = (T)Server.this; + return CompletableFuture.completedFuture(server); + } + return null; + } + } + + public Integer getFd() { + return null; + } + + /** + * If this returns non-null, then it is a promise which, when resolved, points to a new + * capability to which future calls can be sent. Use this in cases where an object implementation + * might discover a more-optimized path some time after it starts. + * + * Implementing this (and returning non-null) will cause the capability to be advertised as a + * promise at the RPC protocol level. Once the promise returned by shortenPath() resolves, the + * remote client will receive a `Resolve` message updating it to point at the new destination. + * + * `shortenPath()` can also be used as a hack to shut up the client. If shortenPath() returns + * a promise that resolves to an exception, then the client will be notified that the capability + * is now broken. Assuming the client is using a correct RPC implemnetation, this should cause + * all further calls initiated by the client to this capability to immediately fail client-side, + * sparing the server's bandwidth. + * + * The default implementation always returns null. + */ + public CompletableFuture shortenPath() { + return null; + } + + protected Client thisCap() { + return new Client(this.hook); + } + + protected static CallContext internalGetTypedContext( + FromPointerReader paramsFactory, + FromPointerBuilder resultsFactory, + CallContext typeless) { + return new CallContext<>(paramsFactory, resultsFactory, typeless.getHook()); + } + + protected static StreamingCallContext internalGetTypedStreamingContext( + FromPointerReader paramsFactory, + CallContext typeless) { + return new StreamingCallContext<>(paramsFactory, typeless.getHook()); + } + + protected abstract DispatchCallResult dispatchCall( + long interfaceId, short methodId, + CallContext context); + + protected static DispatchCallResult streamResult(CompletableFuture result) { + // For streaming calls, we need to add an evalNow() here so that exceptions thrown + // directly from the call can propagate to later calls. If we don't capture the + // exception properly then the caller will never find out that this is a streaming + // call (indicated by the boolean in the return value) so won't know to propagate + // the exception. + // TODO the above comment... + return new DispatchCallResult(result, true); + } + + protected static DispatchCallResult result(CompletableFuture result) { + return new DispatchCallResult(result, false); + } + + protected static CompletableFuture internalUnimplemented(String actualInterfaceName, long requestedTypeId) { + return CompletableFuture.failedFuture(RpcException.unimplemented( + "Method not implemented. " + actualInterfaceName + " " + requestedTypeId)); + } + + protected static CompletableFuture internalUnimplemented(String interfaceName, long typeId, short methodId) { + return CompletableFuture.failedFuture(RpcException.unimplemented( + "Method not implemented. " + interfaceName + " " + typeId + " " + methodId)); + } + + protected static CompletableFuture internalUnimplemented(String interfaceName, String methodName, long typeId, short methodId) { + return CompletableFuture.failedFuture(RpcException.unimplemented( + "Method not implemented. " + interfaceName + " " + typeId + " " + methodName + " " + methodId)); + } + } + + public static ClientHook newLocalPromiseClient(CompletionStage promise) { + return new QueuedClient(promise.toCompletableFuture()); + } + + public static PipelineHook newLocalPromisePipeline(CompletionStage promise) { + return new QueuedPipeline(promise.toCompletableFuture()); + } + + private static class LocalRequest implements RequestHook { + + private final MessageBuilder message = new MessageBuilder(); + private final long interfaceId; + private final short methodId; + private final ClientHook client; + private final CompletableFuture callRelease = new CompletableFuture<>(); + + LocalRequest(long interfaceId, short methodId, ClientHook client) { + this.interfaceId = interfaceId; + this.methodId = methodId; + this.client = client; + } + + @Override + public RemotePromise send() { + var cancel = new CompletableFuture(); + var context = new LocalCallContext(message, client, cancel, this.callRelease); + var promiseAndPipeline = client.call(interfaceId, methodId, context); + var promise = promiseAndPipeline.promise.thenApply(x -> { + context.getResults(); // force allocation + return context.response; + }); + + this.callRelease.complete(null); + assert promiseAndPipeline.pipeline != null; + return new RemotePromise<>(promise, promiseAndPipeline.pipeline); + } + + @Override + public Object getBrand() { + return null; + } + + void releaseCall() { + this.callRelease.complete(null); + } + } + + private static final class LocalPipeline implements PipelineHook { + + private final CallContextHook ctx; + private final AnyPointer.Reader results; + + LocalPipeline(CallContextHook ctx) { + this.ctx = ctx; + this.results = ctx.getResults().asReader(); + } + + @Override + public final ClientHook getPipelinedCap(short[] ops) { + return this.results.getPipelinedCap(ops); + } + } + + private static final class LocalResponse implements ResponseHook { + + final MessageBuilder message; + + LocalResponse(int sizeHint) { + this.message = new MessageBuilder(sizeHint); + } + } + + private static class LocalCallContext implements CallContextHook { + + final CompletableFuture cancelAllowed; + private final CompletableFuture callRelease; + CompletableFuture tailCallPipeline; + MessageBuilder request; + Response response; + AnyPointer.Builder responseBuilder; + ClientHook clientRef; + + LocalCallContext(MessageBuilder request, + ClientHook clientRef, + CompletableFuture cancelAllowed, + CompletableFuture callRelease) { + this.request = request; + this.clientRef = clientRef; + this.cancelAllowed = cancelAllowed; + this.callRelease = callRelease; + } + + @Override + public AnyPointer.Reader getParams() { + return this.request.getRoot(AnyPointer.factory).asReader(); + } + + @Override + public void releaseParams() { + this.request = null; + } + + @Override + public AnyPointer.Builder getResults(int sizeHint) { + if (this.response == null) { + var localResponse = new LocalResponse(sizeHint); + this.responseBuilder = localResponse.message.getRoot(AnyPointer.factory); + this.response = new Response<>(this.responseBuilder.asReader(), localResponse); + } + assert this.response != null; + return this.responseBuilder; + } + + @Override + public void allowCancellation() { + this.cancelAllowed.complete(null); + } + + @Override + public CompletableFuture tailCall(RequestHook request) { + var result = this.directTailCall(request); + if (this.tailCallPipeline != null) { + this.tailCallPipeline.complete(new AnyPointer.Pipeline(result.pipeline)); + } + return result.promise; + } + + @Override + public CompletableFuture onTailCall() { + this.tailCallPipeline = new CompletableFuture<>(); + return this.tailCallPipeline.copy(); + } + + @Override + public ClientHook.VoidPromiseAndPipeline directTailCall(RequestHook request) { + assert this.response == null : "Can't call tailCall() after initializing the results struct."; + var promise = request.send(); + var voidPromise = promise.response.thenAccept(tailResponse -> { + this.response = tailResponse; + }); + return new ClientHook.VoidPromiseAndPipeline(voidPromise, promise.pipeline().hook); + } + + @Override + public CompletableFuture releaseCall() { + return this.callRelease; + } + } + + static Request newTypelessRequest(AnyPointer.Builder params, RequestHook requestHook) { + return new Request<>() { + @Override + public AnyPointer.Builder getParams() { + return params; + } + + @Override + public org.capnproto.Request getTypelessRequest() { + return this; + } + + @Override + public org.capnproto.Request getBaseRequest() { + return this; + } + + @Override + public RequestHook getHook() { + return requestHook; + } + + @Override + public FromPointerBuilder getParamsFactory() { + return AnyPointer.factory; + } + + @Override + public RemotePromise sendInternal() { + return requestHook.send(); + } + }; + } + + static StreamingRequest newTypelessStreamingRequest(AnyPointer.Builder params, RequestHook requestHook) { + return new StreamingRequest<>() { + @Override + public AnyPointer.Builder getParams() { + return params; + } + + @Override + public org.capnproto.StreamingRequest getTypelessRequest() { + return this; + } + + @Override + public RequestHook getHook() { + return requestHook; + } + + @Override + public FromPointerBuilder getParamsFactory() { + return AnyPointer.factory; + } + + public CompletableFuture send() { + return requestHook.sendStreaming(); + } + }; + } + + static PipelineHook newBrokenPipeline(Throwable exc) { + return ops -> newBrokenCap(exc); + } + + static Request newBrokenRequest(Throwable exc) { + + var message = new MessageBuilder(); + var params = message.getRoot(AnyPointer.factory); + + var hook = new RequestHook() { + @Override + public RemotePromise send() { + return new RemotePromise<>(CompletableFuture.failedFuture(exc), + newBrokenPipeline(exc)); + } + + @Override + public CompletableFuture sendStreaming() { + return CompletableFuture.failedFuture(exc); + } + }; + + return Capability.newTypelessRequest(params, hook); + } + + public static ClientHook newBrokenCap(String reason) { + return newBrokenClient(reason, false, BROKEN_CAPABILITY_BRAND); + } + + public static ClientHook newBrokenCap(Throwable exc) { + return newBrokenClient(exc, false, BROKEN_CAPABILITY_BRAND); + } + + public static ClientHook newNullCap() { + return newBrokenClient(RpcException.failed("Called null capability"), true, NULL_CAPABILITY_BRAND); + } + + private static ClientHook newBrokenClient(String reason, boolean resolved, Object brand) { + return newBrokenClient(RpcException.failed(reason), resolved, brand); + } + + private static ClientHook newBrokenClient(Throwable exc, boolean resolved, Object brand) { + return new ClientHook() { + @Override + public Request newCall(long interfaceId, short methodId) { + return newBrokenRequest(exc); + } + + @Override + public VoidPromiseAndPipeline call(long interfaceId, short methodId, CallContextHook context) { + return new VoidPromiseAndPipeline(CompletableFuture.failedFuture(exc), newBrokenPipeline(exc)); + } + + @Override + public CompletableFuture whenMoreResolved() { + return resolved ? null : CompletableFuture.failedFuture(exc); + } + + @Override + public Object getBrand() { + return brand; + } + }; + } + + // Call queues + // + // These classes handle pipelining in the case where calls need to be queued in-memory until some + // local operation completes. + + // A PipelineHook which simply queues calls while waiting for a PipelineHook to which to forward them. + private static final class QueuedPipeline implements PipelineHook { + + private final CompletableFuture promise; + PipelineHook redirect; + private final Map, ClientHook> clientMap = new HashMap<>(); + + QueuedPipeline(CompletableFuture promise) { + this.promise = promise.whenComplete((pipeline, exc) -> { + this.redirect = exc == null + ? pipeline + : newBrokenPipeline(exc); + }); + } + + @Override + public final ClientHook getPipelinedCap(short[] ops) { + if (redirect != null) { + return redirect.getPipelinedCap(ops); + } + + var key = new ArrayList(ops.length); + for (short op: ops) { + key.add(op); + } + + return this.clientMap.computeIfAbsent(key, + k -> new QueuedClient(this.promise.thenApply( + pipeline -> pipeline.getPipelinedCap(ops)))); + } + } + + // A ClientHook which simply queues calls while waiting for a ClientHook to which to forward them. + private static class QueuedClient implements ClientHook { + + private final CompletableFuture selfResolutionOp; + // Represents the operation which will set `redirect` when possible. + + private final CompletableFuture promiseForClientResolution = new CompletableFuture<>(); + // whenMoreResolved() returns forks of this promise. These must resolve *after* queued calls + // have been initiated (so that any calls made in the whenMoreResolved() handler are correctly + // delivered after calls made earlier), but *before* any queued calls return (because it might + // confuse the application if a queued call returns before the capability on which it was made + // resolves). + + private ClientHook redirect; + private final List> queuedCalls = new ArrayList<>(); + private final List pendingCalls = new ArrayList<>(); + + QueuedClient(CompletableFuture promise) { + this.selfResolutionOp = promise.handle((inner, exc) -> { + this.redirect = exc == null + ? inner + : newBrokenCap(exc); + + // Resolve promises for call forwarding. + for (var call: this.queuedCalls) { + call.complete(this.redirect); + } + + // Now resolve the promise for client resolution + this.promiseForClientResolution.complete(this.redirect); + + // Finally, execute any pending calls. + for (var hook: this.pendingCalls) { + hook.releaseCall(); + } + + this.queuedCalls.clear(); + this.pendingCalls.clear(); + + return null; + }); + } + + @Override + public Request newCall(long interfaceId, short methodId) { + var hook = new LocalRequest(interfaceId, methodId, this); + if (this.redirect == null) { + this.pendingCalls.add(hook); + } + else { + hook.releaseCall(); + } + var root = hook.message.getRoot(AnyPointer.factory); + return newTypelessRequest(root, hook); + } + + @Override + public VoidPromiseAndPipeline call(long interfaceId, short methodId, CallContextHook ctx) { + if (this.redirect != null) { + return this.redirect.call(interfaceId, methodId, ctx); + } + + var promise = new CompletableFuture(); + var callResult = promise.thenApply( + client -> client.call(interfaceId, methodId, ctx)); + var pipelineResult = callResult.thenApply(result -> result.pipeline); + var pipeline = new QueuedPipeline(pipelineResult); + this.queuedCalls.add(promise); + return new VoidPromiseAndPipeline(pipelineResult.thenRun(() -> {}), pipeline); + } + + @Override + public ClientHook getResolved() { + return redirect; + } + + @Override + public CompletableFuture whenMoreResolved() { + return this.promiseForClientResolution.copy(); + } + + @Override + public Object getBrand() { + return null; + } + } + + public static final class CapabilityServerSet { + + ClientHook addInternal(T server) { + return server.makeLocalClient(this); + } + + CompletableFuture getLocalServerInternal(ClientHook hook) { + for (;;) { + var next = hook.getResolved(); + if (next != null) { + hook = next; + } + else { + break; + } + } + + if (hook.getBrand() == Server.BRAND) { + var promise = ((Server.LocalClient)hook).getLocalServer(this); + if (promise != null) { + return promise; + } + } + + // The capability isn't part of this set. + var resolver = hook.whenMoreResolved(); + if (resolver != null) { + // This hook is an unresolved promise. It might resolve eventually to a local server, so wait + // for it. + return resolver.thenCompose(this::getLocalServerInternal); + } + else { + // Cap is settled, so it definitely will never resolve to a member of this set. + return CompletableFuture.completedFuture(null); + } + } + + /** + * Create a new capability Client for the given Server and also add this server to the set. + */ + U add(Capability.Factory factory, T server) { + var hook = this.addInternal(server); + return factory.newClient(hook); + } + + CompletableFuture getLocalServer(Client client) { + return this.getLocalServerInternal(client.getHook()) + .thenApply(server -> (T)server); + } + } +} diff --git a/runtime/src/main/java/org/capnproto/CapabilityList.java b/runtime/src/main/java/org/capnproto/CapabilityList.java new file mode 100644 index 00000000..e49066e8 --- /dev/null +++ b/runtime/src/main/java/org/capnproto/CapabilityList.java @@ -0,0 +1,123 @@ +// Copyright (c) 2013-2014 Sandstorm Development Group, Inc. and contributors +// Licensed under the MIT License: +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package org.capnproto; + +public final class CapabilityList { + public static final class Factory extends ListFactory { + Factory() {super (ElementSize.POINTER); } + public final Reader constructReader(SegmentReader segment, + int ptr, + int elementCount, int step, + int structDataSize, short structPointerCount, + int nestingLimit) { + return new Reader(segment, ptr, elementCount, step, structDataSize, structPointerCount, nestingLimit); + } + + public final Builder constructBuilder(SegmentBuilder segment, + int ptr, + int elementCount, int step, + int structDataSize, short structPointerCount) { + return new Builder(segment, ptr, elementCount, step, structDataSize, structPointerCount); + } + } + public static final Factory factory = new Factory(); + + public static final class Reader extends ListReader implements Iterable { + public Reader(SegmentReader segment, + int ptr, + int elementCount, int step, + int structDataSize, short structPointerCount, + int nestingLimit) { + super(segment, ptr, elementCount, step, structDataSize, structPointerCount, nestingLimit); + } + + public Capability.Client get(int index) { + return _getPointerElement(Capability.factory, index); + } + + public final class Iterator implements java.util.Iterator { + public Reader list; + public int idx = 0; + public Iterator(Reader list) { + this.list = list; + } + + public Capability.Client next() { + return this.list._getPointerElement(Capability.factory, idx++); + } + public boolean hasNext() { + return idx < list.size(); + } + public void remove() { + throw new UnsupportedOperationException(); + } + } + + public java.util.Iterator iterator() { + return new Iterator(this); + } + } + + public static final class Builder extends ListBuilder implements Iterable { + public Builder(SegmentBuilder segment, int ptr, + int elementCount, int step, + int structDataSize, short structPointerCount){ + super(segment, ptr, elementCount, step, structDataSize, structPointerCount); + } + + public final Capability.Client get(int index) { + return _getPointerElement(Capability.factory, index); + } + + public final void set(int index, Capability.Client value) { + _setPointerElement(Capability.factory, index, value); + } + + public final Reader asReader() { + return new Reader(this.segment, this.ptr, this.elementCount, this.step, + this.structDataSize, this.structPointerCount, + Integer.MAX_VALUE); + } + + public final class Iterator implements java.util.Iterator { + public Builder list; + public int idx = 0; + public Iterator(Builder list) { + this.list = list; + } + + public Capability.Client next() { + return this.list._getPointerElement(Capability.factory, idx++); + } + public boolean hasNext() { + return this.idx < this.list.size(); + } + public void remove() { + throw new UnsupportedOperationException(); + } + } + + public java.util.Iterator iterator() { + return new Iterator(this); + } + } +} diff --git a/runtime/src/main/java/org/capnproto/ClientHook.java b/runtime/src/main/java/org/capnproto/ClientHook.java new file mode 100644 index 00000000..ed4b3200 --- /dev/null +++ b/runtime/src/main/java/org/capnproto/ClientHook.java @@ -0,0 +1,104 @@ +package org.capnproto; + +import java.io.FileDescriptor; +import java.util.concurrent.CompletableFuture; + +public interface ClientHook { + + static final Object NULL_CAPABILITY_BRAND = new Object(); + static final Object BROKEN_CAPABILITY_BRAND = new Object(); + /** + * Start a new call, allowing the client to allocate request/response objects as it sees fit. + * This version is used when calls are made from application code in the local process. + */ + Request newCall(long interfaceId, short methodId); + + /** + * Call the object, but the caller controls allocation of the request/response objects. If the + * callee insists on allocating these objects itself, it must make a copy. This version is used + * when calls come in over the network via an RPC system. + * + * Since the caller of this method chooses the CallContext implementation, it is the caller's + * responsibility to ensure that the returned promise is not canceled unless allowed via + * the context's `allowCancellation()`. + * + * The call must not begin synchronously; the callee must arrange for the call to begin in a + * later turn of the event loop. Otherwise, application code may call back and affect the + * callee's state in an unexpected way. + */ + VoidPromiseAndPipeline call(long interfaceId, short methodId, CallContextHook context); + + /** + If this ClientHook is a promise that has already resolved, returns the inner, resolved version + of the capability. The caller may permanently replace this client with the resolved one if + desired. Returns null if the client isn't a promise or hasn't resolved yet -- use + `whenMoreResolved()` to distinguish between them. + + @return the resolved capability + */ + default ClientHook getResolved() { + return null; + } + + /** + If this client is a settled reference (not a promise), return nullptr. Otherwise, return a + promise that eventually resolves to a new client that is closer to being the final, settled + client (i.e. the value eventually returned by `getResolved()`). Calling this repeatedly + should eventually produce a settled client. + */ + default CompletableFuture whenMoreResolved() { + return null; + } + + /** + Returns an opaque object that identifies who made this client. This can be used by an RPC adapter to + discover when a capability it needs to marshal is one that it created in the first place, and + therefore it can transfer the capability without proxying. + */ + Object getBrand(); + + /** + * Repeatedly calls whenMoreResolved() until it returns nullptr. + */ + default CompletableFuture whenResolved() { + CompletableFuture promise = whenMoreResolved(); + return promise != null + ? promise.thenCompose(ClientHook::whenResolved) + : CompletableFuture.completedFuture(null); + } + + /** + * Returns true if the capability was created as a result of assigning a Client to null or by + * reading a null pointer out of a Cap'n Proto message. + */ + default boolean isNull() { + return getBrand() == NULL_CAPABILITY_BRAND; + } + + /** + * Returns true if the capability was created by newBrokenCap(). + */ + default boolean isError() { + return getBrand() == BROKEN_CAPABILITY_BRAND; + } + + /** + * Implements Capability.Client.getFd. If this returns null but whenMoreResolved() returns + * non-null, then Capability::Client::getFd() waits for resolution and tries again. + */ + default FileDescriptor getFd() { + return null; + } + + final class VoidPromiseAndPipeline { + + public final CompletableFuture promise; + public final PipelineHook pipeline; + + VoidPromiseAndPipeline(CompletableFuture promise, + PipelineHook pipeline) { + this.promise = promise; + this.pipeline = pipeline; + } + } +} diff --git a/runtime/src/main/java/org/capnproto/Data.java b/runtime/src/main/java/org/capnproto/Data.java index dec66223..b97def10 100644 --- a/runtime/src/main/java/org/capnproto/Data.java +++ b/runtime/src/main/java/org/capnproto/Data.java @@ -39,35 +39,42 @@ public final Reader fromPointerReader(SegmentReader segment, int pointer, int ne return WireHelpers.readDataPointer(segment, pointer, null, 0, 0); } + @Override + public final Reader fromPointerReader(SegmentReader segment, CapTableReader capTable, int pointer, int nestingLimit) { + return WireHelpers.readDataPointer(segment, pointer, null, 0, 0); + } + @Override public final Builder fromPointerBuilderBlobDefault( SegmentBuilder segment, + CapTableBuilder capTable, int pointer, java.nio.ByteBuffer defaultBuffer, int defaultOffset, int defaultSize) { return WireHelpers.getWritableDataPointer(pointer, segment, + capTable, defaultBuffer, defaultOffset, defaultSize); } - @Override - public final Builder fromPointerBuilder(SegmentBuilder segment, int pointer) { + public final Builder fromPointerBuilder(SegmentBuilder segment, CapTableBuilder capTable, int pointer) { return WireHelpers.getWritableDataPointer(pointer, segment, + capTable, null, 0, 0); } @Override - public final Builder initFromPointerBuilder(SegmentBuilder segment, int pointer, int size) { - return WireHelpers.initDataPointer(pointer, segment, size); + public final Builder initFromPointerBuilder(SegmentBuilder segment, CapTableBuilder capTable, int pointer, int size) { + return WireHelpers.initDataPointer(pointer, segment, capTable, size); } @Override - public final void setPointerBuilder(SegmentBuilder segment, int pointer, Reader value) { - WireHelpers.setDataPointer(pointer, segment, value); + public final void setPointerBuilder(SegmentBuilder segment, CapTableBuilder capTable, int pointer, Reader value) { + WireHelpers.setDataPointer(pointer, segment, capTable, value); } } public static final Factory factory = new Factory(); diff --git a/runtime/src/main/java/org/capnproto/DispatchCallResult.java b/runtime/src/main/java/org/capnproto/DispatchCallResult.java new file mode 100644 index 00000000..1c7c86cb --- /dev/null +++ b/runtime/src/main/java/org/capnproto/DispatchCallResult.java @@ -0,0 +1,22 @@ +package org.capnproto; + +import java.util.concurrent.CompletableFuture; + +public final class DispatchCallResult { + + final CompletableFuture promise; + private final boolean streaming; + + public DispatchCallResult(CompletableFuture promise, boolean isStreaming) { + this.promise = promise; + this.streaming = isStreaming; + } + + public DispatchCallResult(Throwable exc) { + this(CompletableFuture.failedFuture(exc), false); + } + + public boolean isStreaming() { + return streaming; + } +} diff --git a/runtime/src/main/java/org/capnproto/ExportTable.java b/runtime/src/main/java/org/capnproto/ExportTable.java new file mode 100644 index 00000000..8cb70896 --- /dev/null +++ b/runtime/src/main/java/org/capnproto/ExportTable.java @@ -0,0 +1,49 @@ +package org.capnproto; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.PriorityQueue; +import java.util.Queue; +import java.util.function.Consumer; + +abstract class ExportTable implements Iterable { + + private final HashMap slots = new HashMap<>(); + private final Queue freeIds = new PriorityQueue<>(); + private int max = 0; + + abstract T newExportable(int id); + + public T find(int id) { + return slots.get(id); + } + + public T erase(int id, T entry) { + T value = slots.get(id); + if (value == entry) { + freeIds.add(id); + return slots.remove(id); + } else { + return null; + } + } + + public T next() { + int id = freeIds.isEmpty() ? max++ : freeIds.remove(); + T value = newExportable(id); + T prev = slots.put(id, value); + assert prev == null; + return value; + } + + @Override + public Iterator iterator() { + return slots.values().iterator(); + } + + @Override + public void forEach(Consumer action) { + slots.values().forEach(action); + } +} + diff --git a/runtime/src/main/java/org/capnproto/FromPointerBuilder.java b/runtime/src/main/java/org/capnproto/FromPointerBuilder.java index 9fa715b7..ca6877e5 100644 --- a/runtime/src/main/java/org/capnproto/FromPointerBuilder.java +++ b/runtime/src/main/java/org/capnproto/FromPointerBuilder.java @@ -22,6 +22,15 @@ package org.capnproto; public interface FromPointerBuilder { - T fromPointerBuilder(SegmentBuilder segment, int pointer); - T initFromPointerBuilder(SegmentBuilder segment, int pointer, int elementCount); + default T fromPointerBuilder(SegmentBuilder segment, int pointer) { + return fromPointerBuilder(segment, null, pointer); + } + + T fromPointerBuilder(SegmentBuilder segment, CapTableBuilder capTable, int pointer); + + default T initFromPointerBuilder(SegmentBuilder segment, int pointer, int elementCount) { + return initFromPointerBuilder(segment, null, pointer, elementCount); + } + + T initFromPointerBuilder(SegmentBuilder segment, CapTableBuilder capTable, int pointer, int elementCount); } diff --git a/runtime/src/main/java/org/capnproto/FromPointerBuilderBlobDefault.java b/runtime/src/main/java/org/capnproto/FromPointerBuilderBlobDefault.java index 377fb3a6..3004beec 100644 --- a/runtime/src/main/java/org/capnproto/FromPointerBuilderBlobDefault.java +++ b/runtime/src/main/java/org/capnproto/FromPointerBuilderBlobDefault.java @@ -22,6 +22,11 @@ package org.capnproto; public interface FromPointerBuilderBlobDefault { - T fromPointerBuilderBlobDefault(SegmentBuilder segment, int pointer, + default T fromPointerBuilderBlobDefault(SegmentBuilder segment, int pointer, + java.nio.ByteBuffer defaultBuffer, int defaultOffset, int defaultSize) { + return fromPointerBuilderBlobDefault(segment, null, pointer, defaultBuffer, defaultOffset, defaultSize); + } + + T fromPointerBuilderBlobDefault(SegmentBuilder segment, CapTableBuilder capTable, int pointer, java.nio.ByteBuffer defaultBuffer, int defaultOffset, int defaultSize); } diff --git a/runtime/src/main/java/org/capnproto/FromPointerBuilderRefDefault.java b/runtime/src/main/java/org/capnproto/FromPointerBuilderRefDefault.java index 50c8b3d2..44d56e85 100644 --- a/runtime/src/main/java/org/capnproto/FromPointerBuilderRefDefault.java +++ b/runtime/src/main/java/org/capnproto/FromPointerBuilderRefDefault.java @@ -22,5 +22,10 @@ package org.capnproto; public interface FromPointerBuilderRefDefault { - T fromPointerBuilderRefDefault(SegmentBuilder segment, int pointer, SegmentReader defaultSegment, int defaultOffset); + + default T fromPointerBuilderRefDefault(SegmentBuilder segment, int pointer, SegmentReader defaultSegment, int defaultOffset) { + return fromPointerBuilderRefDefault(segment, null, pointer, defaultSegment, defaultOffset); + } + + T fromPointerBuilderRefDefault(SegmentBuilder segment, CapTableBuilder capTable, int pointer, SegmentReader defaultSegment, int defaultOffset); } diff --git a/runtime/src/main/java/org/capnproto/FromPointerReader.java b/runtime/src/main/java/org/capnproto/FromPointerReader.java index 1a1a71b0..5738cc57 100644 --- a/runtime/src/main/java/org/capnproto/FromPointerReader.java +++ b/runtime/src/main/java/org/capnproto/FromPointerReader.java @@ -22,5 +22,8 @@ package org.capnproto; public interface FromPointerReader { - T fromPointerReader(SegmentReader segment, int pointer, int nestingLimit); + default T fromPointerReader(SegmentReader segment, int pointer, int nestingLimit) { + return fromPointerReader(segment, null, pointer, nestingLimit); + } + T fromPointerReader(SegmentReader segment, CapTableReader capTable, int pointer, int nestingLimit); } diff --git a/runtime/src/main/java/org/capnproto/FromPointerReaderRefDefault.java b/runtime/src/main/java/org/capnproto/FromPointerReaderRefDefault.java index e6d05e02..cf301638 100644 --- a/runtime/src/main/java/org/capnproto/FromPointerReaderRefDefault.java +++ b/runtime/src/main/java/org/capnproto/FromPointerReaderRefDefault.java @@ -22,5 +22,8 @@ package org.capnproto; public interface FromPointerReaderRefDefault { - T fromPointerReaderRefDefault(SegmentReader segment, int pointer, SegmentReader defaultSegment, int defaultOffset, int nestingLimit); + default T fromPointerReaderRefDefault(SegmentReader segment, int pointer, SegmentReader defaultSegment, int defaultOffset, int nestingLimit) { + return fromPointerReaderRefDefault(segment, null, pointer, defaultSegment, defaultOffset, nestingLimit); + } + T fromPointerReaderRefDefault(SegmentReader segment, CapTableReader capTable, int pointer, SegmentReader defaultSegment, int defaultOffset, int nestingLimit); } diff --git a/runtime/src/main/java/org/capnproto/ImportTable.java b/runtime/src/main/java/org/capnproto/ImportTable.java new file mode 100644 index 00000000..970dee8f --- /dev/null +++ b/runtime/src/main/java/org/capnproto/ImportTable.java @@ -0,0 +1,40 @@ +package org.capnproto; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.function.Consumer; + +abstract class ImportTable implements Iterable { + + private final HashMap slots = new HashMap<>(); + + protected abstract T newImportable(int id); + + public T put(int id) { + return this.slots.computeIfAbsent(id, key -> newImportable(id)); + } + + public T find(int id) { + return slots.get(id); + } + + public T erase(int id, T entry) { + boolean removed = slots.remove(id, entry); + assert removed; + return entry; + } + + public T erase(int id) { + return slots.remove(id); + } + + @Override + public Iterator iterator() { + return slots.values().iterator(); + } + + @Override + public void forEach(Consumer action) { + slots.values().forEach(action); + } +} diff --git a/runtime/src/main/java/org/capnproto/IncomingRpcMessage.java b/runtime/src/main/java/org/capnproto/IncomingRpcMessage.java new file mode 100644 index 00000000..eb5692dc --- /dev/null +++ b/runtime/src/main/java/org/capnproto/IncomingRpcMessage.java @@ -0,0 +1,13 @@ +package org.capnproto; + +import java.io.FileDescriptor; +import java.util.List; + +public interface IncomingRpcMessage { + + AnyPointer.Reader getBody(); + + default List getAttachedFds() { + return List.of(); + } +} diff --git a/runtime/src/main/java/org/capnproto/ListBuilder.java b/runtime/src/main/java/org/capnproto/ListBuilder.java index 15cff8c8..05b581c8 100644 --- a/runtime/src/main/java/org/capnproto/ListBuilder.java +++ b/runtime/src/main/java/org/capnproto/ListBuilder.java @@ -21,11 +21,22 @@ package org.capnproto; -public class ListBuilder { +import java.util.List; + +public class ListBuilder extends CapTableBuilder.BuilderContext { public interface Factory { T constructBuilder(SegmentBuilder segment, int ptr, int elementCount, int step, int structDataSize, short structPointerCount); + default T constructBuilder(SegmentBuilder segment, CapTableBuilder capTable, int ptr, + int elementCount, int step, + int structDataSize, short structPointerCount) { + T result = constructBuilder(segment, ptr, elementCount, step, structDataSize, structPointerCount); + if (result instanceof CapTableBuilder.BuilderContext) { + ((CapTableBuilder.BuilderContext) result).capTable = capTable; + } + return result; + } } final SegmentBuilder segment; @@ -38,12 +49,19 @@ T constructBuilder(SegmentBuilder segment, int ptr, public ListBuilder(SegmentBuilder segment, int ptr, int elementCount, int step, int structDataSize, short structPointerCount) { + this(segment, null, ptr, elementCount, step, structDataSize, structPointerCount); + } + + public ListBuilder(SegmentBuilder segment, CapTableBuilder capTable, int ptr, + int elementCount, int step, + int structDataSize, short structPointerCount) { this.segment = segment; this.ptr = ptr; this.elementCount = elementCount; this.step = step; this.structDataSize = structDataSize; this.structPointerCount = structPointerCount; + this.capTable = capTable; } public int size() { @@ -119,6 +137,7 @@ protected final T _getStructElement(StructBuilder.Factory factory, int in int structPointers = (structData + (this.structDataSize / 8)) / 8; return factory.constructBuilder(this.segment, + this.capTable, structData, structPointers, this.structDataSize, @@ -128,18 +147,21 @@ protected final T _getStructElement(StructBuilder.Factory factory, int in protected final T _getPointerElement(FromPointerBuilder factory, int index) { return factory.fromPointerBuilder( this.segment, + this.capTable, (this.ptr + (int)((long)index * this.step / Constants.BITS_PER_BYTE)) / Constants.BYTES_PER_WORD); } protected final T _initPointerElement(FromPointerBuilder factory, int index, int elementCount) { return factory.initFromPointerBuilder( this.segment, + this.capTable, (this.ptr + (int)((long)index * this.step / Constants.BITS_PER_BYTE)) / Constants.BYTES_PER_WORD, elementCount); } protected final void _setPointerElement(SetPointerBuilder factory, int index, Reader value) { factory.setPointerBuilder(this.segment, + this.capTable, (this.ptr + (int)((long)index * this.step / Constants.BITS_PER_BYTE)) / Constants.BYTES_PER_WORD, value); } diff --git a/runtime/src/main/java/org/capnproto/ListFactory.java b/runtime/src/main/java/org/capnproto/ListFactory.java index bbb84447..61752c2d 100644 --- a/runtime/src/main/java/org/capnproto/ListFactory.java +++ b/runtime/src/main/java/org/capnproto/ListFactory.java @@ -21,7 +21,7 @@ package org.capnproto; -public abstract class ListFactory +public abstract class ListFactory implements ListBuilder.Factory, FromPointerBuilderRefDefault, SetPointerBuilder, @@ -32,45 +32,48 @@ public abstract class ListFactory final byte elementSize; ListFactory(byte elementSize) {this.elementSize = elementSize;} - public final Reader fromPointerReaderRefDefault(SegmentReader segment, int pointer, + public final Reader fromPointerReaderRefDefault(SegmentReader segment, CapTableReader capTable, int pointer, SegmentReader defaultSegment, int defaultOffset, int nestingLimit) { return WireHelpers.readListPointer(this, segment, pointer, + capTable, defaultSegment, defaultOffset, this.elementSize, nestingLimit); } - public final Reader fromPointerReader(SegmentReader segment, int pointer, int nestingLimit) { - return fromPointerReaderRefDefault(segment, pointer, null, 0, nestingLimit); + public final Reader fromPointerReader(SegmentReader segment, CapTableReader capTable, int pointer, int nestingLimit) { + return fromPointerReaderRefDefault(segment, capTable, pointer, null, 0, nestingLimit); } - public Builder fromPointerBuilderRefDefault(SegmentBuilder segment, int pointer, + public Builder fromPointerBuilderRefDefault(SegmentBuilder segment, CapTableBuilder capTable, int pointer, SegmentReader defaultSegment, int defaultOffset) { return WireHelpers.getWritableListPointer(this, pointer, segment, + capTable, this.elementSize, defaultSegment, defaultOffset); } - public Builder fromPointerBuilder(SegmentBuilder segment, int pointer) { + public Builder fromPointerBuilder(SegmentBuilder segment, CapTableBuilder capTable, int pointer) { return WireHelpers.getWritableListPointer(this, pointer, segment, + capTable, this.elementSize, null, 0); } - public Builder initFromPointerBuilder(SegmentBuilder segment, int pointer, int elementCount) { - return WireHelpers.initListPointer(this, pointer, segment, elementCount, this.elementSize); + public Builder initFromPointerBuilder(SegmentBuilder segment, CapTableBuilder capTable, int pointer, int elementCount) { + return WireHelpers.initListPointer(this, capTable, pointer, segment, elementCount, this.elementSize); } - public final void setPointerBuilder(SegmentBuilder segment, int pointer, Reader value) { - WireHelpers.setListPointer(segment, pointer, value); + public final void setPointerBuilder(SegmentBuilder segment, CapTableBuilder capTable, int pointer, Reader value) { + WireHelpers.setListPointer(segment, capTable, pointer, value); } } diff --git a/runtime/src/main/java/org/capnproto/ListList.java b/runtime/src/main/java/org/capnproto/ListList.java index 13488aa2..88d387b6 100644 --- a/runtime/src/main/java/org/capnproto/ListList.java +++ b/runtime/src/main/java/org/capnproto/ListList.java @@ -22,7 +22,7 @@ package org.capnproto; public final class ListList { - public static final class Factory + public static final class Factory extends ListFactory, Reader> { public final ListFactory factory; @@ -68,7 +68,7 @@ public T get(int index) { } } - public static final class Builder extends ListBuilder { + public static final class Builder extends ListBuilder { private final ListFactory factory; public Builder(ListFactory factory, diff --git a/runtime/src/main/java/org/capnproto/ListReader.java b/runtime/src/main/java/org/capnproto/ListReader.java index f1b2970e..2fc68d6c 100644 --- a/runtime/src/main/java/org/capnproto/ListReader.java +++ b/runtime/src/main/java/org/capnproto/ListReader.java @@ -21,13 +21,22 @@ package org.capnproto; -public class ListReader { +public class ListReader extends CapTableReader.ReaderContext { public interface Factory { T constructReader(SegmentReader segment, int ptr, int elementCount, int step, int structDataSize, short structPointerCount, int nestingLimit); + default T constructReader(SegmentReader segment, CapTableReader capTable, int ptr, + int elementCount, int step, + int structDataSize, short structPointerCount, int nestingLimit) { + T result = constructReader(segment, ptr, elementCount, step, structDataSize, structPointerCount, nestingLimit); + if (result instanceof CapTableReader.ReaderContext) { + ((CapTableReader.ReaderContext) result).capTable = capTable; + } + return result; + } } final SegmentReader segment; @@ -46,12 +55,22 @@ public ListReader() { this.structDataSize = 0; this.structPointerCount = 0; this.nestingLimit = 0x7fffffff; + this.capTable = null; } public ListReader(SegmentReader segment, int ptr, int elementCount, int step, int structDataSize, short structPointerCount, int nestingLimit) { + this(segment, null, ptr, elementCount, step, structDataSize, structPointerCount, nestingLimit); + } + + public ListReader(SegmentReader segment, + CapTableReader capTable, + int ptr, + int elementCount, int step, + int structDataSize, short structPointerCount, + int nestingLimit) { this.segment = segment; this.ptr = ptr; this.elementCount = elementCount; @@ -59,7 +78,11 @@ public ListReader(SegmentReader segment, int ptr, this.structDataSize = structDataSize; this.structPointerCount = structPointerCount; this.nestingLimit = nestingLimit; + this.capTable = capTable; + } + ListReader imbue(CapTableReader capTable) { + return new ListReader(segment, capTable, ptr, elementCount, step, structDataSize, structPointerCount, nestingLimit); } public int size() { @@ -103,7 +126,7 @@ protected T _getStructElement(StructReader.Factory factory, int index) { int structData = this.ptr + (int)(indexBit / Constants.BITS_PER_BYTE); int structPointers = structData + (this.structDataSize / Constants.BITS_PER_BYTE); - return factory.constructReader(this.segment, structData, structPointers / 8, this.structDataSize, + return factory.constructReader(this.segment, this.capTable, structData, structPointers / 8, this.structDataSize, this.structPointerCount, this.nestingLimit - 1); } diff --git a/runtime/src/main/java/org/capnproto/MessageBuilder.java b/runtime/src/main/java/org/capnproto/MessageBuilder.java index 8d75b6d3..fbfe0829 100644 --- a/runtime/src/main/java/org/capnproto/MessageBuilder.java +++ b/runtime/src/main/java/org/capnproto/MessageBuilder.java @@ -101,9 +101,9 @@ private AnyPointer.Builder getRootInternal() { if (location != 0) { throw new RuntimeException("First allocated word of new segment was not at offset 0"); } - return new AnyPointer.Builder(rootSegment, location); + return new AnyPointer.Builder(rootSegment, this.arena.getLocalCapTable(), location); } else { - return new AnyPointer.Builder(rootSegment, 0); + return new AnyPointer.Builder(rootSegment, this.arena.getLocalCapTable(), 0); } } diff --git a/runtime/src/main/java/org/capnproto/OutgoingRpcMessage.java b/runtime/src/main/java/org/capnproto/OutgoingRpcMessage.java new file mode 100644 index 00000000..e91f5292 --- /dev/null +++ b/runtime/src/main/java/org/capnproto/OutgoingRpcMessage.java @@ -0,0 +1,20 @@ +package org.capnproto; + +import java.io.FileDescriptor; +import java.util.List; + +public interface OutgoingRpcMessage { + + AnyPointer.Builder getBody(); + + default void setFds(List fds) { + } + + default List getFds() { + return List.of(); + } + + void send(); + + int sizeInWords(); +} diff --git a/runtime/src/main/java/org/capnproto/Pipeline.java b/runtime/src/main/java/org/capnproto/Pipeline.java new file mode 100644 index 00000000..df9ad683 --- /dev/null +++ b/runtime/src/main/java/org/capnproto/Pipeline.java @@ -0,0 +1,10 @@ +package org.capnproto; + +public interface Pipeline { + + AnyPointer.Pipeline typelessPipeline(); + + default void cancel(Throwable exc) { + this.typelessPipeline().cancel(exc); + } +} diff --git a/runtime/src/main/java/org/capnproto/PipelineHook.java b/runtime/src/main/java/org/capnproto/PipelineHook.java new file mode 100644 index 00000000..0b691e02 --- /dev/null +++ b/runtime/src/main/java/org/capnproto/PipelineHook.java @@ -0,0 +1,9 @@ +package org.capnproto; + +public interface PipelineHook { + + ClientHook getPipelinedCap(short[] ops); + + default void cancel(Throwable exc) { + } +} diff --git a/runtime/src/main/java/org/capnproto/ReaderCapabilityTable.java b/runtime/src/main/java/org/capnproto/ReaderCapabilityTable.java new file mode 100644 index 00000000..a1ba09b3 --- /dev/null +++ b/runtime/src/main/java/org/capnproto/ReaderCapabilityTable.java @@ -0,0 +1,17 @@ +package org.capnproto; + +import java.util.List; + +class ReaderCapabilityTable implements CapTableReader { + + final List table; + + ReaderCapabilityTable(List table) { + this.table = table; + } + + @Override + public ClientHook extractCap(int index) { + return index < table.size() ? table.get(index) : null; + } +} diff --git a/runtime/src/main/java/org/capnproto/RemotePromise.java b/runtime/src/main/java/org/capnproto/RemotePromise.java new file mode 100644 index 00000000..431de040 --- /dev/null +++ b/runtime/src/main/java/org/capnproto/RemotePromise.java @@ -0,0 +1,46 @@ +package org.capnproto; + +import java.util.concurrent.CompletableFuture; + +public class RemotePromise + extends CompletableFuture + implements AutoCloseable { + + final CompletableFuture> response; + private final AnyPointer.Pipeline pipeline; + + public RemotePromise(FromPointerReader factory, + RemotePromise other) { + this(other.response.thenApply(response -> Response.fromTypeless(factory, response)), other.pipeline); + } + + public RemotePromise(CompletableFuture> promise, + PipelineHook pipeline) { + this(promise, new AnyPointer.Pipeline(pipeline)); + } + + public RemotePromise(CompletableFuture> promise, + AnyPointer.Pipeline pipeline) { + this.response = promise.whenComplete((response, exc) -> { + if (exc != null) { + this.completeExceptionally(exc); + } + else { + this.complete(response.getResults()); + } + }); + + this.pipeline = pipeline; + } + + @Override + public void close() { + this.pipeline.cancel(RpcException.failed("Cancelled")); + this.join(); + } + + public AnyPointer.Pipeline pipeline() { + return this.pipeline; + } +} + diff --git a/runtime/src/main/java/org/capnproto/Request.java b/runtime/src/main/java/org/capnproto/Request.java new file mode 100644 index 00000000..45dff2b7 --- /dev/null +++ b/runtime/src/main/java/org/capnproto/Request.java @@ -0,0 +1,15 @@ +package org.capnproto; + +public interface Request + extends RequestBase { + + RequestBase getBaseRequest(); + + default FromPointerBuilder getParamsFactory() { + return getBaseRequest().getParamsFactory(); + } + + default RequestBase getTypelessRequest() { + return getBaseRequest().getTypelessRequest(); + } +} diff --git a/runtime/src/main/java/org/capnproto/RequestBase.java b/runtime/src/main/java/org/capnproto/RequestBase.java new file mode 100644 index 00000000..fb3b5f50 --- /dev/null +++ b/runtime/src/main/java/org/capnproto/RequestBase.java @@ -0,0 +1,20 @@ +package org.capnproto; + +public interface RequestBase { + + FromPointerBuilder getParamsFactory(); + + RequestBase getTypelessRequest(); + + default Params getParams() { + return this.getTypelessRequest().getParams().getAs(this.getParamsFactory()); + } + + default RequestHook getHook() { + return this.getTypelessRequest().getHook(); + } + + default RemotePromise sendInternal() { + return this.getTypelessRequest().sendInternal(); + } +} diff --git a/runtime/src/main/java/org/capnproto/RequestHook.java b/runtime/src/main/java/org/capnproto/RequestHook.java new file mode 100644 index 00000000..9cd1a41e --- /dev/null +++ b/runtime/src/main/java/org/capnproto/RequestHook.java @@ -0,0 +1,16 @@ +package org.capnproto; + +import java.util.concurrent.CompletableFuture; + +public interface RequestHook { + + RemotePromise send(); + + default CompletableFuture sendStreaming() { + return this.send().thenApply(results -> null); + } + + default Object getBrand() { + return null; + } +} diff --git a/runtime/src/main/java/org/capnproto/Response.java b/runtime/src/main/java/org/capnproto/Response.java new file mode 100644 index 00000000..3d5ab0de --- /dev/null +++ b/runtime/src/main/java/org/capnproto/Response.java @@ -0,0 +1,27 @@ +package org.capnproto; + +public final class Response { + + private final Results results; + private final ResponseHook hook; + + public Response(Results results, + ResponseHook hook) { + this.results = results; + this.hook = hook; + } + + public Results getResults() { + return this.results; + } + + public ResponseHook getHook() { + return this.hook; + } + + static Response fromTypeless(FromPointerReader resultsFactory, + Response typeless) { + return new Response<>(typeless.getResults().getAs(resultsFactory), typeless.hook); + + } +} diff --git a/runtime/src/main/java/org/capnproto/ResponseHook.java b/runtime/src/main/java/org/capnproto/ResponseHook.java new file mode 100644 index 00000000..24384ead --- /dev/null +++ b/runtime/src/main/java/org/capnproto/ResponseHook.java @@ -0,0 +1,4 @@ +package org.capnproto; + +public interface ResponseHook { +} diff --git a/runtime/src/main/java/org/capnproto/RpcException.java b/runtime/src/main/java/org/capnproto/RpcException.java new file mode 100644 index 00000000..5f0184af --- /dev/null +++ b/runtime/src/main/java/org/capnproto/RpcException.java @@ -0,0 +1,34 @@ +package org.capnproto; + +public final class RpcException extends java.lang.Exception { + + public enum Type { + FAILED, + OVERLOADED, + DISCONNECTED, + UNIMPLEMENTED + } + + private final Type type; + + public RpcException(Type type, String message) { + super(message); + this.type = type; + } + + public final Type getType() { + return type; + } + + public static RpcException unimplemented(String message) { + return new RpcException(Type.UNIMPLEMENTED, message); + } + + public static RpcException failed(String message) { + return new RpcException(Type.FAILED, message); + } + + public static RpcException disconnected(String message) { + return new RpcException(Type.DISCONNECTED, message); + } +} diff --git a/runtime/src/main/java/org/capnproto/Serialize.java b/runtime/src/main/java/org/capnproto/Serialize.java index b7850cfe..7a6fd0a9 100644 --- a/runtime/src/main/java/org/capnproto/Serialize.java +++ b/runtime/src/main/java/org/capnproto/Serialize.java @@ -22,11 +22,13 @@ package org.capnproto; import java.io.IOException; -import java.nio.channels.ReadableByteChannel; -import java.nio.channels.WritableByteChannel; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.nio.channels.*; import java.util.ArrayList; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; import java.util.Optional; /** @@ -305,4 +307,183 @@ public static void write(WritableByteChannel outputChannel, } } } + + static abstract class AsyncMessageReader { + private final ReaderOptions options; + protected final CompletableFuture readCompleted = new CompletableFuture<>(); + + AsyncMessageReader(ReaderOptions options) { + this.options = options; + } + + public CompletableFuture getMessage() { + readHeader(); + return readCompleted; + } + + private void readHeader() { + read(Constants.BYTES_PER_WORD, firstWord -> { + var segmentCount = 1 + firstWord.getInt(0); + var segment0Size = firstWord.getInt(4); + + if (segmentCount == 1) { + readSegments(segment0Size, segmentCount, segment0Size, null); + return; + } + + // check before allocating segment size buffer + if (segmentCount > 512) { + readCompleted.completeExceptionally(new IOException("Too many segments")); + return; + } + + read(4 * (segmentCount & ~1), moreSizesRaw -> { + var moreSizes = new int[segmentCount - 1]; + var totalWords = segment0Size; + + for (int ii = 0; ii < segmentCount - 1; ++ii) { + int size = moreSizesRaw.getInt(ii * 4); + moreSizes[ii] = size; + totalWords += size; + } + + readSegments(totalWords, segmentCount, segment0Size, moreSizes); + }); + }); + } + + private void readSegments(int totalWords, int segmentCount, int segment0Size, int[] moreSizes) { + if (totalWords > options.traversalLimitInWords) { + readCompleted.completeExceptionally( + new DecodeException("Message size exceeds traversal limit.")); + return; + } + + var segmentSlices = new ByteBuffer[segmentCount]; + if (totalWords == 0) { + for (int ii = 0; ii < segmentCount; ++ii) { + segmentSlices[ii] = ByteBuffer.allocate(0); + } + readCompleted.complete(new MessageReader(segmentSlices, options)); + return; + } + + read(totalWords * Constants.BYTES_PER_WORD, allSegments -> { + allSegments.rewind(); + var segment0 = allSegments.slice(); + segment0.limit(segment0Size * Constants.BYTES_PER_WORD); + segment0.order(ByteOrder.LITTLE_ENDIAN); + segmentSlices[0] = segment0; + + int offset = segment0Size; + for (int ii = 1; ii < segmentCount; ++ii) { + allSegments.position(offset * Constants.BYTES_PER_WORD); + var segmentSize = moreSizes[ii-1]; + var segment = allSegments.slice(); + segment.limit(segmentSize * Constants.BYTES_PER_WORD); + segment.order(ByteOrder.LITTLE_ENDIAN); + segmentSlices[ii] = segment; + offset += segmentSize; + } + + readCompleted.complete(new MessageReader(segmentSlices, options)); + }); + } + + abstract void read(int bytes, Consumer consumer); + } + + static class AsynchronousByteChannelReader extends AsyncMessageReader { + private final AsynchronousByteChannel channel; + + AsynchronousByteChannelReader(AsynchronousByteChannel channel, ReaderOptions options) { + super(options); + this.channel = channel; + } + + void read(int bytes, Consumer consumer) { + var buffer = Serialize.makeByteBuffer(bytes); + var handler = new CompletionHandler() { + @Override + public void completed(Integer result, Object attachment) { + //System.out.println(channel.toString() + ": read " + result + " bytes"); + if (result <= 0) { + var text = result == 0 + ? "Read zero bytes. Is the channel in non-blocking mode?" + : "Premature EOF"; + readCompleted.completeExceptionally(new IOException(text)); + } else if (buffer.hasRemaining()) { + // partial read + channel.read(buffer, null, this); + } else { + consumer.accept(buffer); + } + } + + @Override + public void failed(Throwable exc, Object attachment) { + readCompleted.completeExceptionally(exc); + } + }; + + this.channel.read(buffer, null, handler); + } + } + + public static CompletableFuture readAsync(AsynchronousByteChannel channel) { + return readAsync(channel, ReaderOptions.DEFAULT_READER_OPTIONS); + } + + public static CompletableFuture readAsync(AsynchronousByteChannel channel, ReaderOptions options) { + return new AsynchronousByteChannelReader(channel, options).getMessage(); + } + + public static CompletableFuture writeAsync(AsynchronousByteChannel outputChannel, MessageBuilder message) { + var writeCompleted = new CompletableFuture(); + var segments = message.getSegmentsForOutput(); + var header = getHeaderForOutput(segments); + + outputChannel.write(header, -1, new CompletionHandler<>() { + @Override + public void completed(Integer result, Integer index) { + var currentSegment = index < 0 ? header : segments[index]; + + if (result < 0) { + writeCompleted.completeExceptionally(new IOException("Write failed")); + } + else if (currentSegment.hasRemaining()) { + // partial write + outputChannel.write(currentSegment, index, this); + } + else { + index++; + if (index == segments.length) { + writeCompleted.complete(null); + } + else { + outputChannel.write(segments[index], index, this); + } + } + } + + @Override + public void failed(Throwable exc, Integer attachment) { + writeCompleted.completeExceptionally(exc); + } + }); + + return writeCompleted; + } + + private static ByteBuffer getHeaderForOutput(ByteBuffer[] segments) { + assert segments.length > 0: "Empty message"; + int tableSize = (segments.length + 2) & (~1); + var table = ByteBuffer.allocate(4 * tableSize); + table.order(ByteOrder.LITTLE_ENDIAN); + table.putInt(0, segments.length - 1); + for (int ii = 0; ii < segments.length; ++ii) { + table.putInt(4 * (ii + 1), segments[ii].limit() / 8); + } + return table; + } } diff --git a/runtime/src/main/java/org/capnproto/SetPointerBuilder.java b/runtime/src/main/java/org/capnproto/SetPointerBuilder.java index 636c3a23..cdc7f72d 100644 --- a/runtime/src/main/java/org/capnproto/SetPointerBuilder.java +++ b/runtime/src/main/java/org/capnproto/SetPointerBuilder.java @@ -22,5 +22,8 @@ package org.capnproto; public interface SetPointerBuilder { - void setPointerBuilder(SegmentBuilder segment, int pointer, Reader value); + default void setPointerBuilder(SegmentBuilder segment, int pointer, Reader value) { + setPointerBuilder(segment, null, pointer, value); + } + void setPointerBuilder(SegmentBuilder segment, CapTableBuilder capTable, int pointer, Reader value); } diff --git a/runtime/src/main/java/org/capnproto/StreamingCallContext.java b/runtime/src/main/java/org/capnproto/StreamingCallContext.java new file mode 100644 index 00000000..a4128c01 --- /dev/null +++ b/runtime/src/main/java/org/capnproto/StreamingCallContext.java @@ -0,0 +1,21 @@ +package org.capnproto; + +public class StreamingCallContext { + + private final FromPointerReader paramsFactory; + private final CallContextHook hook; + + public StreamingCallContext(FromPointerReader paramsFactory, + CallContextHook hook) { + this.paramsFactory = paramsFactory; + this.hook = hook; + } + + public final Params getParams() { + return this.hook.getParams().getAs(paramsFactory); + } + + public final void allowCancellation() { + this.hook.allowCancellation(); + } +} diff --git a/runtime/src/main/java/org/capnproto/StreamingRequest.java b/runtime/src/main/java/org/capnproto/StreamingRequest.java new file mode 100644 index 00000000..1cf63857 --- /dev/null +++ b/runtime/src/main/java/org/capnproto/StreamingRequest.java @@ -0,0 +1,23 @@ +package org.capnproto; + +import java.util.concurrent.CompletableFuture; + +public interface StreamingRequest { + + FromPointerBuilder getParamsFactory(); + + StreamingRequest getTypelessRequest(); + + default Params getParams() { + return this.getTypelessRequest().getParams().getAs(this.getParamsFactory()); + } + + default RequestHook getHook() { + return this.getTypelessRequest().getHook(); + } + + default CompletableFuture send() { + return this.getHook().sendStreaming(); + } +} + diff --git a/runtime/src/main/java/org/capnproto/StructBuilder.java b/runtime/src/main/java/org/capnproto/StructBuilder.java index 8c3e4f78..f9543771 100644 --- a/runtime/src/main/java/org/capnproto/StructBuilder.java +++ b/runtime/src/main/java/org/capnproto/StructBuilder.java @@ -21,10 +21,18 @@ package org.capnproto; -public class StructBuilder { +public class StructBuilder extends CapTableBuilder.BuilderContext { public interface Factory { T constructBuilder(SegmentBuilder segment, int data, int pointers, int dataSize, short pointerCount); + default T constructBuilder(SegmentBuilder segment, CapTableBuilder capTable, int data, int pointers, int dataSize, + short pointerCount) { + T result = constructBuilder(segment, data, pointers, dataSize, pointerCount); + if (result instanceof CapTableBuilder.BuilderContext) { + ((CapTableBuilder.BuilderContext) result).capTable = capTable; + } + return result; + } StructSize structSize(); } @@ -36,11 +44,17 @@ T constructBuilder(SegmentBuilder segment, int data, int pointers, int dataSize, public StructBuilder(SegmentBuilder segment, int data, int pointers, int dataSize, short pointerCount) { + this(segment, null, data, pointers, dataSize, pointerCount); + } + + public StructBuilder(SegmentBuilder segment, CapTableBuilder capTable, int data, + int pointers, int dataSize, short pointerCount) { this.segment = segment; this.data = data; this.pointers = pointers; this.dataSize = dataSize; this.pointerCount = pointerCount; + this.capTable = capTable; } protected final boolean _getBooleanField(int offset) { @@ -172,30 +186,30 @@ protected final boolean _pointerFieldIsNull(int ptrIndex) { protected final void _clearPointerField(int ptrIndex) { int pointer = this.pointers + ptrIndex; - WireHelpers.zeroObject(this.segment, pointer); + WireHelpers.zeroObject(this.segment, this.capTable, pointer); this.segment.buffer.putLong(pointer * 8, 0L); } protected final T _getPointerField(FromPointerBuilder factory, int index) { - return factory.fromPointerBuilder(this.segment, this.pointers + index); + return factory.fromPointerBuilder(this.segment, this.capTable, this.pointers + index); } protected final T _getPointerField(FromPointerBuilderRefDefault factory, int index, SegmentReader defaultSegment, int defaultOffset) { - return factory.fromPointerBuilderRefDefault(this.segment, this.pointers + index, defaultSegment, defaultOffset); + return factory.fromPointerBuilderRefDefault(this.segment, this.capTable, this.pointers + index, defaultSegment, defaultOffset); } protected final T _getPointerField(FromPointerBuilderBlobDefault factory, int index, java.nio.ByteBuffer defaultBuffer, int defaultOffset, int defaultSize) { - return factory.fromPointerBuilderBlobDefault(this.segment, this.pointers + index, defaultBuffer, defaultOffset, defaultSize); + return factory.fromPointerBuilderBlobDefault(this.segment, this.capTable, this.pointers + index, defaultBuffer, defaultOffset, defaultSize); } protected final T _initPointerField(FromPointerBuilder factory, int index, int elementCount) { - return factory.initFromPointerBuilder(this.segment, this.pointers + index, elementCount); + return factory.initFromPointerBuilder(this.segment, this.capTable, this.pointers + index, elementCount); } protected final void _setPointerField(SetPointerBuilder factory, int index, Reader value) { - factory.setPointerBuilder(this.segment, this.pointers + index, value); + factory.setPointerBuilder(this.segment, this.capTable, this.pointers + index, value); } protected final void _copyContentFrom(StructReader other) { @@ -244,14 +258,16 @@ protected final void _copyContentFrom(StructReader other) { // Zero out all pointers in the target. for (int ii = 0; ii < this.pointerCount; ++ii) { - WireHelpers.zeroObject(this.segment, this.pointers + ii); + WireHelpers.zeroObject(this.segment, this.capTable, this.pointers + ii); } this.segment.buffer.putLong(this.pointers * Constants.BYTES_PER_WORD, 0); for (int ii = 0; ii < sharedPointerCount; ++ii) { WireHelpers.copyPointer(this.segment, + this.capTable, this.pointers + ii, other.segment, + other.capTable, other.pointers + ii, other.nestingLimit); } diff --git a/runtime/src/main/java/org/capnproto/StructFactory.java b/runtime/src/main/java/org/capnproto/StructFactory.java index 5ff08e0d..615cae70 100644 --- a/runtime/src/main/java/org/capnproto/StructFactory.java +++ b/runtime/src/main/java/org/capnproto/StructFactory.java @@ -21,40 +21,42 @@ package org.capnproto; -public abstract class StructFactory +public abstract class StructFactory implements PointerFactory, FromPointerBuilderRefDefault, StructBuilder.Factory, SetPointerBuilder, FromPointerReaderRefDefault, StructReader.Factory { - public final Reader fromPointerReaderRefDefault(SegmentReader segment, int pointer, + public final Reader fromPointerReaderRefDefault(SegmentReader segment, CapTableReader capTable, int pointer, SegmentReader defaultSegment, int defaultOffset, int nestingLimit) { return WireHelpers.readStructPointer(this, segment, + capTable, pointer, defaultSegment, defaultOffset, nestingLimit); } - public final Reader fromPointerReader(SegmentReader segment, int pointer, int nestingLimit) { - return fromPointerReaderRefDefault(segment, pointer, null, 0, nestingLimit); + + public final Reader fromPointerReader(SegmentReader segment, CapTableReader capTable, int pointer, int nestingLimit) { + return fromPointerReaderRefDefault(segment, capTable, pointer, null, 0, nestingLimit); } - public final Builder fromPointerBuilderRefDefault(SegmentBuilder segment, int pointer, + public final Builder fromPointerBuilderRefDefault(SegmentBuilder segment, CapTableBuilder capTable, int pointer, SegmentReader defaultSegment, int defaultOffset) { - return WireHelpers.getWritableStructPointer(this, pointer, segment, this.structSize(), + return WireHelpers.getWritableStructPointer(this, pointer, segment, capTable, this.structSize(), defaultSegment, defaultOffset); } - public final Builder fromPointerBuilder(SegmentBuilder segment, int pointer) { - return WireHelpers.getWritableStructPointer(this, pointer, segment, this.structSize(), - null, 0); + public final Builder fromPointerBuilder(SegmentBuilder segment, CapTableBuilder capTable, int pointer) { + return WireHelpers.getWritableStructPointer(this, pointer, segment, capTable, this.structSize(), + null, 0); } - public final Builder initFromPointerBuilder(SegmentBuilder segment, int pointer, int elementCount) { - return WireHelpers.initStructPointer(this, pointer, segment, this.structSize()); + public final Builder initFromPointerBuilder(SegmentBuilder segment, CapTableBuilder capTable, int pointer, int elementCount) { + return WireHelpers.initStructPointer(this, pointer, segment, capTable, this.structSize()); } - public final void setPointerBuilder(SegmentBuilder segment, int pointer, Reader value) { - WireHelpers.setStructPointer(segment, pointer, value); + public final void setPointerBuilder(SegmentBuilder segment, CapTableBuilder capTable, int pointer, Reader value) { + WireHelpers.setStructPointer(segment, capTable, pointer, value); } public abstract Reader asReader(Builder builder); diff --git a/runtime/src/main/java/org/capnproto/StructList.java b/runtime/src/main/java/org/capnproto/StructList.java index b97ad924..414fcb6d 100644 --- a/runtime/src/main/java/org/capnproto/StructList.java +++ b/runtime/src/main/java/org/capnproto/StructList.java @@ -56,9 +56,31 @@ public final Builder constructBuilder(SegmentBuilder segment, } @Override - public final Builder fromPointerBuilderRefDefault(SegmentBuilder segment, int pointer, + public final Reader constructReader(SegmentReader segment, + CapTableReader capTable, + int ptr, + int elementCount, int step, + int structDataSize, short structPointerCount, + int nestingLimit) { + return new Reader(factory, capTable, + segment, ptr, elementCount, step, structDataSize, structPointerCount, nestingLimit); + } + + @Override + public final Builder constructBuilder(SegmentBuilder segment, + CapTableBuilder capTable, + int ptr, + int elementCount, int step, + int structDataSize, short structPointerCount) { + return new Builder (factory, capTable, + segment, ptr, elementCount, step, structDataSize, structPointerCount); + } + + @Override + public final Builder fromPointerBuilderRefDefault(SegmentBuilder segment, CapTableBuilder capTable, int pointer, SegmentReader defaultSegment, int defaultOffset) { return WireHelpers.getWritableStructListPointer(this, + capTable, pointer, segment, factory.structSize(), @@ -67,8 +89,9 @@ public final Builder fromPointerBuilderRefDefault(SegmentBuilder } @Override - public final Builder fromPointerBuilder(SegmentBuilder segment, int pointer) { + public final Builder fromPointerBuilder(SegmentBuilder segment, CapTableBuilder capTable, int pointer) { return WireHelpers.getWritableStructListPointer(this, + capTable, pointer, segment, factory.structSize(), @@ -76,9 +99,9 @@ public final Builder fromPointerBuilder(SegmentBuilder segment, } @Override - public final Builder initFromPointerBuilder(SegmentBuilder segment, int pointer, + public final Builder initFromPointerBuilder(SegmentBuilder segment, CapTableBuilder capTable, int pointer, int elementCount) { - return WireHelpers.initStructListPointer(this, pointer, segment, elementCount, factory.structSize()); + return WireHelpers.initStructListPointer(this, capTable, pointer, segment, elementCount, factory.structSize()); } } @@ -95,6 +118,17 @@ public Reader(StructReader.Factory factory, this.factory = factory; } + public Reader(StructReader.Factory factory, + CapTableReader capTable, + SegmentReader segment, + int ptr, + int elementCount, int step, + int structDataSize, short structPointerCount, + int nestingLimit) { + super(segment, capTable, ptr, elementCount, step, structDataSize, structPointerCount, nestingLimit); + this.factory = factory; + } + public T get(int index) { return _getStructElement(factory, index); } @@ -130,7 +164,15 @@ public Builder(StructBuilder.Factory factory, SegmentBuilder segment, int ptr, int elementCount, int step, int structDataSize, short structPointerCount){ - super(segment, ptr, elementCount, step, structDataSize, structPointerCount); + this(factory, null, segment, ptr, elementCount, step, structDataSize, structPointerCount); + } + + public Builder(StructBuilder.Factory factory, + CapTableBuilder capTable, + SegmentBuilder segment, int ptr, + int elementCount, int step, + int structDataSize, short structPointerCount) { + super(segment, capTable, ptr, elementCount, step, structDataSize, structPointerCount); this.factory = factory; } diff --git a/runtime/src/main/java/org/capnproto/StructReader.java b/runtime/src/main/java/org/capnproto/StructReader.java index 8a21a216..14532a65 100644 --- a/runtime/src/main/java/org/capnproto/StructReader.java +++ b/runtime/src/main/java/org/capnproto/StructReader.java @@ -21,11 +21,20 @@ package org.capnproto; -public class StructReader { +public class StructReader extends CapTableReader.ReaderContext { public interface Factory { - abstract T constructReader(SegmentReader segment, int data, int pointers, + T constructReader(SegmentReader segment, int data, int pointers, int dataSize, short pointerCount, int nestingLimit); + default T constructReader(SegmentReader segment, CapTableReader capTable, int data, int pointers, + int dataSize, short pointerCount, + int nestingLimit) { + T result = constructReader(segment, data, pointers, dataSize, pointerCount, nestingLimit); + if (result instanceof CapTableReader.ReaderContext) { + ((CapTableReader.ReaderContext) result).capTable = capTable; + } + return result; + } } protected final SegmentReader segment; @@ -42,17 +51,26 @@ public StructReader() { this.dataSize = 0; this.pointerCount = 0; this.nestingLimit = 0x7fffffff; + this.capTable = null; } public StructReader(SegmentReader segment, int data, int pointers, int dataSize, short pointerCount, int nestingLimit) { + this(segment, null, data, pointers, dataSize, pointerCount, nestingLimit); + } + + public StructReader(SegmentReader segment, CapTableReader capTable, + int data, + int pointers, int dataSize, short pointerCount, + int nestingLimit) { this.segment = segment; this.data = data; this.pointers = pointers; this.dataSize = dataSize; this.pointerCount = pointerCount; this.nestingLimit = nestingLimit; + this.capTable = capTable; } protected final boolean _getBooleanField(int offset) { @@ -157,10 +175,12 @@ protected final boolean _pointerFieldIsNull(int ptrIndex) { protected final T _getPointerField(FromPointerReader factory, int ptrIndex) { if (ptrIndex < this.pointerCount) { return factory.fromPointerReader(this.segment, + this.capTable, this.pointers + ptrIndex, this.nestingLimit); } else { return factory.fromPointerReader(SegmentReader.EMPTY, + this.capTable, 0, this.nestingLimit); } @@ -171,12 +191,14 @@ protected final T _getPointerField(FromPointerReaderRefDefault factory, i SegmentReader defaultSegment, int defaultOffset) { if (ptrIndex < this.pointerCount) { return factory.fromPointerReaderRefDefault(this.segment, + this.capTable, this.pointers + ptrIndex, defaultSegment, defaultOffset, this.nestingLimit); } else { return factory.fromPointerReaderRefDefault(SegmentReader.EMPTY, + this.capTable, 0, defaultSegment, defaultOffset, @@ -200,5 +222,4 @@ protected final T _getPointerField(FromPointerReaderBlobDefault factory, defaultSize); } } - } diff --git a/runtime/src/main/java/org/capnproto/Text.java b/runtime/src/main/java/org/capnproto/Text.java index fdf62cc4..50d15f4f 100644 --- a/runtime/src/main/java/org/capnproto/Text.java +++ b/runtime/src/main/java/org/capnproto/Text.java @@ -37,35 +37,37 @@ public final Reader fromPointerReaderBlobDefault(SegmentReader segment, int poin } @Override - public final Reader fromPointerReader(SegmentReader segment, int pointer, int nestingLimit) { + public final Reader fromPointerReader(SegmentReader segment, CapTableReader capTable, int pointer, int nestingLimit) { return WireHelpers.readTextPointer(segment, pointer, null, 0, 0); } @Override - public final Builder fromPointerBuilderBlobDefault(SegmentBuilder segment, int pointer, - java.nio.ByteBuffer defaultBuffer, int defaultOffset, int defaultSize) { + public final Builder fromPointerBuilderBlobDefault(SegmentBuilder segment, CapTableBuilder capTable, int pointer, + java.nio.ByteBuffer defaultBuffer, int defaultOffset, int defaultSize) { return WireHelpers.getWritableTextPointer(pointer, segment, + capTable, defaultBuffer, defaultOffset, defaultSize); } @Override - public final Builder fromPointerBuilder(SegmentBuilder segment, int pointer) { + public final Builder fromPointerBuilder(SegmentBuilder segment, CapTableBuilder capTable, int pointer) { return WireHelpers.getWritableTextPointer(pointer, segment, + capTable, null, 0, 0); } @Override - public final Builder initFromPointerBuilder(SegmentBuilder segment, int pointer, int size) { - return WireHelpers.initTextPointer(pointer, segment, size); + public Builder initFromPointerBuilder(SegmentBuilder segment, CapTableBuilder capTable, int pointer, int size) { + return WireHelpers.initTextPointer(pointer, segment, capTable, size); } @Override - public final void setPointerBuilder(SegmentBuilder segment, int pointer, Reader value) { - WireHelpers.setTextPointer(pointer, segment, value); + public void setPointerBuilder(SegmentBuilder segment, CapTableBuilder capTable, int pointer, Reader value) { + WireHelpers.setTextPointer(pointer, segment, capTable, value); } } public static final Factory factory = new Factory(); diff --git a/runtime/src/main/java/org/capnproto/WireHelpers.java b/runtime/src/main/java/org/capnproto/WireHelpers.java index 8df40a57..dd0a63a0 100644 --- a/runtime/src/main/java/org/capnproto/WireHelpers.java +++ b/runtime/src/main/java/org/capnproto/WireHelpers.java @@ -57,12 +57,13 @@ static class AllocateResult { static AllocateResult allocate(int refOffset, SegmentBuilder segment, + CapTableBuilder capTable, int amount, // in words byte kind) { long ref = segment.get(refOffset); if (!WirePointer.isNull(ref)) { - zeroObject(segment, refOffset); + zeroObject(segment, capTable, refOffset); } if (amount == 0 && kind == WirePointer.STRUCT) { @@ -182,7 +183,7 @@ static FollowFarsResult followFars(long ref, int refTarget, SegmentReader segmen } } - static void zeroObject(SegmentBuilder segment, int refOffset) { + static void zeroObject(SegmentBuilder segment, CapTableBuilder capTable, int refOffset) { //# Zero out the pointed-to object. Use when the pointer is //# about to be overwritten making the target object no longer //# reachable. @@ -195,7 +196,7 @@ static void zeroObject(SegmentBuilder segment, int refOffset) { switch (WirePointer.kind(ref)) { case WirePointer.STRUCT: case WirePointer.LIST: - zeroObject(segment, ref, WirePointer.target(refOffset, ref)); + zeroObject(segment, capTable, ref, WirePointer.target(refOffset, ref)); break; case WirePointer.FAR: { segment = segment.getArena().getSegment(FarPointer.getSegmentId(ref)); @@ -205,13 +206,13 @@ static void zeroObject(SegmentBuilder segment, int refOffset) { if (FarPointer.isDoubleFar(ref)) { SegmentBuilder otherSegment = segment.getArena().getSegment(FarPointer.getSegmentId(ref)); if (otherSegment.isWritable()) { - zeroObject(otherSegment, padOffset + 1, FarPointer.positionInSegment(pad)); + zeroObject(otherSegment, capTable, padOffset + 1, FarPointer.positionInSegment(pad)); } segment.buffer.putLong(padOffset * 8, 0L); segment.buffer.putLong((padOffset + 1) * 8, 0L); } else { - zeroObject(segment, padOffset); + zeroObject(segment, capTable, padOffset); segment.buffer.putLong(padOffset * 8, 0L); } } @@ -219,12 +220,20 @@ static void zeroObject(SegmentBuilder segment, int refOffset) { break; } case WirePointer.OTHER: { - // TODO + assert WirePointer.isCapability(ref) : "Unknown pointer type"; + if (WirePointer.isCapability(ref)) { + var capIndex = WirePointer.upper32Bits(ref); + assert capTable != null: "Cannot zero out capability pointer with no capTable"; + if (capTable != null) { + capTable.dropCap(capIndex); + } + } + break; } } } - static void zeroObject(SegmentBuilder segment, long tag, int ptr) { + static void zeroObject(SegmentBuilder segment, CapTableBuilder capTable, long tag, int ptr) { //# We shouldn't zero out external data linked into the message. if (!segment.isWritable()) return; @@ -233,7 +242,7 @@ static void zeroObject(SegmentBuilder segment, long tag, int ptr) { int pointerSection = ptr + StructPointer.dataSize(tag); int count = StructPointer.ptrCount(tag); for (int ii = 0; ii < count; ++ii) { - zeroObject(segment, pointerSection + ii); + zeroObject(segment, capTable, pointerSection + ii); } memset(segment.buffer, ptr * Constants.BYTES_PER_WORD, (byte)0, StructPointer.wordSize(tag) * Constants.BYTES_PER_WORD); @@ -256,7 +265,7 @@ static void zeroObject(SegmentBuilder segment, long tag, int ptr) { case ElementSize.POINTER: { int count = ListPointer.elementCount(tag); for (int ii = 0; ii < count; ++ii) { - zeroObject(segment, ptr + ii); + zeroObject(segment, capTable, ptr + ii); } memset(segment.buffer, ptr * Constants.BYTES_PER_WORD, (byte)0, count * Constants.BYTES_PER_WORD); @@ -275,7 +284,7 @@ static void zeroObject(SegmentBuilder segment, long tag, int ptr) { for (int ii = 0; ii < count; ++ii) { pos += dataSize; for (int jj = 0; jj < pointerCount; ++jj) { - zeroObject(segment, pos); + zeroObject(segment, capTable, pos); pos += Constants.POINTER_SIZE_IN_WORDS; } } @@ -400,9 +409,17 @@ static T initStructPointer(StructBuilder.Factory factory, int refOffset, SegmentBuilder segment, StructSize size) { - AllocateResult allocation = allocate(refOffset, segment, size.total(), WirePointer.STRUCT); + return initStructPointer(factory, refOffset, segment, null, size); + } + + static T initStructPointer(StructBuilder.Factory factory, + int refOffset, + SegmentBuilder segment, + CapTableBuilder capTable, + StructSize size) { + AllocateResult allocation = allocate(refOffset, segment, capTable, size.total(), WirePointer.STRUCT); StructPointer.setFromStructSize(allocation.segment.buffer, allocation.refOffset, size); - return factory.constructBuilder(allocation.segment, allocation.ptr * Constants.BYTES_PER_WORD, + return factory.constructBuilder(allocation.segment, capTable, allocation.ptr * Constants.BYTES_PER_WORD, allocation.ptr + size.data, size.data * 64, size.pointers); } @@ -410,6 +427,7 @@ static T initStructPointer(StructBuilder.Factory factory, static T getWritableStructPointer(StructBuilder.Factory factory, int refOffset, SegmentBuilder segment, + CapTableBuilder capTable, StructSize size, SegmentReader defaultSegment, int defaultOffset) { @@ -417,7 +435,7 @@ static T getWritableStructPointer(StructBuilder.Factory factory, int target = WirePointer.target(refOffset, ref); if (WirePointer.isNull(ref)) { if (defaultSegment == null) { - return initStructPointer(factory, refOffset, segment, size); + return initStructPointer(factory, refOffset, segment, capTable, size); } else { throw new RuntimeException("unimplemented"); } @@ -440,7 +458,7 @@ static T getWritableStructPointer(StructBuilder.Factory factory, //# Don't let allocate() zero out the object just yet. zeroPointerAndFars(segment, refOffset); - AllocateResult allocation = allocate(refOffset, segment, + AllocateResult allocation = allocate(refOffset, segment, capTable, totalSize, WirePointer.STRUCT); StructPointer.set(allocation.segment.buffer, allocation.refOffset, @@ -466,11 +484,11 @@ static T getWritableStructPointer(StructBuilder.Factory factory, memset(resolved.segment.buffer, resolved.ptr * Constants.BYTES_PER_WORD, (byte)0, (oldDataSize + oldPointerCount * Constants.WORDS_PER_POINTER) * Constants.BYTES_PER_WORD); - return factory.constructBuilder(allocation.segment, allocation.ptr * Constants.BYTES_PER_WORD, + return factory.constructBuilder(allocation.segment, capTable, allocation.ptr * Constants.BYTES_PER_WORD, newPointerSection, newDataSize * Constants.BITS_PER_WORD, newPointerCount); } else { - return factory.constructBuilder(resolved.segment, resolved.ptr * Constants.BYTES_PER_WORD, + return factory.constructBuilder(resolved.segment, capTable, resolved.ptr * Constants.BYTES_PER_WORD, oldPointerSection, oldDataSize * Constants.BITS_PER_WORD, (short)oldPointerCount); } @@ -478,6 +496,7 @@ static T getWritableStructPointer(StructBuilder.Factory factory, } static T initListPointer(ListBuilder.Factory factory, + CapTableBuilder capTable, int refOffset, SegmentBuilder segment, int elementCount, @@ -488,16 +507,17 @@ static T initListPointer(ListBuilder.Factory factory, int pointerCount = ElementSize.pointersPerElement(elementSize); int step = dataSize + pointerCount * Constants.BITS_PER_POINTER; int wordCount = roundBitsUpToWords((long)elementCount * (long)step); - AllocateResult allocation = allocate(refOffset, segment, wordCount, WirePointer.LIST); + AllocateResult allocation = allocate(refOffset, segment, capTable, wordCount, WirePointer.LIST); ListPointer.set(allocation.segment.buffer, allocation.refOffset, elementSize, elementCount); - return factory.constructBuilder(allocation.segment, + return factory.constructBuilder(allocation.segment, capTable, allocation.ptr * Constants.BYTES_PER_WORD, elementCount, step, dataSize, (short)pointerCount); } static T initStructListPointer(ListBuilder.Factory factory, + CapTableBuilder capTable, int refOffset, SegmentBuilder segment, int elementCount, @@ -506,7 +526,7 @@ static T initStructListPointer(ListBuilder.Factory factory, //# Allocate the list, prefixed by a single WirePointer. int wordCount = elementCount * wordsPerElement; - AllocateResult allocation = allocate(refOffset, segment, Constants.POINTER_SIZE_IN_WORDS + wordCount, + AllocateResult allocation = allocate(refOffset, segment, capTable, Constants.POINTER_SIZE_IN_WORDS + wordCount, WirePointer.LIST); //# Initialize the pointer. @@ -515,7 +535,7 @@ static T initStructListPointer(ListBuilder.Factory factory, WirePointer.STRUCT, elementCount); StructPointer.setFromStructSize(allocation.segment.buffer, allocation.ptr, elementSize); - return factory.constructBuilder(allocation.segment, + return factory.constructBuilder(allocation.segment, capTable, (allocation.ptr + 1) * Constants.BYTES_PER_WORD, elementCount, wordsPerElement * Constants.BITS_PER_WORD, elementSize.data * Constants.BITS_PER_WORD, elementSize.pointers); @@ -524,6 +544,7 @@ static T initStructListPointer(ListBuilder.Factory factory, static T getWritableListPointer(ListBuilder.Factory factory, int origRefOffset, SegmentBuilder origSegment, + CapTableBuilder capTable, byte elementSize, SegmentReader defaultSegment, int defaultOffset) { @@ -573,13 +594,14 @@ static T getWritableListPointer(ListBuilder.Factory factory, int step = dataSize + pointerCount * Constants.BITS_PER_POINTER; - return factory.constructBuilder(resolved.segment, resolved.ptr * Constants.BYTES_PER_WORD, + return factory.constructBuilder(resolved.segment, capTable, resolved.ptr * Constants.BYTES_PER_WORD, ListPointer.elementCount(resolved.ref), step, dataSize, (short) pointerCount); } } static T getWritableStructListPointer(ListBuilder.Factory factory, + CapTableBuilder capTable, int origRefOffset, SegmentBuilder origSegment, StructSize elementSize, @@ -615,7 +637,7 @@ static T getWritableStructListPointer(ListBuilder.Factory factory, if (oldDataSize >= elementSize.data && oldPointerCount >= elementSize.pointers) { //# Old size is at least as large as we need. Ship it. - return factory.constructBuilder(resolved.segment, oldPtr * Constants.BYTES_PER_WORD, + return factory.constructBuilder(resolved.segment, capTable, oldPtr * Constants.BYTES_PER_WORD, elementCount, oldStep * Constants.BITS_PER_WORD, oldDataSize * Constants.BITS_PER_WORD, @@ -633,7 +655,7 @@ static T getWritableStructListPointer(ListBuilder.Factory factory, //# Don't let allocate() zero out the object just yet. zeroPointerAndFars(origSegment, origRefOffset); - AllocateResult allocation = allocate(origRefOffset, origSegment, + AllocateResult allocation = allocate(origRefOffset, origSegment, capTable, totalSize + Constants.POINTER_SIZE_IN_WORDS, WirePointer.LIST); @@ -672,7 +694,7 @@ static T getWritableStructListPointer(ListBuilder.Factory factory, memset(resolved.segment.buffer, resolved.ptr * Constants.BYTES_PER_WORD, (byte)0, (1 + oldStep * elementCount) * Constants.BYTES_PER_WORD); - return factory.constructBuilder(allocation.segment, newPtr * Constants.BYTES_PER_WORD, + return factory.constructBuilder(allocation.segment, capTable, newPtr * Constants.BYTES_PER_WORD, elementCount, newStep * Constants.BITS_PER_WORD, newDataSize * Constants.BITS_PER_WORD, @@ -687,7 +709,7 @@ static T getWritableStructListPointer(ListBuilder.Factory factory, if (oldSize == ElementSize.VOID) { //# Nothing to copy, just allocate a new list. - return initStructListPointer(factory, origRefOffset, origSegment, + return initStructListPointer(factory, capTable, origRefOffset, origSegment, elementCount, elementSize); } else { //# Upgrading to an inline composite list. @@ -713,7 +735,7 @@ static T getWritableStructListPointer(ListBuilder.Factory factory, //# Don't let allocate() zero out the object just yet. zeroPointerAndFars(origSegment, origRefOffset); - AllocateResult allocation = allocate(origRefOffset, origSegment, + AllocateResult allocation = allocate(origRefOffset, origSegment, capTable, totalWords + Constants.POINTER_SIZE_IN_WORDS, WirePointer.LIST); @@ -751,7 +773,7 @@ static T getWritableStructListPointer(ListBuilder.Factory factory, memset(resolved.segment.buffer, resolved.ptr * Constants.BYTES_PER_WORD, (byte)0, roundBitsUpToBytes(oldStep * elementCount)); - return factory.constructBuilder(allocation.segment, newPtr * Constants.BYTES_PER_WORD, + return factory.constructBuilder(allocation.segment, capTable, newPtr * Constants.BYTES_PER_WORD, elementCount, newStep * Constants.BITS_PER_WORD, newDataSize * Constants.BITS_PER_WORD, @@ -763,12 +785,13 @@ static T getWritableStructListPointer(ListBuilder.Factory factory, // size is in bytes static Text.Builder initTextPointer(int refOffset, SegmentBuilder segment, + CapTableBuilder capTable, int size) { //# The byte list must include a NUL terminator. int byteSize = size + 1; //# Allocate the space. - AllocateResult allocation = allocate(refOffset, segment, roundBytesUpToWords(byteSize), + AllocateResult allocation = allocate(refOffset, segment, capTable, roundBytesUpToWords(byteSize), WirePointer.LIST); //# Initialize the pointer. @@ -779,8 +802,9 @@ static Text.Builder initTextPointer(int refOffset, static Text.Builder setTextPointer(int refOffset, SegmentBuilder segment, + CapTableBuilder capTable, Text.Reader value) { - Text.Builder builder = initTextPointer(refOffset, segment, value.size); + Text.Builder builder = initTextPointer(refOffset, segment, capTable, value.size); ByteBuffer slice = value.buffer.duplicate(); slice.position(value.offset); @@ -792,6 +816,7 @@ static Text.Builder setTextPointer(int refOffset, static Text.Builder getWritableTextPointer(int refOffset, SegmentBuilder segment, + CapTableBuilder capTable, ByteBuffer defaultBuffer, int defaultOffset, int defaultSize) { @@ -801,13 +826,14 @@ static Text.Builder getWritableTextPointer(int refOffset, if (defaultBuffer == null) { return new Text.Builder(); } else { - Text.Builder builder = initTextPointer(refOffset, segment, defaultSize); + Text.Builder builder = initTextPointer(refOffset, segment, capTable, defaultSize); ByteBuffer slice = defaultBuffer.duplicate(); slice.position(defaultOffset * 8); slice.limit(defaultOffset * 8 + defaultSize); builder.buffer.position(builder.offset); builder.buffer.put(slice); + return builder; } } @@ -837,9 +863,10 @@ static Text.Builder getWritableTextPointer(int refOffset, // size is in bytes static Data.Builder initDataPointer(int refOffset, SegmentBuilder segment, + CapTableBuilder capTable, int size) { //# Allocate the space. - AllocateResult allocation = allocate(refOffset, segment, roundBytesUpToWords(size), + AllocateResult allocation = allocate(refOffset, segment, capTable, roundBytesUpToWords(size), WirePointer.LIST); //# Initialize the pointer. @@ -850,8 +877,9 @@ static Data.Builder initDataPointer(int refOffset, static Data.Builder setDataPointer(int refOffset, SegmentBuilder segment, + CapTableBuilder capTable, Data.Reader value) { - Data.Builder builder = initDataPointer(refOffset, segment, value.size); + Data.Builder builder = initDataPointer(refOffset, segment, capTable, value.size); ByteBuffer slice = value.buffer.duplicate(); slice.position(value.offset); @@ -864,6 +892,7 @@ static Data.Builder setDataPointer(int refOffset, static Data.Builder getWritableDataPointer(int refOffset, SegmentBuilder segment, + CapTableBuilder capTable, ByteBuffer defaultBuffer, int defaultOffset, int defaultSize) { @@ -873,13 +902,14 @@ static Data.Builder getWritableDataPointer(int refOffset, if (defaultBuffer == null) { return new Data.Builder(); } else { - Data.Builder builder = initDataPointer(refOffset, segment, defaultSize); + Data.Builder builder = initDataPointer(refOffset, segment, capTable, defaultSize); ByteBuffer slice = defaultBuffer.duplicate(); slice.position(defaultOffset * 8); slice.limit(defaultOffset * 8 + defaultSize); builder.buffer.position(builder.offset); builder.buffer.put(slice); + return builder; } } @@ -902,6 +932,7 @@ static Data.Builder getWritableDataPointer(int refOffset, static T readStructPointer(StructReader.Factory factory, SegmentReader segment, + CapTableReader capTable, int refOffset, SegmentReader defaultSegment, int defaultOffset, @@ -938,19 +969,61 @@ static T readStructPointer(StructReader.Factory factory, } return factory.constructReader(resolved.segment, - resolved.ptr * Constants.BYTES_PER_WORD, - (resolved.ptr + dataSizeWords), - dataSizeWords * Constants.BITS_PER_WORD, - (short) ptrCount, - nestingLimit - 1); + capTable, + resolved.ptr * Constants.BYTES_PER_WORD, + (resolved.ptr + dataSizeWords), + dataSizeWords * Constants.BITS_PER_WORD, + (short)StructPointer.ptrCount(resolved.ref), + nestingLimit - 1); + } + + static StructReader readStructPointer(SegmentReader segment, + CapTableReader capTable, + int refOffset, + SegmentReader defaultSegment, + int defaultOffset, + int nestingLimit) { + long ref = segment.get(refOffset); + if (WirePointer.isNull(ref)) { + if (defaultSegment == null) { + return new StructReader(SegmentReader.EMPTY, 0, 0, 0, (short) 0, 0x7fffffff); + } else { + segment = defaultSegment; + refOffset = defaultOffset; + ref = segment.get(refOffset); + } + } + + if (nestingLimit <= 0) { + throw new DecodeException("Message is too deeply nested or contains cycles."); + } + + int refTarget = WirePointer.target(refOffset, ref); + FollowFarsResult resolved = followFars(ref, refTarget, segment); + + int dataSizeWords = StructPointer.dataSize(resolved.ref); + + if (WirePointer.kind(resolved.ref) != WirePointer.STRUCT) { + throw new DecodeException("Message contains non-struct pointer where struct pointer was expected."); + } + + resolved.segment.arena.checkReadLimit(StructPointer.wordSize(resolved.ref)); + + return new StructReader(resolved.segment, + capTable, + resolved.ptr * Constants.BYTES_PER_WORD, + (resolved.ptr + dataSizeWords), + dataSizeWords * Constants.BITS_PER_WORD, + (short)StructPointer.ptrCount(resolved.ref), + nestingLimit - 1); } - static SegmentBuilder setStructPointer(SegmentBuilder segment, int refOffset, StructReader value) { + static SegmentBuilder setStructPointer(SegmentBuilder segment, CapTableBuilder capTable, int refOffset, StructReader value) { int dataSize = roundBitsUpToWords(value.dataSize); int totalSize = dataSize + value.pointerCount * Constants.POINTER_SIZE_IN_WORDS; - AllocateResult allocation = allocate(refOffset, segment, totalSize, WirePointer.STRUCT); + AllocateResult allocation = allocate(refOffset, segment, capTable, totalSize, WirePointer.STRUCT); StructPointer.set(allocation.segment.buffer, allocation.refOffset, (short)dataSize, value.pointerCount); @@ -963,25 +1036,25 @@ static SegmentBuilder setStructPointer(SegmentBuilder segment, int refOffset, St int pointerSection = allocation.ptr + dataSize; for (int i = 0; i < value.pointerCount; ++i) { - copyPointer(allocation.segment, pointerSection + i, value.segment, value.pointers + i, + copyPointer(allocation.segment, capTable, pointerSection + i, value.segment, value.capTable, value.pointers + i, value.nestingLimit); } return allocation.segment; }; - static SegmentBuilder setListPointer(SegmentBuilder segment, int refOffset, ListReader value) { + static SegmentBuilder setListPointer(SegmentBuilder segment, CapTableBuilder capTable, int refOffset, ListReader value) { int totalSize = roundBitsUpToWords((long) value.elementCount * value.step); if (value.step <= Constants.BITS_PER_WORD) { //# List of non-structs. - AllocateResult allocation = allocate(refOffset, segment, totalSize, WirePointer.LIST); + AllocateResult allocation = allocate(refOffset, segment, capTable, totalSize, WirePointer.LIST); if (value.structPointerCount == 1) { //# List of pointers. ListPointer.set(allocation.segment.buffer, allocation.refOffset, ElementSize.POINTER, value.elementCount); for (int i = 0; i < value.elementCount; ++i) { - copyPointer(allocation.segment, allocation.ptr + i, - value.segment, value.ptr / Constants.BYTES_PER_WORD + i, value.nestingLimit); + copyPointer(allocation.segment, capTable,allocation.ptr + i, + value.segment, value.capTable, value.ptr / Constants.BYTES_PER_WORD + i, value.nestingLimit); } } else { //# List of data. @@ -1004,7 +1077,7 @@ static SegmentBuilder setListPointer(SegmentBuilder segment, int refOffset, List return allocation.segment; } else { //# List of structs. - AllocateResult allocation = allocate(refOffset, segment, totalSize + Constants.POINTER_SIZE_IN_WORDS, WirePointer.LIST); + AllocateResult allocation = allocate(refOffset, segment, capTable, totalSize + Constants.POINTER_SIZE_IN_WORDS, WirePointer.LIST); ListPointer.setInlineComposite(allocation.segment.buffer, allocation.refOffset, totalSize); short dataSize = (short)roundBitsUpToWords(value.structDataSize); short pointerCount = value.structPointerCount; @@ -1025,7 +1098,7 @@ static SegmentBuilder setListPointer(SegmentBuilder segment, int refOffset, List srcOffset += dataSize; for (int j = 0; j < pointerCount; ++j) { - copyPointer(allocation.segment, dstOffset, value.segment, srcOffset, value.nestingLimit); + copyPointer(allocation.segment, capTable, dstOffset, value.segment, value.capTable, srcOffset, value.nestingLimit); dstOffset += Constants.POINTER_SIZE_IN_WORDS; srcOffset += Constants.POINTER_SIZE_IN_WORDS; } @@ -1051,8 +1124,8 @@ static void memcpy(ByteBuffer dstBuffer, int dstByteOffset, ByteBuffer srcBuffer dstDup.put(srcDup); } - static SegmentBuilder copyPointer(SegmentBuilder dstSegment, int dstOffset, - SegmentReader srcSegment, int srcOffset, int nestingLimit) { + static SegmentBuilder copyPointer(SegmentBuilder dstSegment, CapTableBuilder dstCapTable, int dstOffset, + SegmentReader srcSegment, CapTableReader srcCapTable, int srcOffset, int nestingLimit) { // Deep-copy the object pointed to by src into dst. It turns out we can't reuse // readStructPointer(), etc. because they do type checking whereas here we want to accept any // valid pointer. @@ -1073,8 +1146,9 @@ static SegmentBuilder copyPointer(SegmentBuilder dstSegment, int dstOffset, throw new DecodeException("Message is too deeply nested or contains cycles. See org.capnproto.ReaderOptions."); } resolved.segment.arena.checkReadLimit(StructPointer.wordSize(resolved.ref)); - return setStructPointer(dstSegment, dstOffset, + return setStructPointer(dstSegment, dstCapTable, dstOffset, new StructReader(resolved.segment, + srcCapTable, resolved.ptr * Constants.BYTES_PER_WORD, resolved.ptr + StructPointer.dataSize(resolved.ref), StructPointer.dataSize(resolved.ref) * Constants.BITS_PER_WORD, @@ -1108,7 +1182,7 @@ static SegmentBuilder copyPointer(SegmentBuilder dstSegment, int dstOffset, resolved.segment.arena.checkReadLimit(elementCount); } - return setListPointer(dstSegment, dstOffset, + return setListPointer(dstSegment, dstCapTable, dstOffset, new ListReader(resolved.segment, ptr * Constants.BYTES_PER_WORD, elementCount, @@ -1131,7 +1205,7 @@ static SegmentBuilder copyPointer(SegmentBuilder dstSegment, int dstOffset, resolved.segment.arena.checkReadLimit(elementCount); } - return setListPointer(dstSegment, dstOffset, + return setListPointer(dstSegment, dstCapTable, dstOffset, new ListReader(resolved.segment, resolved.ptr * Constants.BYTES_PER_WORD, elementCount, @@ -1144,7 +1218,14 @@ static SegmentBuilder copyPointer(SegmentBuilder dstSegment, int dstOffset, case WirePointer.FAR : throw new DecodeException("Unexpected FAR pointer."); case WirePointer.OTHER : - throw new RuntimeException("copyPointer is unimplemented for OTHER pointers"); + if (WirePointer.isCapability(srcRef)) { + var cap = readCapabilityPointer(srcSegment, srcCapTable, srcOffset, 0); + setCapabilityPointer(dstSegment, dstCapTable, dstOffset, cap); + return dstSegment; + } + else { + throw new RuntimeException("copyPointer is unimplemented for OTHER pointers"); + } } throw new RuntimeException("unreachable"); } @@ -1152,6 +1233,7 @@ static SegmentBuilder copyPointer(SegmentBuilder dstSegment, int dstOffset, static T readListPointer(ListReader.Factory factory, SegmentReader segment, int refOffset, + CapTableReader capTable, SegmentReader defaultSegment, int defaultOffset, byte expectedElementSize, @@ -1161,7 +1243,7 @@ static T readListPointer(ListReader.Factory factory, if (WirePointer.isNull(ref)) { if (defaultSegment == null) { - return factory.constructReader(SegmentReader.EMPTY, 0, 0, 0, 0, (short) 0, 0x7fffffff); + return factory.constructReader(SegmentReader.EMPTY, capTable, 0, 0, 0, 0, (short) 0, 0x7fffffff); } else { segment = defaultSegment; refOffset = defaultOffset; @@ -1230,13 +1312,13 @@ static T readListPointer(ListReader.Factory factory, default: break; } - return factory.constructReader(resolved.segment, - ptr * Constants.BYTES_PER_WORD, - size, - wordsPerElement * Constants.BITS_PER_WORD, - dataSize * Constants.BITS_PER_WORD, - ptrCount, - nestingLimit - 1); + return factory.constructReader(resolved.segment, capTable, + ptr * Constants.BYTES_PER_WORD, + size, + wordsPerElement * Constants.BITS_PER_WORD, + StructPointer.dataSize(tag) * Constants.BITS_PER_WORD, + (short)StructPointer.ptrCount(tag), + nestingLimit - 1); } default : { //# This is a primitive or pointer list, but all such @@ -1281,6 +1363,7 @@ static T readListPointer(ListReader.Factory factory, } return factory.constructReader(resolved.segment, + capTable, resolved.ptr * Constants.BYTES_PER_WORD, elementCount, step, @@ -1363,4 +1446,46 @@ static Data.Reader readDataPointer(SegmentReader segment, return new Data.Reader(resolved.segment.buffer, resolved.ptr, size); } + static void setCapabilityPointer(SegmentBuilder segment, CapTableBuilder capTable, int refOffset, ClientHook cap) { + long ref = segment.get(refOffset); + + if (!WirePointer.isNull(ref)) { + zeroObject(segment, capTable, refOffset); + } + + if (cap == null) { + // TODO check zeroMemory behaviour + zeroPointerAndFars(segment, refOffset); + } + else if (capTable != null) { + WirePointer.setCapability(segment.buffer, refOffset, capTable.injectCap(cap)); + } + else { + assert false: "Cannot set capability pointer without capTable"; + } + } + + static ClientHook readCapabilityPointer(SegmentReader segment, CapTableReader capTable, int refOffset, int maxValue) { + long ref = segment.get(refOffset); + + if (WirePointer.isNull(ref)) { + return Capability.newNullCap(); + } + + if (WirePointer.kind(ref) != WirePointer.OTHER) { + return Capability.newBrokenCap("Calling capability extracted from a non-capability pointer."); + } + + if (capTable == null) { + return Capability.newBrokenCap("Cannot read capability pointer without capTable."); + } + + int index = WirePointer.upper32Bits(ref); + var cap = capTable.extractCap(index); + if (cap == null) { + return Capability.newBrokenCap("Calling invalid capability pointer."); + } + return cap; + } + } diff --git a/runtime/src/main/java/org/capnproto/WirePointer.java b/runtime/src/main/java/org/capnproto/WirePointer.java index bc375b99..f25ef74c 100644 --- a/runtime/src/main/java/org/capnproto/WirePointer.java +++ b/runtime/src/main/java/org/capnproto/WirePointer.java @@ -87,4 +87,14 @@ public static void setKindAndInlineCompositeListElementCount(ByteBuffer buffer, public static int upper32Bits(long wirePointer) { return (int)(wirePointer >>> 32); } + + public static boolean isCapability(long wirePointer) { + // lower 30 bits are all zero + return offsetAndKind(wirePointer) == OTHER; + } + + public static void setCapability(ByteBuffer buffer, int offset, int cap) { + setOffsetAndKind(buffer, offset, OTHER); + buffer.putInt(offset*8 + 4, cap); + } } diff --git a/runtime/src/main/java/org/capnproto/schema.capnp b/runtime/src/main/java/org/capnproto/schema.capnp new file mode 100644 index 00000000..fd9bf0b0 --- /dev/null +++ b/runtime/src/main/java/org/capnproto/schema.capnp @@ -0,0 +1,533 @@ +# Copyright (c) 2013-2014 Sandstorm Development Group, Inc. and contributors +# Licensed under the MIT License: +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +using Cxx = import "/capnp/c++.capnp"; + +@0xa93fc509624c72d9; +$Cxx.namespace("capnp::schema"); + +using Java = import "/capnp/java.capnp"; +$Java.package("org.capnproto"); +$Java.outerClassname("Schema"); + +using Id = UInt64; +# The globally-unique ID of a file, type, or annotation. + +struct Node { + id @0 :Id; + + displayName @1 :Text; + # Name to present to humans to identify this Node. You should not attempt to parse this. Its + # format could change. It is not guaranteed to be unique. + # + # (On Zooko's triangle, this is the node's nickname.) + + displayNamePrefixLength @2 :UInt32; + # If you want a shorter version of `displayName` (just naming this node, without its surrounding + # scope), chop off this many characters from the beginning of `displayName`. + + scopeId @3 :Id; + # ID of the lexical parent node. Typically, the scope node will have a NestedNode pointing back + # at this node, but robust code should avoid relying on this (and, in fact, group nodes are not + # listed in the outer struct's nestedNodes, since they are listed in the fields). `scopeId` is + # zero if the node has no parent, which is normally only the case with files, but should be + # allowed for any kind of node (in order to make runtime type generation easier). + + parameters @32 :List(Parameter); + # If this node is parameterized (generic), the list of parameters. Empty for non-generic types. + + isGeneric @33 :Bool; + # True if this node is generic, meaning that it or one of its parent scopes has a non-empty + # `parameters`. + + struct Parameter { + # Information about one of the node's parameters. + + name @0 :Text; + } + + nestedNodes @4 :List(NestedNode); + # List of nodes nested within this node, along with the names under which they were declared. + + struct NestedNode { + name @0 :Text; + # Unqualified symbol name. Unlike Node.displayName, this *can* be used programmatically. + # + # (On Zooko's triangle, this is the node's petname according to its parent scope.) + + id @1 :Id; + # ID of the nested node. Typically, the target node's scopeId points back to this node, but + # robust code should avoid relying on this. + } + + annotations @5 :List(Annotation); + # Annotations applied to this node. + + union { + # Info specific to each kind of node. + + file @6 :Void; + + struct :group { + dataWordCount @7 :UInt16; + # Size of the data section, in words. + + pointerCount @8 :UInt16; + # Size of the pointer section, in pointers (which are one word each). + + preferredListEncoding @9 :ElementSize; + # The preferred element size to use when encoding a list of this struct. If this is anything + # other than `inlineComposite` then the struct is one word or less in size and is a candidate + # for list packing optimization. + + isGroup @10 :Bool; + # If true, then this "struct" node is actually not an independent node, but merely represents + # some named union or group within a particular parent struct. This node's scopeId refers + # to the parent struct, which may itself be a union/group in yet another struct. + # + # All group nodes share the same dataWordCount and pointerCount as the top-level + # struct, and their fields live in the same ordinal and offset spaces as all other fields in + # the struct. + # + # Note that a named union is considered a special kind of group -- in fact, a named union + # is exactly equivalent to a group that contains nothing but an unnamed union. + + discriminantCount @11 :UInt16; + # Number of fields in this struct which are members of an anonymous union, and thus may + # overlap. If this is non-zero, then a 16-bit discriminant is present indicating which + # of the overlapping fields is active. This can never be 1 -- if it is non-zero, it must be + # two or more. + # + # Note that the fields of an unnamed union are considered fields of the scope containing the + # union -- an unnamed union is not its own group. So, a top-level struct may contain a + # non-zero discriminant count. Named unions, on the other hand, are equivalent to groups + # containing unnamed unions. So, a named union has its own independent schema node, with + # `isGroup` = true. + + discriminantOffset @12 :UInt32; + # If `discriminantCount` is non-zero, this is the offset of the union discriminant, in + # multiples of 16 bits. + + fields @13 :List(Field); + # Fields defined within this scope (either the struct's top-level fields, or the fields of + # a particular group; see `isGroup`). + # + # The fields are sorted by ordinal number, but note that because groups share the same + # ordinal space, the field's index in this list is not necessarily exactly its ordinal. + # On the other hand, the field's position in this list does remain the same even as the + # protocol evolves, since it is not possible to insert or remove an earlier ordinal. + # Therefore, for most use cases, if you want to identify a field by number, it may make the + # most sense to use the field's index in this list rather than its ordinal. + } + + enum :group { + enumerants@14 :List(Enumerant); + # Enumerants ordered by numeric value (ordinal). + } + + interface :group { + methods @15 :List(Method); + # Methods ordered by ordinal. + + superclasses @31 :List(Superclass); + # Superclasses of this interface. + } + + const :group { + type @16 :Type; + value @17 :Value; + } + + annotation :group { + type @18 :Type; + + targetsFile @19 :Bool; + targetsConst @20 :Bool; + targetsEnum @21 :Bool; + targetsEnumerant @22 :Bool; + targetsStruct @23 :Bool; + targetsField @24 :Bool; + targetsUnion @25 :Bool; + targetsGroup @26 :Bool; + targetsInterface @27 :Bool; + targetsMethod @28 :Bool; + targetsParam @29 :Bool; + targetsAnnotation @30 :Bool; + } + } + + struct SourceInfo { + # Additional information about a node which is not needed at runtime, but may be useful for + # documentation or debugging purposes. This is kept in a separate struct to make sure it + # doesn't accidentally get included in contexts where it is not needed. The + # `CodeGeneratorRequest` includes this information in a separate array. + + id @0 :Id; + # ID of the Node which this info describes. + + docComment @1 :Text; + # The top-level doc comment for the Node. + + members @2 :List(Member); + # Information about each member -- i.e. fields (for structs), enumerants (for enums), or + # methods (for interfaces). + # + # This list is the same length and order as the corresponding list in the Node, i.e. + # Node.struct.fields, Node.enum.enumerants, or Node.interface.methods. + + struct Member { + docComment @0 :Text; + # Doc comment on the member. + } + + # TODO(someday): Record location of the declaration in the original source code. + } +} + +struct Field { + # Schema for a field of a struct. + + name @0 :Text; + + codeOrder @1 :UInt16; + # Indicates where this member appeared in the code, relative to other members. + # Code ordering may have semantic relevance -- programmers tend to place related fields + # together. So, using code ordering makes sense in human-readable formats where ordering is + # otherwise irrelevant, like JSON. The values of codeOrder are tightly-packed, so the maximum + # value is count(members) - 1. Fields that are members of a union are only ordered relative to + # the other members of that union, so the maximum value there is count(union.members). + + annotations @2 :List(Annotation); + + const noDiscriminant :UInt16 = 0xffff; + + discriminantValue @3 :UInt16 = Field.noDiscriminant; + # If the field is in a union, this is the value which the union's discriminant should take when + # the field is active. If the field is not in a union, this is 0xffff. + + union { + slot :group { + # A regular, non-group, non-fixed-list field. + + offset @4 :UInt32; + # Offset, in units of the field's size, from the beginning of the section in which the field + # resides. E.g. for a UInt32 field, multiply this by 4 to get the byte offset from the + # beginning of the data section. + + type @5 :Type; + defaultValue @6 :Value; + + hadExplicitDefault @10 :Bool; + # Whether the default value was specified explicitly. Non-explicit default values are always + # zero or empty values. Usually, whether the default value was explicit shouldn't matter. + # The main use case for this flag is for structs representing method parameters: + # explicitly-defaulted parameters may be allowed to be omitted when calling the method. + } + + group :group { + # A group. + + typeId @7 :Id; + # The ID of the group's node. + } + } + + ordinal :union { + implicit @8 :Void; + explicit @9 :UInt16; + # The original ordinal number given to the field. You probably should NOT use this; if you need + # a numeric identifier for a field, use its position within the field array for its scope. + # The ordinal is given here mainly just so that the original schema text can be reproduced given + # the compiled version -- i.e. so that `capnp compile -ocapnp` can do its job. + } +} + +struct Enumerant { + # Schema for member of an enum. + + name @0 :Text; + + codeOrder @1 :UInt16; + # Specifies order in which the enumerants were declared in the code. + # Like Struct.Field.codeOrder. + + annotations @2 :List(Annotation); +} + +struct Superclass { + id @0 :Id; + brand @1 :Brand; +} + +struct Method { + # Schema for method of an interface. + + name @0 :Text; + + codeOrder @1 :UInt16; + # Specifies order in which the methods were declared in the code. + # Like Struct.Field.codeOrder. + + implicitParameters @7 :List(Node.Parameter); + # The parameters listed in [] (typically, type / generic parameters), whose bindings are intended + # to be inferred rather than specified explicitly, although not all languages support this. + + paramStructType @2 :Id; + # ID of the parameter struct type. If a named parameter list was specified in the method + # declaration (rather than a single struct parameter type) then a corresponding struct type is + # auto-generated. Such an auto-generated type will not be listed in the interface's + # `nestedNodes` and its `scopeId` will be zero -- it is completely detached from the namespace. + # (Awkwardly, it does of course inherit generic parameters from the method's scope, which makes + # this a situation where you can't just climb the scope chain to find where a particular + # generic parameter was introduced. Making the `scopeId` zero was a mistake.) + + paramBrand @5 :Brand; + # Brand of param struct type. + + resultStructType @3 :Id; + # ID of the return struct type; similar to `paramStructType`. + + resultBrand @6 :Brand; + # Brand of result struct type. + + annotations @4 :List(Annotation); +} + +struct Type { + # Represents a type expression. + + union { + # The ordinals intentionally match those of Value. + + void @0 :Void; + bool @1 :Void; + int8 @2 :Void; + int16 @3 :Void; + int32 @4 :Void; + int64 @5 :Void; + uint8 @6 :Void; + uint16 @7 :Void; + uint32 @8 :Void; + uint64 @9 :Void; + float32 @10 :Void; + float64 @11 :Void; + text @12 :Void; + data @13 :Void; + + list :group { + elementType @14 :Type; + } + + enum :group { + typeId @15 :Id; + brand @21 :Brand; + } + struct :group { + typeId @16 :Id; + brand @22 :Brand; + } + interface :group { + typeId @17 :Id; + brand @23 :Brand; + } + + anyPointer :union { + unconstrained :union { + # A regular AnyPointer. + # + # The name "unconstrained" means as opposed to constraining it to match a type parameter. + # In retrospect this name is probably a poor choice given that it may still be constrained + # to be a struct, list, or capability. + + anyKind @18 :Void; # truly AnyPointer + struct @25 :Void; # AnyStruct + list @26 :Void; # AnyList + capability @27 :Void; # Capability + } + + parameter :group { + # This is actually a reference to a type parameter defined within this scope. + + scopeId @19 :Id; + # ID of the generic type whose parameter we're referencing. This should be a parent of the + # current scope. + + parameterIndex @20 :UInt16; + # Index of the parameter within the generic type's parameter list. + } + + implicitMethodParameter :group { + # This is actually a reference to an implicit (generic) parameter of a method. The only + # legal context for this type to appear is inside Method.paramBrand or Method.resultBrand. + + parameterIndex @24 :UInt16; + } + } + } +} + +struct Brand { + # Specifies bindings for parameters of generics. Since these bindings turn a generic into a + # non-generic, we call it the "brand". + + scopes @0 :List(Scope); + # For each of the target type and each of its parent scopes, a parameterization may be included + # in this list. If no parameterization is included for a particular relevant scope, then either + # that scope has no parameters or all parameters should be considered to be `AnyPointer`. + + struct Scope { + scopeId @0 :Id; + # ID of the scope to which these params apply. + + union { + bind @1 :List(Binding); + # List of parameter bindings. + + inherit @2 :Void; + # The place where this Brand appears is actually within this scope or a sub-scope, + # and the bindings for this scope should be inherited from the reference point. + } + } + + struct Binding { + union { + unbound @0 :Void; + type @1 :Type; + + # TODO(someday): Allow non-type parameters? Unsure if useful. + } + } +} + +struct Value { + # Represents a value, e.g. a field default value, constant value, or annotation value. + + union { + # The ordinals intentionally match those of Type. + + void @0 :Void; + bool @1 :Bool; + int8 @2 :Int8; + int16 @3 :Int16; + int32 @4 :Int32; + int64 @5 :Int64; + uint8 @6 :UInt8; + uint16 @7 :UInt16; + uint32 @8 :UInt32; + uint64 @9 :UInt64; + float32 @10 :Float32; + float64 @11 :Float64; + text @12 :Text; + data @13 :Data; + + list @14 :AnyPointer; + + enum @15 :UInt16; + struct @16 :AnyPointer; + + interface @17 :Void; + # The only interface value that can be represented statically is "null", whose methods always + # throw exceptions. + + anyPointer @18 :AnyPointer; + } +} + +struct Annotation { + # Describes an annotation applied to a declaration. Note AnnotationNode describes the + # annotation's declaration, while this describes a use of the annotation. + + id @0 :Id; + # ID of the annotation node. + + brand @2 :Brand; + # Brand of the annotation. + # + # Note that the annotation itself is not allowed to be parameterized, but its scope might be. + + value @1 :Value; +} + +enum ElementSize { + # Possible element sizes for encoded lists. These correspond exactly to the possible values of + # the 3-bit element size component of a list pointer. + + empty @0; # aka "void", but that's a keyword. + bit @1; + byte @2; + twoBytes @3; + fourBytes @4; + eightBytes @5; + pointer @6; + inlineComposite @7; +} + +struct CapnpVersion { + major @0 :UInt16; + minor @1 :UInt8; + micro @2 :UInt8; +} + +struct CodeGeneratorRequest { + capnpVersion @2 :CapnpVersion; + # Version of the `capnp` executable. Generally, code generators should ignore this, but the code + # generators that ship with `capnp` itself will print a warning if this mismatches since that + # probably indicates something is misconfigured. + # + # The first version of 'capnp' to set this was 0.6.0. So, if it's missing, the compiler version + # is older than that. + + nodes @0 :List(Node); + # All nodes parsed by the compiler, including for the files on the command line and their + # imports. + + sourceInfo @3 :List(Node.SourceInfo); + # Information about the original source code for each node, where available. This array may be + # omitted or may be missing some nodes if no info is available for them. + + requestedFiles @1 :List(RequestedFile); + # Files which were listed on the command line. + + struct RequestedFile { + id @0 :Id; + # ID of the file. + + filename @1 :Text; + # Name of the file as it appeared on the command-line (minus the src-prefix). You may use + # this to decide where to write the output. + + imports @2 :List(Import); + # List of all imported paths seen in this file. + + struct Import { + id @0 :Id; + # ID of the imported file. + + name @1 :Text; + # Name which *this* file used to refer to the foreign file. This may be a relative name. + # This information is provided because it might be useful for code generation, e.g. to + # generate #include directives in C++. We don't put this in Node.file because this + # information is only meaningful at compile time anyway. + # + # (On Zooko's triangle, this is the import's petname according to the importing file.) + } + } +} diff --git a/runtime/src/test/java/org/capnproto/LayoutTest.java b/runtime/src/test/java/org/capnproto/LayoutTest.java index acd0a81e..c5f428e1 100644 --- a/runtime/src/test/java/org/capnproto/LayoutTest.java +++ b/runtime/src/test/java/org/capnproto/LayoutTest.java @@ -1,6 +1,5 @@ package org.capnproto; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import java.nio.ByteBuffer; @@ -31,7 +30,7 @@ public void testSimpleRawDataStruct() { ReaderArena arena = new ReaderArena(new ByteBuffer[]{ buffer }, 0x7fffffffffffffffL); - StructReader reader = WireHelpers.readStructPointer(new BareStructReader(), arena.tryGetSegment(0), 0, null, 0, MAX_NESTING_LIMIT); + StructReader reader = WireHelpers.readStructPointer(new BareStructReader(), arena.tryGetSegment(0), null, 0, null, 0, MAX_NESTING_LIMIT); assertEquals(0xefcdab8967452301L, reader._getLongField(0)); assertEquals(0L, reader._getLongField(1)); @@ -89,7 +88,7 @@ public void readStructPointerShouldThrowDecodeExceptionOnOutOfBoundsStructPointe ReaderArena arena = new ReaderArena(new ByteBuffer[]{ buffer }, 0x7fffffffffffffffL); - assertThrows(DecodeException.class, () -> WireHelpers.readStructPointer(new BareStructReader(), arena.tryGetSegment(0), 0, null, 0, MAX_NESTING_LIMIT)); + StructReader reader = WireHelpers.readStructPointer(new BareStructReader(), arena.tryGetSegment(0), null, 0, null, 0, MAX_NESTING_LIMIT); } @@ -117,7 +116,7 @@ public void readListPointerShouldThrowDecodeExceptionOnOutOfBoundsCompositeListP ReaderArena arena = new ReaderArena(new ByteBuffer[]{buffer}, 0x7fffffffffffffffL); - assertThrows(DecodeException.class, () -> WireHelpers.readListPointer(new BareListReader(), arena.tryGetSegment(0), 0, null, 0, (byte) 0, MAX_NESTING_LIMIT)); + assertThrows(DecodeException.class, () -> WireHelpers.readListPointer(new BareListReader(), arena.tryGetSegment(0), 0, null, null, 0, (byte) 0, MAX_NESTING_LIMIT)); } private class BareStructBuilder implements StructBuilder.Factory { diff --git a/runtime/src/test/java/org/capnproto/ListBuilderTest.java b/runtime/src/test/java/org/capnproto/ListBuilderTest.java index ae5009ff..0e4b826d 100644 --- a/runtime/src/test/java/org/capnproto/ListBuilderTest.java +++ b/runtime/src/test/java/org/capnproto/ListBuilderTest.java @@ -1,12 +1,10 @@ package org.capnproto; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.nio.ByteBuffer; -import static org.junit.jupiter.api.Assertions.assertThrows; - public class ListBuilderTest { @Test diff --git a/runtime/src/test/java/org/capnproto/SerializeTest.java b/runtime/src/test/java/org/capnproto/SerializeTest.java index f73b7a6a..2d69795f 100644 --- a/runtime/src/test/java/org/capnproto/SerializeTest.java +++ b/runtime/src/test/java/org/capnproto/SerializeTest.java @@ -30,11 +30,19 @@ import java.util.Arrays; import java.util.Optional; +import java.nio.channels.AsynchronousServerSocketChannel; +import java.nio.channels.AsynchronousSocketChannel; +import java.nio.channels.CompletionHandler; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + + import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; public class SerializeTest { @@ -77,6 +85,47 @@ private void expectSerializesTo(int exampleSegmentCount, byte[] exampleBytes) th MessageReader messageReader = Serialize.read(ByteBuffer.wrap(exampleBytes)); checkSegmentContents(exampleSegmentCount, messageReader.arena); } + + // read via AsyncChannel + expectSerializesToAsyncSocket(exampleSegmentCount, exampleBytes); + } + + private void expectSerializesToAsyncSocket(int exampleSegmentCount, byte[] exampleBytes) throws IOException { + var done = new CompletableFuture(); + var server = AsynchronousServerSocketChannel.open(); + server.bind(null); + server.accept(null, new CompletionHandler() { + @Override + public void completed(AsynchronousSocketChannel socket, Object attachment) { + socket.write(ByteBuffer.wrap(exampleBytes), null, new CompletionHandler() { + @Override + public void completed(Integer result, Object attachment) { + done.complete(null); + } + + @Override + public void failed(Throwable exc, Object attachment) { + done.completeExceptionally(exc); + } + }); + } + + @Override + public void failed(Throwable exc, Object attachment) { + done.completeExceptionally(exc); + } + }); + + var socket = AsynchronousSocketChannel.open(); + try { + socket.connect(server.getLocalAddress()).get(); + var messageReader = Serialize.readAsync(socket).get(); + checkSegmentContents(exampleSegmentCount, messageReader.arena); + done.get(); + } + catch (InterruptedException | ExecutionException exc) { + fail(exc.getMessage()); + } } @Test diff --git a/runtime/src/test/schema/demo.capnp b/runtime/src/test/schema/demo.capnp new file mode 100644 index 00000000..efdbc9ee --- /dev/null +++ b/runtime/src/test/schema/demo.capnp @@ -0,0 +1,59 @@ +@0xb6577a1582e84742; + +using Java = import "/capnp/java.capnp"; +$Java.package("org.capnproto.demo"); +$Java.outerClassname("Demo"); + +struct TestParams0 { + param0 @0 :Int32; +} + +struct TestResults0 { + result0 @0 :Int32; +} + +struct TestParams1 { + param0 @0 :AnyPointer; +} + +struct TestResults1 { + result0 @0 :AnyPointer; + result1 @1 :AnyPointer; + result2 @2 :AnyPointer; +} + +struct Struct0 { + f0 @0 :Bool; +} + +interface Iface0 { + method0 @0 (); + method1 @1 () -> stream; +} + +struct Struct2 { + f0 @0 :AnyPointer; + f1i @1 :Iface0; +} + +interface TestCap0 { + testMethod0 @0 TestParams0 -> TestResults0; + testMethod1 @1 TestParams1 -> TestResults1; +} + +interface TestCap1 { +} + + +interface Iface1 { + + struct Struct1 { + f0 @0 :Bool; + f1 @1 :AnyPointer; + } + + method0 @0 () -> (result0 :Struct0, result1 :Struct1); + method1 @1 () -> (result0: Iface0); +} + + diff --git a/runtime/src/test/schema/generics.capnp b/runtime/src/test/schema/generics.capnp new file mode 100644 index 00000000..54ffe6d6 --- /dev/null +++ b/runtime/src/test/schema/generics.capnp @@ -0,0 +1,27 @@ +@0xbf250a886b8a4258; + +using Java = import "/capnp/java.capnp"; +$Java.package("org.capnproto.test"); +$Java.outerClassname("TestGenerics"); + +interface Aaa { + struct S { + bar @0 :UInt32; + } +} + +struct Sss(X) { +} + +interface Bbb(X) { + + foo @0 (value: Aaa); +} + +interface Ccc(X) { +} + +#interface Ddd(X, Y) { +# foo @0 (value: X); +# bar @1 () -> (value: X); +#} \ No newline at end of file