How to make ZEND_BEGIN_ARG_INFO_EX control number of arguments, passed to a PHP extension?
Asked Answered
M

1

5

I'm developing a PHP extension, using C. So far I'm working on the proper validation of arguments, passed to the extension's function from PHP userspace.

The macro ZEND_BEGIN_ARG_INFO_EX can be used to provide Zend Engine with information about the function's arguments. The 4th parameter of the macro, named as required_num_args, let the engine automatically control the number of arguments, removing this hassle from me. However, I couldn't find the way to make it work: the engine always runs the extension's function without any warnings, even if a PHP script doesn't pass there enough arguments.

Here is my definition of function arguments:

ZEND_BEGIN_ARG_INFO_EX(test_func_swt_arginfo, 0, 0, 3)
    ZEND_ARG_INFO(1, firstArg)
    ZEND_ARG_ARRAY_INFO(0, secondArg, true)
    ZEND_ARG_OBJ_INFO(1, thirdArg, SomeClass, false)
ZEND_END_ARG_INFO()

Here is my definition of the functions, exported by the PHP extension:

static const zend_function_entry test_func_functions[] = {
    PHP_FE(sample_with_types, test_func_swt_arginfo)
    PHP_FE_END
};

Here is my function:

PHP_FUNCTION(sample_with_types)
{
    RETURN_TRUE;
}

Here is the PHP script I run:

<?php
sample_with_types();

Expected result: PHP shows error/warning/exception, something like "not enough arguments are passed to the function"; the function doesn't execute.

Actual result: the function executes and returns true.

How can I properly configure the function arguments structure, so that Zend Engine check the number of arguments automatically? Or do I mistake the purpose of required_num_args argument in ZEND_BEGIN_ARG_INFO_EX macro?

Mclin answered 31/1, 2013 at 21:3 Comment(0)
N
8

As far as I know, this is not what ZEND_BEGIN_ARG_INFO_EX is for.

ZEND_BEGIN_ARG_INFO_EX is a PHP 5 addition is used for producing cleaner code, enabling type hinting, pass-by-reference and reflection. Consider the following arginfo declarations for your actual function that just returns true:

ZEND_BEGIN_ARG_INFO_EX(arginfo_test, 0, 0, 3)
    ZEND_ARG_INFO(0, firstArg)
    ZEND_ARG_OBJ_INFO(0, objNonNull, stdClass, 0)
    ZEND_ARG_OBJ_INFO(0, obj, stdClass, 1)
    ZEND_ARG_OBJ_INFO(1, objByRef, stdClass, 1)
ZEND_END_ARG_INFO()

It has the following effect:

sample_with_types();                          // ok
sample_with_types(1, null);                   // error: arg #2 should be stdClass
sample_with_types(1, new stdClass, null);     // ok
sample_with_types(1, new stdClass, 1);        // error: arg #3 should be stdClass
sample_with_types(1, new stdClass, null, 2);  // error: arg #4 must be reference

Additionally, it provides reflection capabilities to your function:

$ref = new ReflectionFunction('sample_with_types');
var_dump($ref->getParameters());

...giving output similar to:

array(4) {
  [0]=>
  &object(ReflectionParameter)#2 (1) {
    ["name"]=>
    string(8) "firstArg"
  }
  [1]=>
  &object(ReflectionParameter)#3 (1) {
    ["name"]=>
    string(10) "objNonNull"
  }
  [2]=>
  &object(ReflectionParameter)#4 (1) {
    ["name"]=>
    string(3) "obj"
  }
  [3]=>
  &object(ReflectionParameter)#5 (1) {
    ["name"]=>
    string(8) "objByRef"
  }
}

If you omit the arginfo, ReflectionFunction::getParameters() returns an empty array instead.

The required_num_args macro parameter is used specifically for reflection, and denotes how many parameters will be marked required when reflecting the function.

If you need to make the arguments required and not just mark them as required when using reflection, you still have to use zend_parse_parameters, which in most cases, you will still need to get the actual values of the arguments:

PHP_FUNCTION(sample_with_types)
{
    long arg1;
    zval *arg2 = NULL, *arg3 = NULL, *arg4 = NULL;
    zend_class_entry ce2, ce3, ce4;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "looo", &arg1, 
                              &arg2, &ce2, &arg3, &ce3, &arg4, &ce4) == FAILURE)
    {
        return;
    }

    RETURN_TRUE;
}

Note how I used "looo" (generic object types) and not "lOO!O!" (specific object types with null specifiers) in the above. The type hinting already has been specified with arginfo, so there's no need to do it twice.

So, without arginfo:

  • You'd have to use a handful of zend_fetch_class calls and class entries to type hint your object arguments.
  • It wouldn't enable reflection.
  • You wouldn't be able to declare arguments passed by reference.
  • It would obviously produce less clean code.

For obvious reasons, you'll want to make sure both your arginfo declaration and your zend_parse_parameters call matches.

Neritic answered 1/2, 2013 at 16:45 Comment(2)
Thank you @Neritic for the info and efforts to clarify the purpose of that macro. It makes pretty much sense, and actually I had discovered that before asking the question. The main, thing, however, that caused my puzzlement and made me go to SO, was the required_num_args parameter. It seemed like auto-validation of args num, but in practice it does nothing. Could it be, that you know its purpose?Mclin
@AndreyTserkus: required_num_args is used for reflection and denotes when to stop counting parameters as required (i.e.: when calling isOptional()). It doesn't have any effect other than that. (I updated the answer with this information as well.)Neritic

© 2022 - 2024 — McMap. All rights reserved.