Skip to content

Commit a4e8f95

Browse files
xaionaro@dx.centerxaionaro@dx.center
authored andcommitted
feat(javagen): generate Java adapter classes for abstract callback classes
The specgen now detects abstract classes (e.g. ScanCallback, BluetoothGattCallback) and emits abstract_callbacks entries in the YAML specs. The javagen reads these entries and generates Java adapter classes that extend the abstract class, delegating all methods to GoAbstractDispatch.invoke(handlerID, methodName, args). The generated adapters are placed in a java/ subdirectory alongside the Go output and follow the center.dx.jni.generated package convention. The adapter naming (SimpleClassName + "Adapter") matches the naming convention that tryAbstractAdapter in proxy.go already searches for, so no changes to the runtime proxy fallback logic are needed beyond adding a secondary search pattern.
1 parent 6e4053b commit a4e8f95

22 files changed

Lines changed: 955 additions & 16 deletions

proxy.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -547,9 +547,12 @@ func tryAbstractAdapter(
547547
simpleName = fullName[idx+1:]
548548
}
549549

550-
// Try adapter class names.
550+
// Try adapter class names. The first pattern matches adapters generated
551+
// by javagen (e.g. ScanCallbackAdapter). The second pattern matches
552+
// the legacy hand-written naming in the gatt repo.
551553
adapterNames := []string{
552554
"center/dx/jni/generated/" + simpleName + "Adapter",
555+
"center/dx/jni/generated/Go" + simpleName + "Adapter",
553556
"center/dx/gatt/internal/Go" + simpleName,
554557
}
555558

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package center.dx.jni.generated;
2+
3+
import {{.JavaClass}};
4+
import center.dx.jni.internal.GoAbstractDispatch;
5+
6+
public class {{.AdapterClassName}} extends {{.JavaClass}} {
7+
private final long handlerID;
8+
9+
public {{.AdapterClassName}}(long handlerID) {
10+
this.handlerID = handlerID;
11+
}
12+
{{range .Methods}}
13+
@Override
14+
public {{.JavaReturnType}} {{.JavaMethod}}({{.JavaParamList}}) {
15+
{{- if .HasReturn}}
16+
return {{.JavaCastReturn}} GoAbstractDispatch.invoke(
17+
handlerID, "{{.JavaMethod}}", new Object[]{ {{.JavaArgList}} }){{.JavaUnboxReturn}};
18+
{{- else}}
19+
GoAbstractDispatch.invoke(
20+
handlerID, "{{.JavaMethod}}", new Object[]{ {{.JavaArgList}} });
21+
{{- end}}
22+
}
23+
{{end -}}
24+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package javagen
2+
3+
// AbstractCallback describes an abstract Java class whose abstract methods
4+
// are delegated to Go via GoAbstractDispatch.
5+
type AbstractCallback struct {
6+
JavaClass string `yaml:"java_class"`
7+
GoType string `yaml:"go_type"`
8+
Methods []AbstractCallbackMethod `yaml:"methods"`
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package javagen
2+
3+
// AbstractCallbackMethod describes a single abstract method in an abstract callback class.
4+
type AbstractCallbackMethod struct {
5+
JavaMethod string `yaml:"java_method"`
6+
Params []string `yaml:"params"`
7+
Returns string `yaml:"returns"`
8+
GoField string `yaml:"go_field"`
9+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package javagen
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"text/template"
9+
)
10+
11+
// javaGeneratedHeader is prepended to all generated Java files.
12+
var javaGeneratedHeader = "// Code generated by javagen. DO NOT " + "EDIT.\n\n"
13+
14+
// renderAbstractAdapters generates a Java adapter class for each abstract
15+
// callback. The Java files are placed in a java/ subdirectory alongside the
16+
// Go output, and follow the center.dx.jni.generated package convention.
17+
func renderAbstractAdapters(
18+
templatesDir string,
19+
merged *MergedSpec,
20+
pkgDir string,
21+
) error {
22+
tmplPath := filepath.Join(templatesDir, "abstract_adapter.java.tmpl")
23+
tmplData, err := os.ReadFile(tmplPath)
24+
if err != nil {
25+
// Template not found: nothing to render.
26+
return nil
27+
}
28+
29+
funcMap := template.FuncMap{
30+
"javaClassToSlash": JavaClassToSlash,
31+
}
32+
33+
tmpl, err := template.New("abstract_adapter.java.tmpl").Funcs(funcMap).Parse(string(tmplData))
34+
if err != nil {
35+
return fmt.Errorf("parse abstract adapter template: %w", err)
36+
}
37+
38+
javaDir := filepath.Join(pkgDir, "java")
39+
if err := os.MkdirAll(javaDir, dirPerm); err != nil {
40+
return fmt.Errorf("mkdir java: %w", err)
41+
}
42+
43+
cleanStaleJavaFiles(javaDir)
44+
45+
for i := range merged.AbstractCallbacks {
46+
acb := &merged.AbstractCallbacks[i]
47+
outputFile := filepath.Join(javaDir, acb.AdapterClassName()+".java")
48+
if err := renderJavaTemplate(tmpl, acb, outputFile); err != nil {
49+
return fmt.Errorf("render adapter %s: %w", acb.GoType, err)
50+
}
51+
}
52+
53+
return nil
54+
}
55+
56+
// renderJavaTemplate executes a Java template and writes the result.
57+
func renderJavaTemplate(
58+
tmpl *template.Template,
59+
data any,
60+
outputPath string,
61+
) error {
62+
var buf bytes.Buffer
63+
if err := tmpl.Execute(&buf, data); err != nil {
64+
return fmt.Errorf("execute template: %w", err)
65+
}
66+
67+
src := javaGeneratedHeader + buf.String()
68+
return os.WriteFile(outputPath, []byte(src), filePerm)
69+
}
70+
71+
// cleanStaleJavaFiles removes generated .java files from a directory.
72+
func cleanStaleJavaFiles(dir string) {
73+
entries, err := os.ReadDir(dir)
74+
if err != nil {
75+
return
76+
}
77+
for _, e := range entries {
78+
if e.IsDir() || filepath.Ext(e.Name()) != ".java" {
79+
continue
80+
}
81+
path := filepath.Join(dir, e.Name())
82+
data, err := os.ReadFile(path)
83+
if err != nil {
84+
continue
85+
}
86+
if bytes.Contains(data, []byte("Code generated by javagen. DO NOT EDIT.")) {
87+
_ = os.Remove(path)
88+
}
89+
}
90+
}

tools/pkg/javagen/e2e_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,84 @@ func TestGenerate_ContentPatterns(t *testing.T) {
428428
}
429429
}
430430

431+
// TestGenerate_AbstractCallbackAdapters verifies that abstract callback
432+
// entries in a spec produce Java adapter files in a java/ subdirectory.
433+
func TestGenerate_AbstractCallbackAdapters(t *testing.T) {
434+
root := findRepoRoot(t)
435+
templatesDir := filepath.Join(root, "templates", "java")
436+
outputDir := t.TempDir()
437+
438+
specYAML := `
439+
package: test_le
440+
go_import: github.com/AndroidGoLab/jni/test_le
441+
classes:
442+
- java_class: android.bluetooth.le.BluetoothLeScanner
443+
go_type: Scanner
444+
methods:
445+
- java_method: startScan
446+
go_name: StartScan
447+
params:
448+
- java_type: android.bluetooth.le.ScanCallback
449+
go_name: arg0
450+
returns: void
451+
error: true
452+
abstract_callbacks:
453+
- java_class: android.bluetooth.le.ScanCallback
454+
go_type: ScanCallbackCB
455+
methods:
456+
- java_method: onScanFailed
457+
params:
458+
- int
459+
returns: void
460+
go_field: OnScanFailed
461+
- java_method: onScanResult
462+
params:
463+
- int
464+
- android.bluetooth.le.ScanResult
465+
returns: void
466+
go_field: OnScanResult
467+
`
468+
specPath := filepath.Join(t.TempDir(), "test.yaml")
469+
if err := os.WriteFile(specPath, []byte(specYAML), 0o644); err != nil {
470+
t.Fatal(err)
471+
}
472+
473+
overlayPath := filepath.Join(t.TempDir(), "nonexistent.yaml")
474+
if err := Generate(specPath, overlayPath, templatesDir, outputDir, testGoModule); err != nil {
475+
t.Fatalf("Generate: %v", err)
476+
}
477+
478+
// Verify Java adapter file was generated.
479+
javaFile := filepath.Join(outputDir, "test_le", "java", "ScanCallbackAdapter.java")
480+
data, err := os.ReadFile(javaFile)
481+
if err != nil {
482+
t.Fatalf("expected Java adapter file at %s: %v", javaFile, err)
483+
}
484+
485+
content := string(data)
486+
if !strings.Contains(content, "Code generated by javagen. DO NOT EDIT.") {
487+
t.Error("missing generated header")
488+
}
489+
if !strings.Contains(content, "extends android.bluetooth.le.ScanCallback") {
490+
t.Error("missing extends clause")
491+
}
492+
if !strings.Contains(content, "ScanCallbackAdapter") {
493+
t.Error("missing adapter class name")
494+
}
495+
if !strings.Contains(content, "GoAbstractDispatch.invoke") {
496+
t.Error("missing GoAbstractDispatch.invoke")
497+
}
498+
if !strings.Contains(content, `"onScanFailed"`) {
499+
t.Error("missing onScanFailed dispatch")
500+
}
501+
if !strings.Contains(content, `"onScanResult"`) {
502+
t.Error("missing onScanResult dispatch")
503+
}
504+
if !strings.Contains(content, "Integer.valueOf(arg0)") {
505+
t.Error("missing int autoboxing")
506+
}
507+
}
508+
431509
func readFile(t *testing.T, path string) string {
432510
t.Helper()
433511
data, err := os.ReadFile(path)

tools/pkg/javagen/merge.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ func Merge(spec *Spec, overlay *Overlay) (*MergedSpec, error) {
5353
merged.Callbacks = append(merged.Callbacks, *mcb)
5454
}
5555

56+
for _, acb := range spec.AbstractCallbacks {
57+
macb := mergeAbstractCallback(&acb, overlay)
58+
merged.AbstractCallbacks = append(merged.AbstractCallbacks, *macb)
59+
}
60+
5661
merged.ConstantGroups = mergeConstants(spec.Constants)
5762

5863
resolveConstantTypeCollisions(merged)
@@ -316,6 +321,45 @@ func mergeCallback(cb *Callback, overlay *Overlay) (*MergedCallback, error) {
316321
return mcb, nil
317322
}
318323

324+
func mergeAbstractCallback(acb *AbstractCallback, overlay *Overlay) *MergedAbstractCallback {
325+
macb := &MergedAbstractCallback{
326+
JavaClass: acb.JavaClass,
327+
JavaClassSlash: JavaClassToSlash(acb.JavaClass),
328+
GoType: acb.GoType,
329+
}
330+
331+
for _, m := range acb.Methods {
332+
var params []MergedParam
333+
for i, jt := range m.Params {
334+
goName := fmt.Sprintf("arg%d", i)
335+
isString := jt == "String" || jt == "java.lang.String" || jt == "java.lang.CharSequence" || jt == "CharSequence"
336+
goType := "*jni.Object"
337+
if isString {
338+
goType = "string"
339+
}
340+
params = append(params, MergedParam{
341+
JavaType: jt,
342+
GoName: goName,
343+
GoType: goType,
344+
IsString: isString,
345+
IsObject: true,
346+
})
347+
}
348+
349+
goParams := buildGoParamList(params)
350+
351+
macb.Methods = append(macb.Methods, MergedAbstractCallbackMethod{
352+
JavaMethod: m.JavaMethod,
353+
GoField: m.GoField,
354+
Params: params,
355+
GoParams: goParams,
356+
Returns: m.Returns,
357+
})
358+
}
359+
360+
return macb
361+
}
362+
319363
// deriveJavaPackageDesc extracts the Java package name from the first
320364
// class in the spec to produce a human-readable description for the
321365
// doc.go package comment. Returns the Java package (e.g.
@@ -429,6 +473,9 @@ func resolveConstantTypeCollisions(merged *MergedSpec) {
429473
for _, cb := range merged.Callbacks {
430474
typeNames[cb.GoType] = struct{}{}
431475
}
476+
for _, acb := range merged.AbstractCallbacks {
477+
typeNames[acb.GoType] = struct{}{}
478+
}
432479
for _, grp := range merged.ConstantGroups {
433480
if grp.GoType != "" {
434481
typeNames[grp.GoType] = struct{}{}

0 commit comments

Comments
 (0)