perl5 had broken attribute handling forever
perl5 attributes were invented to provide extendable hooks to attach data or run code at any data, and made for nice syntax, almost resembling other languages.
E.g.
my $i :Int = 1;
sub calc :prototype($$) { shift + shift }
There were a few number of builtin attributes, like :lvalue
,
:shared
, :const
, adding a flag to a function or data, and you could
add package-specific for compile-time or run-time hooks to process arbitrary custom
attributes.
A &MyClass::FETCH_SCALAR_ATTRIBUTES
hook would be called on every
not-builtin MyClass::
scalar attribute at run-time, and
&MyClass::MODIFY_SCALAR_ATTRIBUTES
at compile-time.
If you would want to process attributes in all classes you’d need to add UNIVERSAL
hooks
or use handlers like Attribute::Handlers
.
This would simplify declaring code for attributes.
sub Good : ATTR(SCALAR) {
my ($package, $symbol, $referent, $attr, $data) = @_;
# Invoked for any scalar variable with a :Good attribute,
# provided the variable was declared in MyClass (or
# a derived class) or typed to MyClass.
# Do whatever to $referent here (executed in CHECK phase).
...
}
Note the last $data
argument above. This is the optional argument
for an :Good
attribute, such as in
my MyClass $obj :Good(print a number);
. Do you see the problem?
$data
will be the result of the evaluation of print a number
.
Which will create this error: Can't locate object method "a" via package "number"
.
This would be the correct declaration: my MyClass $obj :Good("print a number");
.
So Attribute::Handlers
is entirely unsafe by evaluating all attribute arguments.
But Damian was right thinking of the use-cases. Attribute arguments are needed to attach certain data to a variable of function. He just didn’t implement it properly, as with all of his modules.
E.g. in cperl we added type support via attributes:
sub calc ($a:int) :int { $a + 10 }
declares calc as returning an int
type, and the
$a
argument to accept int
types.
For the upcoming cperl ffi (Foreign FunctionCall Interface) we need attribute arguments more urgently.
sub random () :native :long;
declares random as native function,
searched in all loaded shared libraries. I.e. libc
must already be
loaded. It is by default, so this works. But for non-default shared
libraries we need to specify the name of the library.
Look e.g. at this perl6 NativeCall declaration:
use NativeCall;
sub mysql_init( OpaquePointer $mysql_client )
returns OpaquePointer
is native('libmysqlclient')
{ ... }
Of course this syntax is not ideal.
returns OpaquePointer
is abbrevated in cperl to:OpaquePointer
.is native('libmysqlclient')
has the syntax:native('mysqlclient')
.- The empty
{ ... }
block is of course left out. Ditto for{ * }
. This is superfluous syntax.
A ffi declaration is just a declaration without a body. The body is looked up by the native
attribute, in the declared library, and optionally under the :symbol('mysql_init')
name.
See the p6 nativecall docs.
cperl syntax:
use NativeCall;
extern sub mysql_init( OpaquePointer $mysql_client ) :OpaquePointer :native('mysqlclient');
The extern sub
declaration is syntax sugar, extern
means the same
as :native
, it just looks better, as in better languages.
Attribute arguments
Now to the :native
argument, the name of library. You saw in the
first zavolaj example the lib
prefix stated explictly. is native('libmysqlclient')
This will not work on windows and cygwin. cygwin needs a cyg
prefix
and a version suffix, the dll is called cygmysqlclient-18.dll
.
On windows the library would be called libmysql.dll
, but this varies
wildly, as there’s no naming convention for shared libs, only for
import libs.
The world is not made for FFI’s, just for linking libraries at
compile-time. There a -lmysqlclient
is enough, on windows this would
find libmysqlclient.dll.a
or libmysqlclient.lib
, which is an
import library which refers to the proper versioned name of the
current shared library. Remember that windows does not solve the
versionining problem of shared libraries via symlinks. One does not
load shared libraries directly on Windows.
So your FFI mysql connector would do some little application logic, like
my $libname = "mysqlclient";
$libname = "cygmysqlclient-18.dll" if $^O eq 'cygwin`;
sub mysql_init( OpaquePointer $mysql_client ) :OpaquePointer :native($libname);
It cannot be solved in the native attribute handler unless you add the
version, like :native('mysqlclient', 18)
. Then the library searcher
can add some magic to find the proper shared library. But it is
usually done application specific.
But all this will not work in perl5, as perl5 has no proper way to resolve the
attribute argument $libname
at run-time. What perl5 does is parsing
:native($libname)
to the string 'native($libname)'
and passes it
to BEGIN { use attributes ... ':native($libname)'; }
.
Note ':native($libname)'
and not ":native($libname)"
,
i.e. $libname
is not expanded to it’s value, and it would not help much
as the call happens at compile-time, so $libname
would have been empty still.
What it should have done instead is to inject the code
use attributes ... "native($libname)";
, which is the equivalent of
BEGIN { require attributes; }
attributes->import(__PACKAGE__, \&msql_init, "native", $libname);
Which means the import call needs to be deferred to run-time. perl5 does this only for my variable attribute parsing, but not for functions.
my $var :native($libname)
would correctly call the importer at
run-time, but sub random() :native($libname);
would falsely call the
importer at compile-time, and the argument would not be parsed at
all. Everything is passed as string to the importer, and the hook
needs to parse the argument. Hence the Attribute:Handler security
nightmare, simply calling eval on all args. Which is a lot of fun
e.g. with a documentation attribute with App::Rad
, when your
docstring is eval’ed.
Now with cperl there are now two kind of builtin attributes. The old
:prototype args are still compiled as barewords, but the new :native
and :symbol attribute args (and probably more upcoming) are compiled
as data, with constant strings being compiled at compile-time, and
scalar values being defered to run-time. Just as with use attributes "native", $libname;
Internally
Internally perl5 has 3 attrs API’s. Two of them are useful, if still broken.
apply_attrs
is the compile-time variant, passing the verbatim string
with the argument to the attribute import call at compile-time.
This translates sub func :native($libname)
to
BEGIN { use attributes __PACKAGE__, \&func, 'native($libname)'; }
.
apply_attrs_my
is the run-time variant, passing the verbatim string
with the argument to the attribute import call at run-time.
This translates my $var :native($libname);
to my $var; use attributes __PACKAGE__, \&var, 'native($libname);
. This is almost
correct. At least the import is done at run-time, and the attribute
handler will have a chance to handle the value of the thing inside the
parens. So eval will work there.
cperl detects in the lexer scalar variables from attribute arguments,
constructs a proper list for the argument,
and passes it to apply_attrs()
, which then tries to detect needed
run-time deferral. And if so calls apply_attrs_my()
instead.
This translates my $var :native($libname);
to
my $var; use attributes __PACKAGE__, \$var, 'native', $libname;
And sub func :native($libname);
to
sub func; use attributes __PACKAGE__, \&func, 'native', $libname;
The third internal API apply_attrs_string
is extremely naive and
only useful to process simple ATTRS:
token in XS declarations. It
cannot handle utf8, and splits arguments by space, not being able to
handle nested parens. And then it calls the importer at compile-time.
In cperl I added an attrs_runtime()
API, which looks at the list of
attrs from the lexer, and calls the runtime variant apply_attrs_my
when
a scalar variable or function call is detected.
So far I treat :native and :symbol barewords as constant strings and
not as function calls.
I.e. :native(mysqlclient)
does not call the mysqlclient function to return the name.
Attribute::Handler would do that. I’ll probably add that with the more explicit
:native(&mysqlclient)
syntax.
Why attributes?
A better idea than attributes to attach data would have been metadata as methods, because then you could also query the current values. With attributes you can only set it.
\&mysql_ffi_fetch->NATIVE = "mysqlclient.6.so";
print \&mysql_ffi_fetch->NATIVE, \&mysql_ffi_fetch->SYMBOL;
This would be independent of packages, and much easier than with
package specific FETCH_CODE_ATTRIBUTES
hooks. You just query the
data. And the magic method would be lvalue, so it could be used as
getter and setter.
But for compatibility with other languages attributes do make a fine
syntax to declare data properties. So cperl will continue to use the perl5
attribute syntax for perl6 traits
.