Skip to content

Commit

Permalink
fix(java): compilation fails with "code too large" (#1605)
Browse files Browse the repository at this point in the history
In cases where a module includes a large amount of types, compilation of
Java packages could fail with the "code too large" error. This is due to
the presence of a single method in the `$Module` class that contains a
reference to all types in the package, resulting in a large amount of
generated bytecode (more than Java's maximum of 64KB).

In order to remediate, moved the FQN to Class mapping to a resource file
that gets loaded when the `$Module` class is initialized. The individual
`Class` entries are then resolved at the last minute using
`Class.forName` to ensure the reoslution happens *after* the full
mapping has been processed.
  • Loading branch information
RomainMuller authored Apr 22, 2020
1 parent fe58cc2 commit b9ec853
Show file tree
Hide file tree
Showing 9 changed files with 489 additions and 226 deletions.
89 changes: 82 additions & 7 deletions packages/jsii-pacmak/lib/targets/java.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1544,6 +1544,20 @@ class JavaGenerator extends Generator {
return `${path.join('src', 'main', 'java', ...packageName.split('.'), typeName.split('.')[0])}.java`;
}

private toJavaResourcePath(assm: spec.Assembly, fqn: string, ext = '.txt') {
const { packageName, typeName } = this.toNativeName(assm, {
fqn,
kind: spec.TypeKind.Class,
assembly: assm.name,
name: fqn.replace(/.*\.([^.]+)$/, '$1')
});

const name = `${path.join(...packageName.split('.'), typeName.split('.')[0])}${ext}`;
const filePath = path.join('src', 'main', 'resources', name);

return { filePath, name };
}

// eslint-disable-next-line complexity
private addJavaDocs(doc: spec.Documentable, defaultText?: string) {
if (!defaultText && Object.keys(doc.docs ?? {}).length === 0
Expand Down Expand Up @@ -1813,24 +1827,74 @@ class JavaGenerator extends Generator {
private emitModuleFile(mod: spec.Assembly) {
const moduleName = mod.name;
const moduleClass = this.makeModuleClass(moduleName);

const { filePath: moduleResFile, name: moduleResName } = this.toJavaResourcePath(mod, `${mod.name}.${MODULE_CLASS_NAME}`);
this.code.openFile(moduleResFile);
for (const fqn of Object.keys(this.assembly.types ?? {})) {
this.code.line(`${fqn}=${this.toNativeFqn(fqn, { binaryName: true })}`);
}
this.code.closeFile(moduleResFile);

const moduleFile = this.toJavaFilePath(mod, {
assembly: mod.name,
fqn: `${mod.name}.${MODULE_CLASS_NAME}`,
kind: spec.TypeKind.Class,
name: MODULE_CLASS_NAME,
});

this.code.openFile(moduleFile);
this.code.line(`package ${this.toNativeName(mod).packageName};`);
this.code.line();
if (Object.keys(mod.dependencies ?? {}).length > 0) {
this.code.line('import static java.util.Arrays.asList;');
this.code.line();
}
this.code.line('import java.io.BufferedReader;');
this.code.line('import java.io.InputStream;');
this.code.line('import java.io.InputStreamReader;');
this.code.line('import java.io.IOException;');
this.code.line('import java.io.Reader;');
this.code.line('import java.io.UncheckedIOException;');
this.code.line();
this.code.line('import java.nio.charset.StandardCharsets;');
this.code.line();
this.code.line('import java.util.HashMap;');
if (Object.keys(mod.dependencies ?? {}).length > 0) {
this.code.line('import java.util.List;');
}
this.code.line('import java.util.Map;');
this.code.line();
this.code.line('import software.amazon.jsii.JsiiModule;');
this.code.line();

this.code.openBlock(`public final class ${MODULE_CLASS_NAME} extends JsiiModule`);
this.code.line('private static final Map<String, String> MODULE_TYPES = load();');
this.code.line();

this.code.openBlock('private static Map<String, String> load()');
this.code.line('final Map<String, String> result = new HashMap<>();');
this.code.line(`final ClassLoader cl = ${MODULE_CLASS_NAME}.class.getClassLoader();`);
this.code.line(`try (final InputStream is = cl.getResourceAsStream("${moduleResName}");`);
this.code.line(' final Reader rd = new InputStreamReader(is, StandardCharsets.UTF_8);');
this.code.openBlock(' final BufferedReader br = new BufferedReader(rd))');
this.code.line('br.lines()');
this.code.line(' .filter(line -> !line.trim().isEmpty())');
this.code.openBlock(' .forEach(line -> ');
this.code.line('final String[] parts = line.split("=", 2);');
this.code.line('final String fqn = parts[0];');
this.code.line('final String className = parts[1];');
this.code.line('result.put(fqn, className);');
this.code.unindent('});'); // Proxy for closeBlock
this.code.closeBlock();
this.code.openBlock('catch (final IOException exception)');
this.code.line('throw new UncheckedIOException(exception);');
this.code.closeBlock();
this.code.line('return result;');
this.code.closeBlock();
this.code.line();

this.code.line('private final Map<String, Class<?>> cache = new HashMap<>();');
this.code.line();

// ctor
this.code.openBlock(`public ${MODULE_CLASS_NAME}()`);
Expand All @@ -1854,11 +1918,19 @@ class JavaGenerator extends Generator {
this.code.line();
this.code.line('@Override');
this.code.openBlock('protected Class<?> resolveClass(final String fqn) throws ClassNotFoundException');
this.code.openBlock('switch (fqn)');
for (const type of Object.keys(this.assembly.types ?? {})) {
this.code.line(`case "${type}": return ${this.toNativeFqn(type)}.class;`);
}
this.code.line('default: throw new ClassNotFoundException("Unknown JSII type: " + fqn);');
this.code.openBlock('if (!MODULE_TYPES.containsKey(fqn))');
this.code.line('throw new ClassNotFoundException("Unknown JSII type: " + fqn);');
this.code.closeBlock();
this.code.line('return this.cache.computeIfAbsent(MODULE_TYPES.get(fqn), this::findClass);');
this.code.closeBlock();

this.code.line();
this.code.openBlock('private Class<?> findClass(final String binaryName)');
this.code.openBlock('try');
this.code.line('return Class.forName(binaryName);');
this.code.closeBlock();
this.code.openBlock('catch (final ClassNotFoundException exception)');
this.code.line('throw new RuntimeException(exception);');
this.code.closeBlock();
this.code.closeBlock();

Expand Down Expand Up @@ -1893,7 +1965,7 @@ class JavaGenerator extends Generator {
*
* @throws if the assembly the FQN belongs to does not have a `targets.java.package` set.
*/
private toNativeFqn(fqn: string): string {
private toNativeFqn(fqn: string, { binaryName }: { binaryName: boolean } = { binaryName: false }): string {
const [mod, ...name] = fqn.split('.');
const depMod = this.findModule(mod);
// Make sure any dependency (direct or transitive) of which any type is explicitly referenced by the generated
Expand All @@ -1905,7 +1977,10 @@ class JavaGenerator extends Generator {
}

const { packageName, typeName } = this.toNativeName(this.assembly, this.assembly.types![fqn]);
return `${packageName}${typeName ? `.${typeName}` : ''}`;
const className = typeName && binaryName
? typeName.replace('.', '$')
: typeName;
return `${packageName}${className ? `.${className}` : ''}`;
}

private getNativeName(assm: spec.Assembly, name: string | undefined): string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,63 @@
package software.amazon.jsii.tests.calculator.baseofbase;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.Reader;
import java.io.UncheckedIOException;

import java.nio.charset.StandardCharsets;

import java.util.HashMap;
import java.util.Map;

import software.amazon.jsii.JsiiModule;

public final class $Module extends JsiiModule {
private static final Map<String, String> MODULE_TYPES = load();

private static Map<String, String> load() {
final Map<String, String> result = new HashMap<>();
final ClassLoader cl = $Module.class.getClassLoader();
try (final InputStream is = cl.getResourceAsStream("software/amazon/jsii/tests/calculator/baseofbase/$Module.txt");
final Reader rd = new InputStreamReader(is, StandardCharsets.UTF_8);
final BufferedReader br = new BufferedReader(rd)) {
br.lines()
.filter(line -> !line.trim().isEmpty())
.forEach(line -> {
final String[] parts = line.split("=", 2);
final String fqn = parts[0];
final String className = parts[1];
result.put(fqn, className);
});
}
catch (final IOException exception) {
throw new UncheckedIOException(exception);
}
return result;
}

private final Map<String, Class<?>> cache = new HashMap<>();

public $Module() {
super("@scope/jsii-calc-base-of-base", "0.0.0", $Module.class, "jsii-calc-base-of-base@0.0.0.jsii.tgz");
}

@Override
protected Class<?> resolveClass(final String fqn) throws ClassNotFoundException {
switch (fqn) {
case "@scope/jsii-calc-base-of-base.IVeryBaseInterface": return software.amazon.jsii.tests.calculator.baseofbase.IVeryBaseInterface.class;
case "@scope/jsii-calc-base-of-base.Very": return software.amazon.jsii.tests.calculator.baseofbase.Very.class;
case "@scope/jsii-calc-base-of-base.VeryBaseProps": return software.amazon.jsii.tests.calculator.baseofbase.VeryBaseProps.class;
default: throw new ClassNotFoundException("Unknown JSII type: " + fqn);
if (!MODULE_TYPES.containsKey(fqn)) {
throw new ClassNotFoundException("Unknown JSII type: " + fqn);
}
return this.cache.computeIfAbsent(MODULE_TYPES.get(fqn), this::findClass);
}

private Class<?> findClass(final String binaryName) {
try {
return Class.forName(binaryName);
}
catch (final ClassNotFoundException exception) {
throw new RuntimeException(exception);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@scope/jsii-calc-base-of-base.IVeryBaseInterface=software.amazon.jsii.tests.calculator.baseofbase.IVeryBaseInterface
@scope/jsii-calc-base-of-base.Very=software.amazon.jsii.tests.calculator.baseofbase.Very
@scope/jsii-calc-base-of-base.VeryBaseProps=software.amazon.jsii.tests.calculator.baseofbase.VeryBaseProps
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,47 @@

import static java.util.Arrays.asList;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.Reader;
import java.io.UncheckedIOException;

import java.nio.charset.StandardCharsets;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import software.amazon.jsii.JsiiModule;

public final class $Module extends JsiiModule {
private static final Map<String, String> MODULE_TYPES = load();

private static Map<String, String> load() {
final Map<String, String> result = new HashMap<>();
final ClassLoader cl = $Module.class.getClassLoader();
try (final InputStream is = cl.getResourceAsStream("software/amazon/jsii/tests/calculator/base/$Module.txt");
final Reader rd = new InputStreamReader(is, StandardCharsets.UTF_8);
final BufferedReader br = new BufferedReader(rd)) {
br.lines()
.filter(line -> !line.trim().isEmpty())
.forEach(line -> {
final String[] parts = line.split("=", 2);
final String fqn = parts[0];
final String className = parts[1];
result.put(fqn, className);
});
}
catch (final IOException exception) {
throw new UncheckedIOException(exception);
}
return result;
}

private final Map<String, Class<?>> cache = new HashMap<>();

public $Module() {
super("@scope/jsii-calc-base", "0.0.0", $Module.class, "jsii-calc-base@0.0.0.jsii.tgz");
}
Expand All @@ -17,11 +54,18 @@ public List<Class<? extends JsiiModule>> getDependencies() {

@Override
protected Class<?> resolveClass(final String fqn) throws ClassNotFoundException {
switch (fqn) {
case "@scope/jsii-calc-base.Base": return software.amazon.jsii.tests.calculator.base.Base.class;
case "@scope/jsii-calc-base.BaseProps": return software.amazon.jsii.tests.calculator.base.BaseProps.class;
case "@scope/jsii-calc-base.IBaseInterface": return software.amazon.jsii.tests.calculator.base.IBaseInterface.class;
default: throw new ClassNotFoundException("Unknown JSII type: " + fqn);
if (!MODULE_TYPES.containsKey(fqn)) {
throw new ClassNotFoundException("Unknown JSII type: " + fqn);
}
return this.cache.computeIfAbsent(MODULE_TYPES.get(fqn), this::findClass);
}

private Class<?> findClass(final String binaryName) {
try {
return Class.forName(binaryName);
}
catch (final ClassNotFoundException exception) {
throw new RuntimeException(exception);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@scope/jsii-calc-base.Base=software.amazon.jsii.tests.calculator.base.Base
@scope/jsii-calc-base.BaseProps=software.amazon.jsii.tests.calculator.base.BaseProps
@scope/jsii-calc-base.IBaseInterface=software.amazon.jsii.tests.calculator.base.IBaseInterface
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,47 @@

import static java.util.Arrays.asList;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.Reader;
import java.io.UncheckedIOException;

import java.nio.charset.StandardCharsets;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import software.amazon.jsii.JsiiModule;

public final class $Module extends JsiiModule {
private static final Map<String, String> MODULE_TYPES = load();

private static Map<String, String> load() {
final Map<String, String> result = new HashMap<>();
final ClassLoader cl = $Module.class.getClassLoader();
try (final InputStream is = cl.getResourceAsStream("software/amazon/jsii/tests/calculator/lib/$Module.txt");
final Reader rd = new InputStreamReader(is, StandardCharsets.UTF_8);
final BufferedReader br = new BufferedReader(rd)) {
br.lines()
.filter(line -> !line.trim().isEmpty())
.forEach(line -> {
final String[] parts = line.split("=", 2);
final String fqn = parts[0];
final String className = parts[1];
result.put(fqn, className);
});
}
catch (final IOException exception) {
throw new UncheckedIOException(exception);
}
return result;
}

private final Map<String, Class<?>> cache = new HashMap<>();

public $Module() {
super("@scope/jsii-calc-lib", "0.0.0", $Module.class, "jsii-calc-lib@0.0.0.jsii.tgz");
}
Expand All @@ -17,17 +54,18 @@ public List<Class<? extends JsiiModule>> getDependencies() {

@Override
protected Class<?> resolveClass(final String fqn) throws ClassNotFoundException {
switch (fqn) {
case "@scope/jsii-calc-lib.EnumFromScopedModule": return software.amazon.jsii.tests.calculator.lib.EnumFromScopedModule.class;
case "@scope/jsii-calc-lib.IDoublable": return software.amazon.jsii.tests.calculator.lib.IDoublable.class;
case "@scope/jsii-calc-lib.IFriendly": return software.amazon.jsii.tests.calculator.lib.IFriendly.class;
case "@scope/jsii-calc-lib.IThreeLevelsInterface": return software.amazon.jsii.tests.calculator.lib.IThreeLevelsInterface.class;
case "@scope/jsii-calc-lib.MyFirstStruct": return software.amazon.jsii.tests.calculator.lib.MyFirstStruct.class;
case "@scope/jsii-calc-lib.Number": return software.amazon.jsii.tests.calculator.lib.Number.class;
case "@scope/jsii-calc-lib.Operation": return software.amazon.jsii.tests.calculator.lib.Operation.class;
case "@scope/jsii-calc-lib.StructWithOnlyOptionals": return software.amazon.jsii.tests.calculator.lib.StructWithOnlyOptionals.class;
case "@scope/jsii-calc-lib.Value": return software.amazon.jsii.tests.calculator.lib.Value.class;
default: throw new ClassNotFoundException("Unknown JSII type: " + fqn);
if (!MODULE_TYPES.containsKey(fqn)) {
throw new ClassNotFoundException("Unknown JSII type: " + fqn);
}
return this.cache.computeIfAbsent(MODULE_TYPES.get(fqn), this::findClass);
}

private Class<?> findClass(final String binaryName) {
try {
return Class.forName(binaryName);
}
catch (final ClassNotFoundException exception) {
throw new RuntimeException(exception);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@scope/jsii-calc-lib.EnumFromScopedModule=software.amazon.jsii.tests.calculator.lib.EnumFromScopedModule
@scope/jsii-calc-lib.IDoublable=software.amazon.jsii.tests.calculator.lib.IDoublable
@scope/jsii-calc-lib.IFriendly=software.amazon.jsii.tests.calculator.lib.IFriendly
@scope/jsii-calc-lib.IThreeLevelsInterface=software.amazon.jsii.tests.calculator.lib.IThreeLevelsInterface
@scope/jsii-calc-lib.MyFirstStruct=software.amazon.jsii.tests.calculator.lib.MyFirstStruct
@scope/jsii-calc-lib.Number=software.amazon.jsii.tests.calculator.lib.Number
@scope/jsii-calc-lib.Operation=software.amazon.jsii.tests.calculator.lib.Operation
@scope/jsii-calc-lib.StructWithOnlyOptionals=software.amazon.jsii.tests.calculator.lib.StructWithOnlyOptionals
@scope/jsii-calc-lib.Value=software.amazon.jsii.tests.calculator.lib.Value
Loading

0 comments on commit b9ec853

Please sign in to comment.