1

Motivation

I am developing an R package (call it pkg) which collects in a running cache some objects generated by its function calls. It is simple enough to implement the cache as a list object (call it .cache) in the pkg namespace. However, I want to prevent the user from interfering directly with the cache, which can always be exposed via the ::: syntax: pkg:::.cache.

That is, I want to protect .cache analogously to private fields in object-oriented programming. All operations on .cache should be done by internal helper functions, which are called exclusively by @exported functions with an "API flavor".

The Problem

As such, I had the idea of defining an environment object (call it .vault) in the namespace, so I can...put my .cache in my .vault.

3

My quandary is that an environment like .vault is one of the few things in R that work by reference. So I fear pkg:::.vault will expose .vault or its contents to modification as well as to viewing. I can alter my accessor functions like pkg:::.get_cache() so they don't enable such exposure, but all such efforts are in vain if .vault can be directly modified in place.

DO NOT RUN

By way of example, consider this dangerous (?) code below. First I examine the internal .S3MethodsClasses environment from dplyr, to "identify a target":

dplyr:::.S3MethodsClasses
#> <environment: 0x7fab9c6c5310>

ls(dplyr:::.S3MethodsClasses)
#> [1] "grouped_df" "rowwise_df"

Then I examine the rowwise_df object and deem it a worthy "target":

dplyr:::.S3MethodsClasses$rowwise_df
#> Virtual Class "rowwise_df" [package "dplyr"]
#> 
#> Slots:
#> 
#> Name:                .Data               names           row.names
#> Class:                list           character data.frameRowLabels
#> 
#> Name:             .S3Class
#> Class:           character
#> 
#> Extends:
#> Class "tbl_df", directly
#> Class "tbl", by class "tbl_df", distance 2
#  ...

Finally, I exploit the fact that unlike most objects from the dplyr namespace...

library(dplyr)

mutate <- NULL

dplyr::mutate
#> function (.data, ...) 
#> {
#>     UseMethod("mutate")
#> }
#> <bytecode: 0x7fab9c2f6120>
#> <environment: namespace:dplyr>

...an environment like .S3MethodsClasses points by reference, and so its contents can be easily modified in place:

# "Copy" the pointer to allow `<-` assignment.
same_pointer <- dplyr:::.S3MethodsClasses

same_pointer
#> <environment: 0x7fab9c6c5310>



# Modify in place.
same_pointer$rowwise_df <- NULL

dplyr:::.S3MethodsClasses$rowwise_df
#> NULL

And just like that, the "vault" is robbed!

Suspicions

I suspect the answer might lie here, with lockEnvironment() and friends, but the application is a bit beyond me. Perhaps I could do something .onLoad to set up the .vault environment, which would then be lock*()ed — both the .vault itself and the bindings within it — but not before making an active binding between .cache and an accessor function .get_cache(), which would populate within the pkg namespace.

Note

I am already developing a feature to terminate (helper) functions like .get_cache() when they are called outside a fellow function from the pkg namespace. Thus, the exposure of pkg:::.get_cache() will not let the user operate .get_cache() either manually or in a custom function of their own.

Canonicity

I would especially appreciate the advice of R developers experienced enough to provide a canonical answer (if any).

Greg
  • 3,054
  • 6
  • 27
  • 1
    Even if could "lock" something, the user could just "unlock it". R made the decision to trust the user. You cannot hide code or values in a package that the user cannot access. If your package code can access the value, then so can the user. YOu can make the process as inconvenient as you like, but you can't make it impossible That was not a design goal of the language so you're righting an uphill battle. – MrFlick Nov 06 '21 at 05:30
  • Thanks @MrFlick, I suspected as much, given that there is an `unlock*()` for every `lock*()`...and the fact that open source language enables reverse engineering. However, I would like to at least prevent a user's curiosity from "killing the cat", so to speak. – Greg Nov 06 '21 at 05:32

0 Answers0