The Future of Reflective Programming in Swift

Hello Swift Community,

TL;DR

Swift has various reflection mechanisms, but they’re fragmented. By combining them, a more flexible and efficient reflection system could be established, that could enable features like compiler-synthesized conformances.

:warning: This post simply aims to start a discussion about the future of reflection and cultivate ideas for specific features; no concrete features are proposed. :warning:

Current Issues

Swift does a great job of combining runtime features with compile-time optimizations. From generic calls being devirtualized into calls with direct dispatch, to specific types to magic functions like type(of:), Swift offers great features that blur the line between dynamic and static. Reflection, though, is firmly divided into these two categories: dynamic and static, with the former offering both high- and low-level APIs.

High-level dynamic reflection brings Mirror to mind, mostly used for debugging and reverse-engineering proprietary code. It provides an easy-to-use collection of fields, each containing a label and a value in the form of safe standard-library types: String and Any. Further, library authors are given the flexibility to customize their types’ mirror representation through the CustomReflectable protocol. This safety and flexibility, though, comes at a performance cost.

Low-level dynamic APIs are the polar opposites of Mirror, being fast and slightly less safe, but hiding their not-so-intuitive APIs from public awareness. One such API is the standard library’s _forEachField, which is only available to libraries shipping with the OS, like Foundation, and offers less refined APIs with types such as C strings and access to implementation details like metadata kind. Other libraries, like TokamakUI, reimplement compiler logic, essentially creating a reflection system from scratch. These APIs are less user-friendly and don’t allow for API-author customizability, but benefit from significantly increased performance and the ability to mutate fields. Still, compile-time metaprogramming is faster.

Static metaprogramming in Swift mainly takes the form of protocol-conformance synthesis. Arguably the most ubiquitous synthesized protocols are Hashable and Codable. These protocols don’t rely on metadata to synthesize a conformance at runtime, but integrate with the compiler to synthesize static conformances. Static synthesis also comes with compile-time diagnostics, which further increase safety. Albeit having great performance, conformance synthesis can result in greater binary sizes. What’s more, the compile-time synthesizable protocols need to be coded into the compiler, limiting library authors and increasing compiler complexity.

While current reflection mechanisms are varied and can satisfy many use cases, they are fragmented. This can confuse library authors from crafting lightweight APIs without boilerplate. Such confusion is avoidable, though, since the compiler already features optimization flags to aim for decreased binary size or increased speed, not to mention that it has knowledge of client code, allowing for more effective, per-use-case optimizations. The compiler’s current lack of knowledge about reflection also means that metadata has to emitted for all nominal types, leading to unnecessary binary-size increases. Apart from performance and size limitations, the current approach deprives library authors of the much-requested compiler-synthesized conformances. A solution could thus allow for less boilerplate and faster yet smaller binaries.

While I believe this is a good starting point for a discussion, I would like to propose a general direction for a solution. This solution should combine dynamic, less-customizable APIs with compiler support for producing compile-time diagnostics, and perhaps offering standard-library-like conformance synthesis.

Potential Solution

Targeted Reflection

To start off, one of the most evident issues with Swift is lack of opt-in reflection metadata. This is an obvious step, which could be solved with a marker protocol:

@_marker
protocol Reflectable {}

Only types conforming to this protocol will have metadata emitted. This approach would be a good starting point, but miss the case where reflection is only needed for synthesizing conformances.

Scoped Reflection

A new attribute @synthesized could be introduced, which would be applicable only in protocol extensions to declarations that can witness that and parent protocols’ requirements. Inside @synthesized declarations, Self is treated as conforming to Reflectable:

public protocol Dog {
  var isParent: Bool { get }
}

extension Dog {
  @synthesized
  public var isParent: Bool {
    // Self is treated as conforming to Reflectable and
    // can be passed to generic functions.
    hasParents(self)
  }

  func hasParents<DogType: Dog & Reflectable>(dog: DogType) -> Bool { ... }
}

Similarly to access-control modifiers, @synthesized will tell the compiler exactly where metadata is needed, enabling more aggressive optimizations through scoped reflection. Namely, the compiler would emit metadata for a type only if its conformance is witnessed by at least one @synthesized declaration. However, if that declaration (1) has all its requirements for Self: Reflectable removed, and (2) the type doesn’t conform to Reflectable, no metadata would be emitted.

To force the compiler to synthesize statically, as is required for standard-library protocols like Hashable, the standard library will have access to @synthesized(__statically). This attribute would require that the declaration be @inlinable if marked public. It would work by having the compiler inline all uses of the standard-library reflection API (as shown below), such that no requirements R: Reflectable are left. If there are still such references, implying use of another reflection API, an error would be emitted.

These methods would work well to precisely describe the reflection requirements of a program to the compiler, so that the reflection APIs can be very efficient. These new reflection APIs could, in fact, be distinguished into the conformance-synthesis type and the debugging type. Conformance-synthesis would be achieved by low-level dynamic and static reflection, while Mirror would mainly be used for debugging.

Reflection Mechanisms

The Mirror API would remain as is, providing a user-friendly, intuitive debugging experience. However, creating a Mirror would only be available to Reflectable-conforming types, similarly to conformance-synthesis reflection.

The existing conformance-synthesis reflection mechanisms suffer from a lack of user-friendly APIs. Therefore, a new API will be created, that will integrate with static synthesis and compile-time diagnostics:

struct ReflectedFields<Subject> {
  struct Options: OptionSet {
    static let diagnose: Options
  }

  init(_ type: Subject.Type, options: Options = .cache) 
}

extension ReflectedFields: RandomAccessCollection {
  typealias Element = Field
 
  var startIndex: Int { get }
  var endIndex: Int { get }

  subscript(position: Int) -> Field { get }
}

extension ReflectedFields {
  struct Diagnostic {
    static let error: Diagnostic
    static let warning: Diagnostic
  }

  // For static diagnostics, the type
  // declaration's file and line will be used.
  func diagnose(
    _ diagnostic: Diagnostic,
    _ message: StaticString,
    file: StaticString = #file,
    line: UInt = #line
  )
}

extension ReflectedFields {
  struct Field {
    var path: PartialKeyPath<Subject> { get }
    var name: String { get }
 
    func diagnose(
      _ diagnostic: Diagnostic,
      _ message: StaticString,
      file: StaticString = #file,
      line: UInt = #line
    )
  }
}

This API would work dynamically like _forEachField and lack Mirror-like customizability to preserve speed. If used in a @synthesized declaration, though, the compiler would have the knowledge to inline properties, if deemed appropriate, to either synthesize statically (removing any runtime reflection) or to produce diagnostics.

To produce diagnostics, the compiler would evaluate the inlined declaration, whose implementation would, of course, be quite demanding. If it came across a Never-returning call or one of the above diagnose calls in all paths, it would emit that error. For example,

protocol Proto { func myRequirementWitness() }

extension Proto {
  @synthesized(__statically)
    func myRequirementWitness() {
      let fields = ReflectedFields(Self.self)
	  
      fields.allSatisfy { field in
        type(of: field.path).valueType == Int.self
      }
  }
}

struct MyStruct: Proto {
  let a: Int, b: String
}

// myRequirementWitness() would become:
func myRequirementWitness() {
  let fields = ReflectedFields<Self>(_fields: [
    Field(path: \.a, name: "a"),
    Field(path: \.b, name: "b")
  ]
  
  let succeeded = fields.allSatisfy { field in
    type(of: field.path).valueType == Int.self
  }

  if !succeeded { fields.diagnose(.error, "Failed") }
}

// Which would be constant-folded into:
func myRequirementWitness() {
  var succeeded = true

  if type(of: \Self.a.path).valueType != Int.self {
    succeeded = false
  }

  if type(of: \Self.b.path).valueType != Int.self {
    succeeded = false
  }

  if !succeded { fields.diagnose(.error, "Failed") }
}

// Then:
func myRequirementWitness() {
  var succeeded = true

  if Int.self != Int.self {
    succeeded = false
  }

  if String.self != Int.self {
    succeeded = false
  }

  if !succeded { fields.diagnose(.error, "Failed") }
}

// Lastly:
func myRequirementWitness() {
  if !false { fields.diagnose(.error, "Failed") }
}

// So the error is printed

Example: Hashable

public protocol Hashable: Equatable {
  func hash(into hasher: inout Hasher)
}

extension Hashable {
  @synthesized(__statically)
  @inline(__always)
  public func hash(into hasher: inout Hasher) {
    ReflectedFields(Self.self).allSatisfy { field in
      guard let property = self[keyPath: field.path] as? Hashable else {
        field.diagnose(.error, "\(field.name) does not conform to 'Hashable'")
      }
      
      hasher.combine(property)
    }
  }
}

Past Pitches

Structural

The Automatic Requirement Satisfaction in Plain Swift proposal introduced the Structural protocol, which was automatically synthesized for nominal types, encoding their structure into the type system. As Joe Groff put it:

KeyPathIterable

In the above post, Joe goes on to reference the Dynamic Property Iteration Using Key Paths proposal. This discussion was heavily inspired by KeyPathIterable which uses a collection of key paths to traverse the structure of a given type. The major difference between these solutions is that Reflectable integrates with the existing Mirror API. If however, synthesizing an array of key paths at compile time is found to be faster, the solution discussed in this post is flexible enough to accommodate such a change.

Implementation Path

Stage 1: Opt-in Metadata

Introducing opt-in metadata with Reflectable will likely be straightforward. Building on this, a version of @synthesized that just extends Reflectable should also be quite simple.

Stage 2: ReflectedFields

Crafting the API of a new reflection mechanism should be a thorough process, but the implementation can simply rely on the existing standard-library _forEachField function.

Stage 3: Dynamic → Static

Introducing inlining for static synthesis, and @synthesized(__statically) by extension, will likely be straightforward in terms of schematics, but will have a challenging implementation.


I’m interested to hear your thoughts!

20 Likes

I’m delighted to see further work in this area, and in particular the example of writing synthesized Hashable conformances in Swift is fantastic and instructive as to the envisioned goal.

However, I have to say that I am quite lost in reading this text. I don’t understand, for instance, the following sentence or why this functionality requires a new attribute:

What is the distinction between this attribute, then, and writing where Self: Reflectable?

2 Likes

Also delighted to see interest in developing this part of the language!

I may be misunderstanding, but isn't this missing a as? Hashable when accessing the property via its keypath? Or is there some kind of magic going on?

Since this is pitched as a discussion, I'll point out that ultimately, I think that if we really want to build a complete static reflection system, we'll need a macro system - i.e. a way to write a program whose input is Swift code, and which processes that code to produce more Swift code. Reflecting fields is a welcome step that would solve some basic use-cases, but it is still rather limited - for example, it is not clear to me what a "field" means to an enum.

Don't get me wrong - it is a great step forward, and it would allow for plenty of exciting use-cases (simple form UI generation, ORMs, etc). But "The Future of Metaprogramming in Swift" should, IMO, really involve some kind of macro system. We have rich APIs for inspecting Swift code already -- they're just not made available to every Swift program.

7 Likes

Thank you for the feedback, I'll revisit the text later to try to clarify some points.

A type conforming to Reflectable tells the compiler to always emit metadata; @synthesized does this selectively. For example, if the compiler deems that an @synthesized declaration can be statically synthesized (not relying on runtime reflection), no metadata will be emitted. This difference ensures that the compiler can distinguish between reflectability for debugging, and reflectability for automatic/synthesized conformances. Thus, the compiler should be able to optimize more effectively.

Another minor difference is that if the given protocol doesn't inherit from Reflectable, @synthesized can implicitly add metadata, whereas where Self: Reflectable can't:

protocol DogA { func bark() }
extension DogA where Self: Reflectable {
  func bark() {}
}

// ❌ Doesn't conform
struct TypeA: DogA {}

protocol DogB { func bark() }
extension DogB {
  @synthesized func bark() {}
}

// ✅
struct TypeB: DogB {}
1 Like

Yes, thank you!

Could you elaborate on these use cases? They sound very interesting.

For form generation, you could imagine something like the following:

struct Person: AutoForm {
  var name: String
  var dateOfBirth: Date
}

protocol AutoForm {
  var form: some View { get }
}

extension AutoForm {
  @synthesized
  var form: some View {
    VStack {
      ForEach(ReflectedFields(Self.self)) { field in
          if let stringValue = self[keyPath: field.path] as? String {
            HStack { 
              Text(field.name)
              Text(stringValue)
            }
          } else if let dateValue = self[keyPath: field.path] as? Date {
            DatePicker(field.name, .constant(dateValue))
          } else {
            field.diagnose(.error, "No view for \(field.name)!")
          }
       }
    }
  }
}

But, you know... better. Probably there would be some kind of protocol for types to customise their presentation, rather than a hard-coded list of if statements, and the form could store an instance of the type and provide bindings to set properties. Maybe you'd have some simple property wrappers to say that an Int should be presented as a slider, etc.

It wouldn't replace full SwiftUI for bespoke interfaces, but there are situations where small components or sections of a view could probably be generated from a data model. Then you can spend more of your time stitching those components together and customising them with modifiers, rather than tediously translating each item of the data model in to views.

EDIT: hacked a proof-of-concept together before dinner. Using suuuuuper dodgy techniques to call private stdlib APIs.

Similarly, you could generate CoreData models/SQL table definitions from a struct. Xcode actually has this ability already, but you start by defining your model in a graphical editor and generate a Swift object from that. With reflection, you could start with a Swift object and generate a database schema.

4 Likes

This is very exciting, thanks for putting this together @filip-sakel!

I think this direction is great, and will add a great deal of expressivity to the language. Particularly, the diagnostic infrastructure is quite nice. I only have one concern:

Implicitly making the fields of a type part of its API is a dangerous proposition. In the following example you give, TypeB may be defined in another module, whose author has no idea someone is using @synthesized to implement bark(). It is possible that innocuous changes like re-ordering private properties can break the implementation of bark().

Introducing this to the language would force library vendor to defensively assume the field layout of all public types is automatically part of the API, which feels very un-Swifty. I strongly believe we need a marker for “this type’s fields are a part of its API”, alongside the “We emit runtime metadata for this type” flag.
Types which do not opt in to having their layout part of the API should refuse to synthesize methods, and types should also be allowed to customize this behavior. The latter can be very important to evolving an API whilst maintaining backward compatibility, as well as manually laid out types (i.e. ones which manually decode their storage as bits).

UPDATE: One other important thing to consider is how visibility interacts with Field.path. For public settable properties, it should probably be writable, but what about private settable properties? Should you even be able to read private properties using ReflectedFields? Maybe we should provide a non-readable non-writable keypath for properties which would not be visible where the reflected fields are accessed.

Just to be clear, if TypeB were defined in another module, the @synthesized method would need to be public. But I agree, this is a good point. Some types, like Hashable, though, are not affected by reordering. So, perhaps we could add explicit and implicit synthesis:

protocol Dog {
  var name: String { get }
  var childNames: [String] { get }
  var isParent: String { get }
}

extension Dog {
  // Gets the names of the children in the order 
  // they were declared.
  @synthesized(explicit)
  var childNames: [String] { ... }

  // Whether there are children
  @synthesized(implicit)
  var isParent: Bool { ... }
}

// ❌ Conformance to dog may only be synthesized explicitly
struct Luna: Dog {
  let name = "Luna"
}

struct Max: @synthesized Dog {
  let name = "Max"
}

struct Bear: Dog {
  let name = "Bear"

  var childNames: [String] { ... }
}

When applying @synthesized to a declaration, the default should probably be explicit, so that authors can opt into implicit API when they know it's safe to do so.

I hope this addressed your concern; let me know if I misunderstood.

This is really interesting.

Popular frameworks, like Combine, use key paths to access private properties synthesized by @Published. Furthermore, Mirror already accesses internal properties. In the proposed model, the client even has to mark their type Reflectable or conform to a protocol with @synthesized witnesses. This means that the client gives their permission for reflection to happen, so I think reading private properties is fine.

Mutating private properties is a bit more nuanced. On the one hand, since reading private properties is fine, how much worse could mutating them be? On the other hand, API authors can just box the types they need to mutate (for example their property wrappers), to mutate them and restrict the abuse potential. I'm inclined towards not allowing mutation of private properties, at least for the beginning. Then, if folks find real-world limitations after using the initial feature, we can of course open up the model.

Doesn’t @Published only provide a notification on the objectWillChange publisher? I might be wrong but I don’t think it actually allows access to the value. Mirror does allow access but I’d argue this is one of the shortcomings of Mirror.

Another approach would be to have a per-property “include in reflection” flag, which would by default include public properties and exclude private properties but allow customization with something like @reflection(IncludeInReflectedFields). Allowing such customization may be nice for other reasons (like changing the name of the property when it is being reflected).

I don’t think this is either sufficient or desirable. We don’t force folks to opt-in to default implementations of protocol methods, and I’m not sure we should do so for reflected implementations.
In your new example, the module where Luna is defined, does not need to know that the Dog Protocol exists, uses reflection or even that Luna conforms to Dog (It could be a retroactive conformance). Yet the mere fact that Luna conforms to Dog makes the reflected structure of Luna part of its public API. To make it clearer, it is the author of Luna that needs to opt-in to reflectability, not the author of extension Luna: Dog.
This means that any public type could possibly participate in this reflection dance, thus the structure of all public types is part of their public API (without even a mechanism to opt-out!) which seems highly undesirable (and even more undesirable in specialized use cases like library evolution).

Currently (compiler) synthesized conformances can only be declared in the same file as the type definition, I would expect that to remain mostly true with library synthesized conformances.

Specifically, I expect library synthesized conformances to only be declarable if in the same file as the type, or the type explicitly conforms to Reflectable, which (I think) itself can only be conformed to in the same file as the type definition.

3 Likes

I like that generate conformance by optimizable swift code.

By the way, Swift uses sharp symbol for static constant derived from compiler.

So I propose

let fields = #reflectedFields(Self)

instead of

let fields = ReflectedFields(Self.self)
1 Like

Thank you for bringing these issues up.

I agree that a type using @synthesized witnesses to satisfy a conformance should only be allowed if this conformance is in the same file as the conforming type.

Reflectable conformances, though, should, in my opinion, be allowed within the module of the conforming type, as is the case for the only other marker protocol: Sendable.

Perhaps that would be a good fit for conformances that should always be static, similarly to what @synthesized(__statically) would do. However, the proposed model offers a mix of dynamic and static reflection, making an API that alludes to fully static APIs, like the #-prefixed symbols, inappropriate.

I think, @synthesized(__statically) implies compile-time execution of the code, which itself should be implemented with a couple of pitches.

Right. To be clear what's proposed is just a model, without any concrete features. But yes, the model does heavily depend on evaluating code at compile time. I believe some progress has been made on this on the compiler front, but, of course, no publicly available features exist.

This was extremely helpful! I'm encountering an unexpected hurdle in my effort to compile Swift for TensorFlow on Xcode release toolchains. I'm trying to achieve this goal so that S4TF can run on iOS once the Metal backend is fully functional, and so that people can compile it without downloading a large release toolchain from Swift.org.

It's possible to bypass the restriction on importing _Differentiation via my differentiation package. The current (Swift 5.6.1) toolchain is a bit far behind on AutoDiff fixes, although my current experiments with a Metal backend only cover a small subset of the S4TF code base and shouldn't trigger compiler crashers yet. By the Swift 5.7 release toolchain, the last of the S4TF crashers should be integrated into mainline. Thus, I hope to add built-in support for compiling S4TF on release toolchains very soon (Summer 2022). Being able to pull this off so soon is a miracle.

:tada: :tada::tada:

Or not.

When the compiler encounters the line @_spi(Reflection) import Swift, I come across a very pleasing message:

file.swift:1:2: warning: '@_spi' import
of 'Swift' will not include any SPI symbols;
'Swift' was built from the public interface
at /Applications/Xcode.app/Contents/
Developer/Platforms/MacOSX.platform/
Developer/SDKs/MacOSX12.3.sdk/usr/lib/swift
/Swift.swiftmodule/arm64e-apple-macos.swiftinterface
@_spi(Reflection) import Swift
 ^

Examining documentation for @_spi(spiName) in swift/UnderscoredAttributes.md at main · apple/swift · GitHub, it is a special form of API called "System Programming Interface". It comes in an extra .private.swiftinterface file when compiling a Swift module with library evolution. I don't know whether the error is because that interface file is missing.

As @filip-sakel mentioned above, @_spi(...) stuff such as _forEachField is only available to the standard library and Foundation in release toolchains, not user-facing code. The built-in Xcode toolchain strips SPI symbols from the Standard Library, so that end-users can't use it. S4TF never had a problem with this before, because it was either (a) part of the Stdlib itself or (b) built on a development toolchain. Compiling it with the -parse-stdlib flag like I do with philipturner/differentiation still doesn't fix the error. It is technically possible to overcome this hurdle, and moreover by using the same approach as _Differentiation. But I'm not yet sure if that's a good idea.

The package IntrospectionKit by @technogen is very similar in purpose to philipturner/differentiation. This package plays with the foundational building blocks of dynamic runtime reflection: pre-compiled C++ functions called at runtime by Swift code. Its design relates to how Differentiation uses runtime functions in the Builtin module for calculating gradients.

For things like retaining and releasing objects, Swift code calls into C-interface symbols called swift_retain and swift_release. There are also symbols such as swift_allocObject, etc. implemented in C++ code. These can be seen in IR and assembly code when compiling Swift, and they are used within the standard library. For reflection, there are a plethora of similar symbol names, mentioned at the top of ReflectionMirror.swift:

swift_isClassType
swift_getMetadataKind
swift_reflectionMirror_normalizedType
swift_reflectionMirror_count
swift_reflectionMirror_recursiveCount
swift_reflectionMirror_recursiveChildMetadata
swift_reflectionMirror_recursiveChildOffset
swift_reflectionMirror_subscript
swift_reflectionMirror_displayStyle
swift_reflectionMirror_quickLookObject
_swift_stdlib_NSObject_isKindOfClass

But these alone aren't enough to use runtime reflection in S4TF. I need the function _forEachFieldWithKeyPath, which is used in EuclideanDifferentiable.swift among many other places. In the standard library, it's at the bottom of ReflectionMirror.swift. I made an attempt at copying-and-pasting the Swift code there into a script, but it quickly became intractable. I started accessing internal types and private properties, and the script grew to 1500 copy-pasted lines. Sounds like I should reorganize it into another Swift package like _Differentiation.

But then again, we have @technogen's package, which I will have to investigate more thoroughly. My concerns are whether I can reconstruct the behavior of _forEachFieldWithKeyPath using his library, and whether I should. I don't know the performance delta between the Swift stdlib's private code and his lightweight package. Maybe it's faster, slower, or it doesn't matter.

Adding another third-party dependency could complicate the build process. If functions in ReflectionMirror are never going to become part of mainline Swift, then I must use either IntrospectionKit or Swift stdlib copypasta indefinitely. Or there's a third option: look at the history of S4TF's usage of this troubling _forEachFieldWithKeyPath (please help @dan-zheng), and either revert to a prior implementation or make a new one entirely. Ultimately, I'm trying to decide: what's the best solution for my use case in the long term? I need some outside input here.

Meanwhile, I can continue secretly prototyping the Metal backend for S4TF. But I can only use toolchains downloaded from Swift.org and can't run the prototype on my iPhone's A15 GPU :upside_down_face:.

2 Likes

I think a reflection proposal powerful enough for your use case (and not just a refinement) will take time to implement. I hope to work on this during the summer because I’m currently very busy, but I may only be able to implement the Reflectable part of the above model.

I haven’t used introspectionKit, so I don’t really know it’s performance characteristics. However, projects that need performant reflection, usually recreate the type layouts in swift code and use these to get runtime metadata, which is what IntrospectionKit, if I understood your post correctly. Such approaches avoid the main performance pitfalls of the Mirror API such as casting, getting all properties at once, etc.

If you want a more mainstream reflection library, I recommend Echo which also offers key-path based access to elements, like _ forEachField. Another options is to look at other projects using high-performance reflection, such as TomamakUI, which reimplemented a large part of the reflection runtime in a module inside the overall Tokamak package last time I checked. If you’re looking to build _ forEachField yourself, I recommend not relying too much on internal code, because it could obviously break at any moment.

1 Like

What we have there isn't a reimplementation, but a stripped-down version of the Runtime library. We initially had the library as a SwiftPM dependency, but during one of our previous investigations of runtime issues it was more convenient to have a subset of that code embedded directly in our codebase. Just easier to tweak and debug it that way.

1 Like

I would very much want to see a super-powerful reflection mechanism in Swift and be able to implement an API similar to SwiftUI without resorting to hidden compiler magic. I would, however strongly push for "inheritable opt-in reflectivity". The @introspectable or @reflectable be applied to a protocol that relied on the conforming type to be available for run-time introspection. This attribute could only be applied (or inherited through protocol conformance) on the main declaration and not through an extension (at least not outside of the file where the type is declared). All of this would be to give the programmer the flexibility to opt parts of their code from language features that come with serious security risks, which could be unacceptable for some cases.

2 Likes