| /* | |
| * Copyright 2024 WebAssembly Community Group participants | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| // This is a J2CL specific pass that merges itables into vtables. It is meant | |
| // to be run at the beginning before structs corresponding to Java classes are | |
| // optimized. | |
| // | |
| // The motivation for embedding itables into vtables is to reduce memory usage. | |
| // | |
| // The pass makes the following transformation on the structs related to Java | |
| // classes. For given type `Foo` with `Foo[vtable] = { m1, m2, m3, ... }` | |
| // and `Foo[itable] = { i1, i2, ...}`, this pass transforms it to | |
| // `Foo[vtable] = { i1, i2, ...., m1, m2, m3, ... }`, and fixes all accesses | |
| // and initializations accordingly. | |
| #include <unordered_map> | |
| #include <unordered_set> | |
| #include "ir/effects.h" | |
| #include "ir/localize.h" | |
| #include "ir/ordering.h" | |
| #include "ir/struct-utils.h" | |
| #include "ir/subtypes.h" | |
| #include "ir/type-updating.h" | |
| #include "ir/utils.h" | |
| #include "pass.h" | |
| #include "wasm-builder.h" | |
| #include "wasm-type.h" | |
| #include "wasm.h" | |
| namespace wasm { | |
| namespace { | |
| // Information about the structs that have vtables and itables. | |
| struct StructInfo { | |
| HeapType javaClass; | |
| HeapType vtable; | |
| HeapType itable; | |
| }; | |
| struct J2CLItableMerging : public Pass { | |
| // Keep track of all the structInfos so that they will be automatically | |
| // released after the pass is done. | |
| std::list<StructInfo> structInfos; | |
| // Globals that hold vtables and itables indexed by their heap type. | |
| // There is exactly 1 global for each vtable/itable type. | |
| std::unordered_map<HeapType, Global*> tableGlobalsByType; | |
| std::unordered_map<HeapType, StructInfo*> structInfoByVtableType; | |
| std::unordered_map<HeapType, StructInfo*> structInfoByITableType; | |
| unsigned long itableSize = 0; | |
| void run(Module* module) override { | |
| if (!module->features.hasGC()) { | |
| return; | |
| } | |
| if (!getPassOptions().closedWorld) { | |
| Fatal() << "--merge-j2cl-itables requires --closed-world"; | |
| } | |
| collectVtableAndItableTypes(*module); | |
| // Update the indices to access the functions in the vtables and update | |
| // the construction of the vtable instances. | |
| updateVtableFieldsAccesses(*module); | |
| // And now we can transform the accesses to the itable fields into their | |
| // corresponding vtable fields. Needs to be done after | |
| // updateVtableFieldsAccesses. | |
| rerouteItableAccess(*module); | |
| // The type structures are updated last since types are used as keys in | |
| // the maps used above. | |
| updateTypes(*module); | |
| // Since now vtables are initialized with `global.get` of the interface | |
| // vtable instances, we need to reorder the globals. | |
| PassRunner runner(module); | |
| runner.add("reorder-globals-always"); | |
| runner.setIsNested(true); | |
| runner.run(); | |
| } | |
| // Collects all structs corresponding to Java classes, their vtables and | |
| // their itables. This is very tied to the way j2cl emits these constructs. | |
| void collectVtableAndItableTypes(Module& wasm) { | |
| // 1. Collect all structs that correspond that a Java type. | |
| for (auto [heapType, typeNameInfo] : wasm.typeNames) { | |
| if (!heapType.isStruct()) { | |
| continue; | |
| } | |
| auto type = heapType.getStruct(); | |
| if (typeNameInfo.fieldNames.empty() || | |
| !typeNameInfo.fieldNames[0].equals("vtable")) { | |
| continue; | |
| } | |
| if (typeNameInfo.fieldNames.size() < 1 || | |
| !typeNameInfo.fieldNames[1].equals("itable")) { | |
| continue; | |
| } | |
| auto vtabletype = type.fields[0].type.getHeapType(); | |
| auto itabletype = type.fields[1].type.getHeapType(); | |
| auto structItableSize = itabletype.getStruct().fields.size(); | |
| if (itableSize != 0 && itableSize != structItableSize) { | |
| Fatal() << "--merge-j2cl-itables needs to be the first pass to run " | |
| << "on j2cl output. (found itables with different sizes)"; | |
| } | |
| itableSize = structItableSize; | |
| // Add a new StructInfo to the list by value so that its memory gets | |
| // reclaimed automatically on exit. | |
| structInfos.push_back(StructInfo{heapType, vtabletype, itabletype}); | |
| // Point to the StructInfo just added to the list to be able to look it | |
| // up by its vtable and itable types. | |
| structInfoByVtableType[vtabletype] = &structInfos.back(); | |
| structInfoByITableType[itabletype] = &structInfos.back(); | |
| } | |
| // 2. Collect the globals for vtables and itables. | |
| for (auto& g : wasm.globals) { | |
| if (!g->type.isStruct()) { | |
| continue; | |
| } | |
| if (structInfoByVtableType.count(g->type.getHeapType())) { | |
| tableGlobalsByType[g->type.getHeapType()] = g.get(); | |
| } else if (structInfoByITableType.count(g->type.getHeapType())) { | |
| tableGlobalsByType[g->type.getHeapType()] = g.get(); | |
| } | |
| } | |
| if (itableSize == 0) { | |
| Fatal() << "--merge-j2cl-itables needs to be the first pass to run " | |
| << "on j2cl output. (no Java classes found)"; | |
| } | |
| } | |
| // Fix the indexes of `struct.get` for vtable fields, and prepend the | |
| // initializers for the itable fields to `struct.new`. | |
| // Note that there isn't any `struct.set` because the vtable fields are | |
| // immutable. | |
| void updateVtableFieldsAccesses(Module& wasm) { | |
| struct Reindexer : public WalkerPass<PostWalker<Reindexer>> { | |
| bool isFunctionParallel() override { return true; } | |
| J2CLItableMerging& parent; | |
| Reindexer(J2CLItableMerging& parent) : parent(parent) {} | |
| std::unique_ptr<Pass> create() override { | |
| return std::make_unique<Reindexer>(parent); | |
| } | |
| void visitStructGet(StructGet* curr) { | |
| if (curr->ref->type == Type::unreachable) { | |
| return; | |
| } | |
| if (!parent.structInfoByVtableType.count( | |
| curr->ref->type.getHeapType())) { | |
| return; | |
| } | |
| // This is a struct.get on the vtable. | |
| // It is ok to just change the index since the field has moved but | |
| // the type is the same. | |
| curr->index += parent.itableSize; | |
| } | |
| void visitStructNew(StructNew* curr) { | |
| if (curr->type == Type::unreachable) { | |
| return; | |
| } | |
| auto it = parent.structInfoByVtableType.find(curr->type.getHeapType()); | |
| if (it == parent.structInfoByVtableType.end()) { | |
| return; | |
| } | |
| // The struct.new is for a vtable type and structInfo has the | |
| // information relating the struct types for the Java class, its vtable | |
| // and its itable. | |
| auto structInfo = it->second; | |
| // Get the global that holds the corresponding itable instance. | |
| auto* itableGlobal = parent.tableGlobalsByType[structInfo->itable]; | |
| StructNew* itableStructNew = nullptr; | |
| if (itableGlobal && itableGlobal->init) { | |
| if (itableGlobal->init->is<GlobalGet>()) { | |
| // The global might get initialized with the shared empty itable, | |
| // obtain the itable struct.new from the global.init. | |
| auto* globalGet = itableGlobal->init->dynCast<GlobalGet>(); | |
| auto* global = getModule()->getGlobal(globalGet->name); | |
| itableStructNew = global->init->dynCast<StructNew>(); | |
| } else { | |
| // The global is initialized with a struct.new of the itable. | |
| itableStructNew = itableGlobal->init->dynCast<StructNew>(); | |
| } | |
| } | |
| if (!itableStructNew) { | |
| Fatal() << "--merge-j2cl-itables needs to be the first pass to run " | |
| << "on j2cl output. (itable initializer not found)"; | |
| } | |
| auto& itableFieldInitializers = itableStructNew->operands; | |
| // Add the initialization for the itable fields. | |
| for (Index i = parent.itableSize; i > 0; i--) { | |
| if (itableFieldInitializers.size() >= i) { | |
| // The itable was initialized with a struct.new, copy the | |
| // initialization values. | |
| curr->operands.insertAt( | |
| 0, | |
| ExpressionManipulator::copy(itableFieldInitializers[i - 1], | |
| *getModule())); | |
| } else { | |
| // The itable was initialized with struct.new_default. So use | |
| // null values to initialize the itable fields. | |
| Builder builder(*getModule()); | |
| curr->operands.insertAt( | |
| 0, | |
| builder.makeRefNull(itableStructNew->type.getHeapType() | |
| .getStruct() | |
| .fields[i - 1] | |
| .type.getHeapType())); | |
| } | |
| } | |
| } | |
| }; | |
| Reindexer reindexer(*this); | |
| reindexer.run(getPassRunner(), &wasm); | |
| reindexer.runOnModuleCode(getPassRunner(), &wasm); | |
| } | |
| // Redirects all itable access by changing `struct.get` of the `itable` field | |
| // to `struct.get` on the to `vtable` field. | |
| void rerouteItableAccess(Module& wasm) { | |
| struct Rerouter : public WalkerPass<PostWalker<Rerouter>> { | |
| bool isFunctionParallel() override { return true; } | |
| J2CLItableMerging& parent; | |
| Rerouter(J2CLItableMerging& parent) : parent(parent) {} | |
| std::unique_ptr<Pass> create() override { | |
| return std::make_unique<Rerouter>(parent); | |
| } | |
| void visitStructGet(StructGet* curr) { | |
| if (curr->ref->type == Type::unreachable) { | |
| return; | |
| } | |
| if (!curr->type.isStruct() || | |
| !parent.structInfoByITableType.count(curr->type.getHeapType())) { | |
| return; | |
| } | |
| // This is a struct.get that returns an itable type; | |
| // Change to return the corresponding vtable type. | |
| Builder builder(*getModule()); | |
| replaceCurrent(builder.makeStructGet( | |
| 0, | |
| curr->ref, | |
| MemoryOrder::Unordered, | |
| parent.structInfoByITableType[curr->type.getHeapType()] | |
| ->javaClass.getStruct() | |
| .fields[0] | |
| .type)); | |
| } | |
| }; | |
| Rerouter rerouter(*this); | |
| rerouter.run(getPassRunner(), &wasm); | |
| rerouter.runOnModuleCode(getPassRunner(), &wasm); | |
| } | |
| // Modify the struct definitions adding the itable fields to the vtable and | |
| // preserving the vtable field names. | |
| void updateTypes(Module& wasm) { | |
| class TypeRewriter : public GlobalTypeRewriter { | |
| J2CLItableMerging& parent; | |
| public: | |
| TypeRewriter(Module& wasm, J2CLItableMerging& parent) | |
| : GlobalTypeRewriter(wasm), parent(parent) {} | |
| void modifyStruct(HeapType oldStructType, Struct& struct_) override { | |
| if (parent.structInfoByVtableType.count(oldStructType)) { | |
| auto& newFields = struct_.fields; | |
| auto structInfo = parent.structInfoByVtableType[oldStructType]; | |
| // Add the itable fields to the beginning of the vtable. | |
| auto it = structInfo->itable.getStruct().fields.rbegin(); | |
| while (it != structInfo->itable.getStruct().fields.rend()) { | |
| newFields.insert(newFields.begin(), *it++); | |
| newFields[0].type = getTempType(newFields[0].type); | |
| } | |
| // Update field names as well. The Type Rewriter cannot do this for | |
| // us, as it does not know which old fields map to which new ones | |
| // (it just keeps the names in sequence). | |
| auto& nameInfo = wasm.typeNames[oldStructType]; | |
| // Make a copy of the old ones before clearing them. | |
| auto oldFieldNames = nameInfo.fieldNames; | |
| // Clear the old names and write the new ones. | |
| nameInfo.fieldNames.clear(); | |
| // Only need to preserve the field names for the vtable fields; the | |
| // itable fields do not have names (in the original .wat file they | |
| // are accessed by index). | |
| for (Index i = 0; i < oldFieldNames.size(); i++) { | |
| nameInfo.fieldNames[i + parent.itableSize] = oldFieldNames[i]; | |
| } | |
| } | |
| } | |
| }; | |
| TypeRewriter(wasm, *this).update(); | |
| } | |
| }; | |
| } // anonymous namespace | |
| Pass* createJ2CLItableMergingPass() { return new J2CLItableMerging(); } | |
| } // namespace wasm |