A 2D array expression doesn’t decay to a pointer to pointer, it decays to a pointer to an array.
Unless it is the operand of the sizeof, _Alignof, or unary & operators, or is a string literal used initialize a character array in a declaration, an expression of type "N-element array of T" will be converted, or "decay", to an expression of type "pointer to T" and the value of the expression will be the address of the first element of the array.
In the call to function, the expression ex decays from type "num_1-element array of num_2-element array of struct example" to "pointer to num_2-element array of struct example", so your function prototype needs to be
void function( struct example (*ex)[num_2] )
{
  ...
  ex[i][j] = some_value();
  ...
}
In the context of a function parameter declaration, T a[N] and T a[] are "adjusted" to T *a, so you could also write that prototype as
void function( struct example ex[][num_2] )
{
  ...
  ex[i][j] = some_value();
  ...
}
There are a couple of problems with this - function doesn’t know how many rows are in the array, so you would need to pass that as a separate parameter:
void function( struct example (*ex)[num_2], size_t rows )
{
  ...
}
Also the function can only accept arrays with num_2 columns - it cannot work on any arbitrarily-sized 2D array.
If your function needs to handle arbitrarily-sized arrays and you have a compiler that supports variable-length arrays, you can do something like this:
void function( size_t rows, size_t cols, struct example ex[rows][cols] )
{
  ...
}
and call it from main as
function( num_1, num_2, ex );
If your compiler doesn’t support VLAs, you’ll have to get creative.  One trick is to pass a pointer to the first element and treat it as a 1D array in the function:
void function( struct example *ex, size_t rows, size_t cols )
{
  ...
  ex[i * rows + j] = some_value();
  ...
}
and call it as
function( &ex[0][0], num_1, num_2 );
Note that this trick only works for 2D arrays defined as
T a[M][N];
or arrays dynamically allocated as
T (*p)[N] = malloc( sizeof *p, M );
It won’t work for 2D arrays allocated as
T **p = malloc( sizeof *p * M );
for ( size_t i = 0; i < N; i++ )
  p[i] = malloc( sizeof *p[i], N );
but in that case, your function prototype would be
void function( T **p, size_t rows, size_t cols )
{
  ...
}
As a final note, main returns int, not void.  Older compilers won’t complain, but the behavior is undefined and could cause problems at runtime.