Skip to content

Commit

Permalink
Add the swift_interop_hint rule to be used with aspect_hints.
Browse files Browse the repository at this point in the history
The new `aspect_hints` attribute in Bazel, available on all rules, lets us attach arbitrary providers to targets that `swift_clang_module_aspect` can read. The `swift_interop_hint` rule uses this to provide the module name for whichever target references it in its `aspect_hints`.

A canonical auto-deriving hint is also provided as part of the Swift build rules (in `.../swift:auto_module`), so that the default/recommended behavior of deriving a module name from the target label can be easily obtained without having to declare one's own `swift_module_hint` targets.

This is a more principled approach to handling Swift interop than the current `tags` implementation (which will be removed in a future change), and lets us associate additional metadata easily in the future, including files (for example, custom module maps or APINotes).

PiperOrigin-RevId: 387147846
  • Loading branch information
allevato authored and swiple-rules-gardener committed Jul 27, 2021
1 parent dad2488 commit c42a37a
Show file tree
Hide file tree
Showing 7 changed files with 193 additions and 7 deletions.
19 changes: 16 additions & 3 deletions examples/xplatform/c_from_swift/BUILD
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
load("//swift:swift.bzl", "swift_binary", "swift_library")
load(
"//swift:swift.bzl",
"swift_binary",
"swift_interop_hint",
"swift_library",
)

licenses(["notice"])

Expand All @@ -9,15 +14,23 @@ cc_library(
hdrs = ["counter.h"],
)

# 2. ...but Swift can't import C++ yet, so we implement a wrapper API in C.
# 2. ...but Swift can't import C++ yet, so we implement a wrapper API in C. Use
# the `swift_interop_hint` rule to enable module map generation and provide the
# module name for these headers, since `cc_library` doesn't do enable this by
# default.
cc_library(
name = "c_counter",
srcs = ["c_counter.cc"],
hdrs = ["c_counter.h"],
tags = ["swift_module=CCounter"],
aspect_hints = [":c_counter_swift_hint"],
deps = [":counter"],
)

swift_interop_hint(
name = "c_counter_swift_hint",
module_name = "CCounter",
)

# 3. The Swift library then depends on the `cc_library`. This causes a
# Swift-compatible module map to be created for the `cc_library` so that the
# Swift code can import it.
Expand Down
9 changes: 9 additions & 0 deletions swift/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ load(
"//swift/internal:build_settings.bzl",
"per_module_swiftcopt_flag",
)
load(":swift.bzl", "swift_interop_hint")

package(default_visibility = ["//visibility:public"])

Expand Down Expand Up @@ -39,6 +40,14 @@ filegroup(
],
)

# An aspect hint that enables module map generation for a non-Swift,
# non-Objective-C target, deriving the module name automatically based on the
# hinted target's label.
swift_interop_hint(
name = "auto_module",
visibility = ["//visibility:public"],
)

# User settable flag that specifies additional Swift copts on a per-swiftmodule basis.
per_module_swiftcopt_flag(
name = "per_module_swiftcopt",
Expand Down
1 change: 1 addition & 0 deletions swift/internal/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ bzl_library(
"swift_feature_allowlist.bzl",
"swift_grpc_library.bzl",
"swift_import.bzl",
"swift_interop_hint.bzl",
"swift_library.bzl",
"swift_module_alias.bzl",
"swift_proto_library.bzl",
Expand Down
59 changes: 57 additions & 2 deletions swift/internal/swift_clang_module_aspect.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,61 @@ def _handle_module(

return providers

def _find_swift_interop_info(target, aspect_ctx):
"""Finds a `_SwiftInteropInfo` provider associated with the target.
This function first looks at the target itself to determine if it propagated
a `_SwiftInteropInfo` provider directly (that is, its rule implementation
function called `swift_common.create_swift_interop_info`). If it did not,
then the target's `aspect_hints` attribute is checked for a reference to a
target that propagates `_SwiftInteropInfo` (such as `swift_interop_hint`).
It is an error if `aspect_hints` contains two or more targets that propagate
`_SwiftInteropInfo`, or if the target directly propagates the provider and
there is also any target in `aspect_hints` that propagates it.
Args:
target: The target to which the aspect is currently being applied.
aspect_ctx: The aspect's context.
Returns:
The `_SwiftInteropInfo` associated with the target, if found; otherwise,
None.
"""
interop_target = None
interop_from_rule = False

if _SwiftInteropInfo in target:
interop_target = target
interop_from_rule = True

# We don't break this loop early when we find a matching hint, because we
# want to give an error message if there are two aspect hints that provide
# `_SwiftInteropInfo` (or if both the rule and an aspect hint do).
for hint in aspect_ctx.rule.attr.aspect_hints:
if _SwiftInteropInfo in hint:
if interop_target:
if interop_from_rule:
fail(("Conflicting Swift interop info from the target " +
"'{target}' ({rule} rule) and the aspect hint " +
"'{hint}'. Only one is allowed.").format(
hint = str(hint.label),
target = str(target.label),
rule = aspect_ctx.rule.kind,
))
else:
fail(("Conflicting Swift interop info from aspect hints " +
"'{hint1}' and '{hint2}'. Only one is " +
"allowed.").format(
hint1 = str(interop_target.label),
hint2 = str(hint.label),
))
interop_target = hint

if interop_target:
return interop_target[_SwiftInteropInfo]
return None

def _swift_clang_module_aspect_impl(target, aspect_ctx):
# Do nothing if the target already propagates `SwiftInfo`.
if SwiftInfo in target:
Expand All @@ -522,8 +577,8 @@ def _swift_clang_module_aspect_impl(target, aspect_ctx):
requested_features = aspect_ctx.features
unsupported_features = aspect_ctx.disabled_features

if _SwiftInteropInfo in target:
interop_info = target[_SwiftInteropInfo]
interop_info = _find_swift_interop_info(target, aspect_ctx)
if interop_info:
module_map_file = interop_info.module_map
module_name = (
interop_info.module_name or derive_module_name(target.label)
Expand Down
101 changes: 101 additions & 0 deletions swift/internal/swift_interop_hint.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Copyright 2021 The Bazel Authors. All rights reserved.
#
# 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.

"""Implementation of the `swift_interop_hint` rule."""

load(":swift_common.bzl", "swift_common")

def _swift_interop_hint_impl(ctx):
# TODO(b/194733180): For now, this rule only supports interop via an
# auto-derived module name or an explicit module name, but still using the
# auto-generated module name. Add support for manual module maps and other
# features, like APINotes, later.
return swift_common.create_swift_interop_info(
module_name = ctx.attr.module_name,
)

swift_interop_hint = rule(
attrs = {
"module_name": attr.string(
doc = """\
The name that will be used to import the hinted module into Swift.
If left unspecified, the module name will be computed based on the hinted
target's build label, by stripping the leading `//` and replacing `/`, `:`, and
other non-identifier characters with underscores.
""",
mandatory = False,
),
},
doc = """\
Defines an aspect hint that associates non-Swift BUILD targets with additional
information required for them to be imported by Swift.
Some build rules, such as `objc_library`, support interoperability with Swift
simply by depending on them; a module map is generated automatically. This is
for convenience, because the common case is that most `objc_library` targets
contain code that is compatible (i.e., capable of being imported) by Swift.
For other rules, like `cc_library`, additional information must be provided to
indicate that a particular target is compatible with Swift. This is done using
the `aspect_hints` attribute and the `swift_interop_hint` rule.
#### Using the automatically derived module name (recommended)
If you want to import a non-Swift, non-Objective-C target into Swift using the
module name that is automatically derived from the BUILD label, there is no need
to declare an instance of `swift_interop_hint`. A canonical one that requests
module name derivation has been provided in
`@build_bazel_rules_swift//swift:auto_module`. Simply add it to the `aspect_hints` of
the target you wish to import:
```build
# //my/project/BUILD
cc_library(
name = "somelib",
srcs = ["somelib.c"],
hdrs = ["somelib.h"],
aspect_hints = ["@build_bazel_rules_swift//swift:auto_module"],
)
```
When this `cc_library` is a dependency of a Swift target, a module map will be
generated for it. In this case, the module's name would be `my_project_somelib`.
#### Using an explicit module name
If you need to provide an explicit name for the module (for example, if it is
part of a third-party library that expects to be imported with a specific name),
then you can declare your own `swift_interop_hint` target to define the name:
```build
# //my/project/BUILD
cc_library(
name = "somelib",
srcs = ["somelib.c"],
hdrs = ["somelib.h"],
aspect_hints = [":somelib_swift_interop"],
)
swift_interop_hint(
name = "somelib_swift_interop",
module_name = "CSomeLib",
)
```
When this `cc_library` is a dependency of a Swift target, a module map will be
generated for it with the module name `CSomeLib`.
""",
implementation = _swift_interop_hint_impl,
)
5 changes: 5 additions & 0 deletions swift/swift.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ load(
"@build_bazel_rules_swift//swift/internal:swift_import.bzl",
_swift_import = "swift_import",
)
load(
"@build_bazel_rules_swift//swift/internal:swift_interop_hint.bzl",
_swift_interop_hint = "swift_interop_hint",
)
load(
"@build_bazel_rules_swift//swift/internal:swift_library.bzl",
_swift_library = "swift_library",
Expand Down Expand Up @@ -86,6 +90,7 @@ swift_c_module = _swift_c_module
swift_feature_allowlist = _swift_feature_allowlist
swift_grpc_library = _swift_grpc_library
swift_import = _swift_import
swift_interop_hint = _swift_interop_hint
swift_library = _swift_library
swift_module_alias = _swift_module_alias
swift_proto_library = _swift_proto_library
Expand Down
6 changes: 4 additions & 2 deletions test/fixtures/private_deps/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ swift_library(
cc_library(
name = "private_cc",
hdrs = ["private.h"],
aspect_hints = ["//swift:auto_module"],
features = [
# A bit hacky, but by claiming we don't support PIC, we can get the
# output libraries in `libraries_to_link.static_library` instead of
Expand All @@ -51,18 +52,19 @@ cc_library(
"-pic",
"-supports_pic",
],
tags = FIXTURE_TAGS + ["swift_module"],
tags = FIXTURE_TAGS,
)

cc_library(
name = "public_cc",
hdrs = ["public.h"],
aspect_hints = ["//swift:auto_module"],
features = [
# See the comment in the target above.
"-pic",
"-supports_pic",
],
tags = FIXTURE_TAGS + ["swift_module"],
tags = FIXTURE_TAGS,
)

swift_library(
Expand Down

2 comments on commit c42a37a

@keith
Copy link
Member

@keith keith commented on c42a37a Sep 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brentleyjones
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.