Data Flow: Configuration → C++ → JavaScript
workerd.capnp config → server.c++ (parsing) → WorkerdApi::Global → createBindingValue() → v8::Object (env)
File: src/workerd/server/workerd.capnp
- Lines 335-474:
struct Binding- defines all binding types - Lines 578-596:
struct WrappedBinding- for composed bindings like D1
struct Binding {
name @0 :Text;
union {
text @4 :Text;
service @9 :ServiceDesignator;
kvNamespace @11 :ServiceDesignator;
wrapped @14 :WrappedBinding;
# ... other binding types
}
}
struct WrappedBinding {
moduleName @0 :Text; # e.g., "cloudflare-internal:d1-api"
entrypoint @1 :Text = "default";
innerBindings @2 :List(Binding); # e.g., fetcher service binding
}File: src/workerd/server/server.c++
- Lines 3437-3683:
createBinding()- converts capnp config toWorkerdApi::Global
// server.c++ line 3664-3682
case config::Worker::Binding::WRAPPED: {
auto wrapped = binding.getWrapped();
kj::Vector<Global> innerGlobals;
for (const auto& innerBinding: wrapped.getInnerBindings()) {
KJ_IF_SOME(global, createBinding(...)) {
innerGlobals.add(kj::mv(global));
}
}
return makeGlobal(Global::Wrapped{
.moduleName = kj::str(wrapped.getModuleName()),
.entrypoint = kj::str(wrapped.getEntrypoint()),
.innerBindings = innerGlobals.releaseAsArray(),
});
}File: src/workerd/server/workerd-api.h
- Lines 85-280:
struct Global- intermediate representation of bindings
struct Global {
kj::String name;
kj::OneOf<
Json,
Fetcher, // service bindings (channel-based)
KvNamespace, // KV (channel + bindingName)
R2Bucket, // R2 (channel + bucket + bindingName)
Wrapped, // D1, etc. (moduleName + entrypoint + innerBindings)
DurableActorNamespace,
// ... more types
> value;
};File: src/workerd/server/workerd-api.c++
- Lines 645-841:
createBindingValue()- convertsGlobal→v8::Value
// workerd-api.c++ line 780-810 (Wrapped binding instantiation)
KJ_CASE_ONEOF(wrapped, Global::Wrapped) {
auto moduleRegistry = jsg::ModuleRegistry::from(lock);
auto moduleName = kj::Path::parse(wrapped.moduleName); // "cloudflare-internal:d1-api"
KJ_IF_SOME(moduleInfo, moduleRegistry->resolve(lock, moduleName, ...)) {
auto module = moduleInfo.module.getHandle(lock);
jsg::instantiateModule(lock, module);
// Build env object with inner bindings (e.g., {fetcher: Fetcher})
auto env = v8::Object::New(lock.v8Isolate);
for (const auto& innerBinding: wrapped.innerBindings) {
lock.v8Set(env, innerBinding.name,
createBindingValue(lock, innerBinding, ...)); // Recursive!
}
// Call makeBinding(env) → D1Database instance
auto fn = lock.v8Get(moduleNs, wrapped.entrypoint); // "default"
value = fn->Call(context, context->Global(), 1, &env);
}
}File: src/workerd/io/worker.c++
- Lines 1704-1928:
Worker::Worker()constructor
// worker.c++ line 1800-1818
if (useModuleSyntax) {
bindingsScope = v8::Object::New(lock.v8Isolate); // Creates 'env'
} else {
bindingsScope = context->Global(); // Service worker syntax: globals
}
// Load all globals/bindings
for (auto& global: script->impl->globals) {
lock.v8Set(bindingsScope, global.name, global.value);
}
compileBindings(lock, api, bindingsScope, ctxExports); // Calls compileGlobals
// For ES modules, attach env to handler
obj.env = lock.v8Ref(bindingsScope.As<v8::Value>());File: src/workerd/jsg/jsg.h
- Line 56-82:
JSG_RESOURCE_TYPEmacro
// Example: KV binding class (kv.h line 22-276)
class KvNamespace: public jsg::Object {
public:
explicit KvNamespace(kj::String bindingName,
kj::Array<AdditionalHeader> additionalHeaders,
uint subrequestChannel)
: additionalHeaders(kj::mv(additionalHeaders)),
subrequestChannel(subrequestChannel),
bindingName(kj::mv(bindingName)) {}
jsg::Promise<GetResult> get(jsg::Lock& js, kj::String name, ...);
JSG_RESOURCE_TYPE(KvNamespace, CompatibilityFlags::Reader flags) {
JSG_METHOD(get);
JSG_METHOD(list);
JSG_METHOD(put);
// ...
}
private:
uint subrequestChannel; // Channel ID for IoContext::getSubrequestChannel()
};D1 uses the "Wrapped Binding" pattern:
d1-api-test.wd-test line 14-24:
bindings = [(
name = "d1",
wrapped = (
moduleName = "cloudflare-internal:d1-api",
innerBindings = [(
name = "fetcher",
service = "d1-mock"
)],
)
)]src/cloudflare/internal/d1-api.ts:
// Line 811-813
export default function makeBinding(env: { fetcher: Fetcher }): D1Database {
return new D1Database(env.fetcher);
}
// D1Database uses fetcher.fetch() for all operations
class D1Database {
constructor(fetcher: Fetcher) {
this.fetcher = fetcher;
}
// All operations go through HTTP to the d1 service
async _send(endpoint: string, ...) {
const response = await this._wrappedFetch(url.href, {...});
// ...
}
}src/workerd/api/modules.h line 93:
registry.addBuiltinBundle(CLOUDFLARE_BUNDLE); // Contains d1-api.jssrc/cloudflare/BUILD.bazel:
wd_ts_bundle(
name = "cloudflare",
import_name = "cloudflare",
internal_modules = glob(["internal/*.ts"]), # Includes d1-api.ts
# ...
)File: src/workerd/io/io-context.h
- Lines 835-888: Channel-based subrequest system
// Each binding gets a numeric "channel" that maps to a service
kj::Own<WorkerInterface> getSubrequestChannel(
uint channel, // Index into IoChannelFactory
bool isInHouse, // KV/R2 = true, user services = false
kj::Maybe<kj::String> cfBlobJson,
kj::ConstString operationName);Channel Assignment (server.c++ line 3630-3637):
case config::Worker::Binding::KV_NAMESPACE: {
uint channel = static_cast<uint>(subrequestChannels.size()) +
IoContext::SPECIAL_SUBREQUEST_CHANNEL_COUNT; // Starts at 2
subrequestChannels.add(FutureSubrequestChannel{binding.getKvNamespace(), ...});
return makeGlobal(Global::KvNamespace{
.subrequestChannel = channel, .bindingName = kj::str(binding.getName())});
}┌─────────────────────────────────────────────────────────────────────────────┐
│ Configuration Layer │
├─────────────────────────────────────────────────────────────────────────────┤
│ workerd.capnp: │
│ bindings = [(name = "MY_D1", wrapped = (moduleName = "...:d1-api", ...))] │
└───────────────────────────────────┬─────────────────────────────────────────┘
│ parsed by
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ C++ Binding Creation │
├─────────────────────────────────────────────────────────────────────────────┤
│ server.c++: createBinding() → WorkerdApi::Global struct │
│ workerd-api.c++: createBindingValue() → v8::Local<v8::Value> │
│ │
│ For wrapped bindings: │
│ 1. Resolve module from ModuleRegistry │
│ 2. Create inner bindings (e.g., Fetcher with channel) │
│ 3. Call module's makeBinding(env) → returns JS object │
└───────────────────────────────────┬─────────────────────────────────────────┘
│ attached to
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ Worker Initialization │
├─────────────────────────────────────────────────────────────────────────────┤
│ worker.c++: Worker constructor │
│ - Creates v8::Object for 'env' │
│ - Populates with all binding values │
│ - Attaches to ExportedHandler.env │
└───────────────────────────────────┬─────────────────────────────────────────┘
│ exposed to
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ JavaScript Runtime │
├─────────────────────────────────────────────────────────────────────────────┤
│ export default { fetch(request, env) { ... } } │
│ │
│ env.MY_D1 → D1Database instance │
│ └── this.fetcher → Fetcher (JSG_RESOURCE_TYPE) │
│ └── fetch() → IoContext::getSubrequestChannel(channel) │
│ └── IoChannelFactory::startSubrequest() │
│ └── WorkerInterface → HTTP to D1 service │
└─────────────────────────────────────────────────────────────────────────────┘
| Abstraction | File | Purpose |
|---|---|---|
Binding (capnp) |
workerd.capnp:335 |
Configuration schema |
WorkerdApi::Global |
workerd-api.h:85 |
C++ intermediate representation |
createBinding() |
server.c++:3437 |
Config → Global conversion |
createBindingValue() |
workerd-api.c++:645 |
Global → v8::Value |
JSG_RESOURCE_TYPE |
jsg.h:56 |
C++ class → JS object binding |
IoChannelFactory |
io-channels.h:97 |
I/O channel abstraction |
subrequestChannel |
throughout | Numeric channel for service routing |
| Wrapped binding | d1-api.ts |
TS module wrapping inner bindings |
Direct C++ class with JSG_RESOURCE_TYPE:
// Created in createBindingValue()
value = lock.wrap(context,
lock.alloc<api::KvNamespace>(kj::str(ns.bindingName), ..., ns.subrequestChannel));TypeScript module wrapping inner bindings:
// cloudflare-internal:d1-api
export default function makeBinding(env: { fetcher: Fetcher }): D1Database {
return new D1Database(env.fetcher);
}Simple Fetcher with channel:
return makeGlobal(Global::Fetcher{
.channel = channel, .requiresHost = true, .isInHouse = false});