From d79471ef5c8987fa8bd4a83d24d5ff3bebe8c070 Mon Sep 17 00:00:00 2001 From: Jaroslav Bachorik Date: Tue, 28 Jul 2026 11:55:32 +0200 Subject: [PATCH] feat(development): add the extension authoring skill Covers designing a new extension against a target library, which nothing in the suite addressed. It sits in btrace-development because the trigger is building something rather than diagnosing a running system, beside the permissions skill it depends on. The guidance is bounded by what @ExternalType can actually express, which is narrower than it first appears and is not stated anywhere an author would look: - The whole method signature must be nameable at compile time. Adapters are emitted with an exact MethodType from erased declared types and handed to findVirtual/findStatic, so any target method taking or returning a target-library type is unreachable. Overloads are rejected outright, and fields, constructors, instanceof and non-public members are unsupported. - Static dispatch resolves the owning class through the thread context class loader while virtual dispatch uses the receiver's defining loader. The defining loader is the documented guidance, and a context loader is whatever the application thread happens to have set. - Class resolution runs on every call. Only the method handle is cached, keyed by the resolved class, so a class that never resolves is never cached and the failure repeats at the probe call site as an undeclared exception. So the skill routes anything richer, and anything version-variant, to ClassLoadingUtil and MethodHandleCache, whose failures are deliberately not cached and therefore work as a capability check that recovers if the target appears later. The @ExternalType gaps are recorded upstream in btraceio/btrace#931. It also states the implementation naming rule as an exact name or a provider declaration, rather than treating the provider file as optional. Skipping it is what left the Spark example unable to resolve its own service. --- CHANGELOG.md | 1 + evals/btrace-observability.json | 18 ++++++ .../btrace-extension-authoring/SKILL.md | 55 +++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 plugins/btrace-development/skills/btrace-extension-authoring/SKILL.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 409249f..d339f33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,3 +7,4 @@ - Provider-neutral eval corpus and repository validation gate. - `btrace-legacy-libs-migration` skill for moving retired `libs`/profile packaging into an extension. - Moved `btrace-extensions-and-permissions` into `btrace-development`; extension authoring is build-time work, not incident diagnosis. +- `btrace-extension-authoring` skill for designing a new extension against a target library. diff --git a/evals/btrace-observability.json b/evals/btrace-observability.json index e06fb9c..87e8e44 100644 --- a/evals/btrace-observability.json +++ b/evals/btrace-observability.json @@ -167,5 +167,23 @@ "grantAll=true" ], "rubric": "Recognise that libs= no longer loads anything and the missing classes are the symptom. Direct to one extension project with a single source set and declared services, not separate API and impl modules. Keep permission grants in permissions.properties rather than extensions.conf. Include verification by attaching and taking a trace." + }, + { + "id": "extension-authoring-target-library", + "prompt": "I want to trace a third-party job scheduler library from BTrace. Its listener class is only on the application classpath and some methods only exist in newer versions. How should I structure the extension?", + "requiredSkills": [ + "btrace-extension-authoring" + ], + "mustMention": [ + "Object", + "MethodHandleCache", + "Impl", + "version" + ], + "mustAvoid": [ + "publicLookup", + "grantAll=true" + ], + "rubric": "Analyse before scaffolding. Keep target types out of the service signature and hand the object across as Object. Use @ExternalType only where the signature is nameable at compile time, and MethodHandleCache for version-variant members because its failures are not cached. Name the implementation Impl or ship a provider declaration." } ] diff --git a/plugins/btrace-development/skills/btrace-extension-authoring/SKILL.md b/plugins/btrace-development/skills/btrace-extension-authoring/SKILL.md new file mode 100644 index 0000000..4cb676a --- /dev/null +++ b/plugins/btrace-development/skills/btrace-extension-authoring/SKILL.md @@ -0,0 +1,55 @@ +--- +name: btrace-extension-authoring +description: Use when designing a new BTrace extension for a target library, framework, or runtime, deciding how a probe should reach target types safely, or choosing between @ExternalType and hand-written method handles. +--- + +# Extension Authoring + +Start in analysis only. Establish which target entry points are public and stable, which versions +must be supported, and which types a probe actually needs, then say what the extension would look +like before creating any files. Report use of internal or version-sensitive target APIs as a risk +at this point rather than after scaffolding. + +Derive the service surface from the probe-facing use cases, because everything downstream follows +from it. Service methods take and return only primitives, `java.*` types, and the extension's own +types; a target object crosses the boundary as `Object` and is resolved inside the implementation. +Keeping target types out of the service signature is what lets the extension load when the target +library is absent or a different version than expected. + +Model an individual target type with `@ExternalType`, whose processor emits a companion adapter +with cached method handles. It fits a narrow shape: the whole signature must be nameable at +compile time, so any method that takes or returns a target-library type is out of reach; overloaded +names are rejected outright; fields, constructors, `instanceof`, and non-public members are not +supported; and members of packages a named module does not export are inaccessible. Check a target +method against that list before assuming the annotation can express it. + +Note also that static dispatch resolves the owning class through the thread context class loader +while virtual dispatch uses the receiver's defining loader. Prefer virtual dispatch on a handed-off +receiver where a choice exists, because a context loader is whatever the application thread happens +to have set, and is frequently wrong under application servers, OSGi, and plugin loaders. Class +resolution is not cached, so a missing target class throws on every call rather than once, and it +surfaces at the probe call site rather than at load. + +Use `ClassLoadingUtil` and `MethodHandleCache` for everything the annotation cannot express, and +for anything that varies across target versions. That cache stores successful lookups and never +stores failures, so catching a lookup failure is a genuine capability check that degrades to a +reduced feature set and recovers if the class appears later. `@ExternalType` offers no such catch +point, so version-variant members belong on the hand-written path. + +Nothing in the metadata describes which target versions an extension supports; that is author +discipline. Because binding is per method and per class, a member missing from one version fails +only where it is used. Hand-written stubs of the target API under the test source set give unit +tests realistic fixtures and let a version matrix be exercised without the real dependency. + +Name the implementation exactly `Impl`, or declare it in +`META-INF/services/`. Separating `api` and `impl` packages is reasonable and +requires the provider declaration, because the fallback is an exact name and nothing else. Extend +`Extension` when the service needs the extension context, initialization, or cleanup on detach; a +stateless service is better without it, since instantiation fails when no context is available. + +Verify by inspecting the packaged artifacts, installing, attaching, and taking one trace that +exercises the injected service against the real target library rather than a stub. + +Packaging mechanics, permission scanning, and the shared project layout are covered by +`btrace-extensions-and-permissions`. Fat agents for deployment are covered by the +`btrace-observability` plugin's startup and packaging guidance.